Building an encrypted todo list with 3Box (part 1/2)

Make privacy preserving, collaborative and censorship resistant applications with frontend code.

Rachel Black
3Box Labs
Published in
9 min readJul 29, 2020

--

Part 1 : Personal lists

Intro

Earlier this year we were excited to launch confidential threads. Private and encrypted messaging opens up the possibilities of what can be built with 3Box.

This two part series will guide you step by step through creating an individual and team todo application. The first part will get us set up with a personal to do app. The second part will extend this to include team functionality.

As with all products built with 3Box.js, there is no need for a server to manage data. This application is built client side. 3Box.js will persist the data in encrypted form on IPFS without the need for you to run your own node. To take a deep dive in to 3Box’s architecture, check out this blog.

👉 Check out the demo here

🛠 Completed tutorial codebase

Read on for part one, building a todo application with personal confidential threads.

What you will learn in this tutorial

Personal Confidential Threads

We start by learning how to use confidential threads by creating a todo list application for a single user. Here you will learn how to join, save, read and delete data from a confidential thread.

  1. Getting started with the boilerplate code
  2. Installing 3Box.js and opening a space for the application
  3. Joining a personal confidential thread
  4. Saving and getting todos from a confidential thread
  5. Checking done and deleting todos

In part 2 we will be building out this application out to work for groups and teams.

Setup and Tech Stack

For the tutorial, some familiarity with react is required. You can complete by copying and pasting each step, but for a more meaningful understanding, it is recommended you are familiar with the basics.

  • React — frontend framework
  • IPFS + OrbitDB — where the data is stored (provided by 3Box, so we won’t need to touch this directly, no need to install separately )
  • MetaMask — Web3 wallet integration (required to facilitate signing and encryption of data)
  • 3Box.js — 3Box SDK that connects wallets to IPFS database storage via 3ID
  • Profile Hover, and Profile Edit Plugins — drop-in React components that we will use to speed up UI development
  • React Bootstrap — UI Library

1. Getting started with the boilerplate code

To get started let’s clone and install the boilerplate code :

git clone <https://github.com/RachBLondon/todo-template.git>
cd todo-template
npm i
npm start

This boilerplate code is made with create react app. It connects with MetaMask to enable the provider and obtain the users ethereum address. This happens in lines 15–30 in App.js

async getAddressFromMetaMask() {
if (typeof window.ethereum == "undefined") {
this.setState({ needToAWeb3Browser: true })
} else {
window.ethereum.autoRefreshOnNetworkChange = false;
//silences warning about no autofresh on network change
const accounts = await window.ethereum.enable();
this.setState({ accounts })
}
}
async componentDidMount() {
await this.getAddressFromMetaMask()
if (this.state.accounts) {
}
}

This repo also contains some frontend boilerplate components which will be used later in the tutorial (modals, todo lists etc).

2. Installing 3Box and opening a space for the application

First we will install 3Box.js

npm i 3box

and import it to our App.js file

import Box from '3box'

Note do not import as 3Box as variables cannot start with numbers in JavaScript

Now that we have 3Box.js installed, we are going to add the following code to componentDidMount

async componentDidMount() {
// creates an instance of 3Box
const box = await Box.create(window.ethereum)
await this.getAddressFromMetaMask()
if (this.state.accounts) {
await box.auth(['todo-space'], {
address : this.state.accounts[0]
})
// opens the space we are using for the TODO app
const space = await box.openSpace('todo-space')
this.setState({ space })
}
}

First we use Box.create to create an instance of 3Box. This takes a single argument of the ethereum provider so can be called straight away in componentDidMount. The page needs to be loaded, as we are dependant on MetaMask injecting the provider in to the frontend.

Next once the provider has been enabled using the boilerplate function, getAddressFromMetaMask and the address saved to state, we call box.auth. This authenticates the instance of 3Box we have created with an array of spaces (in this case we only have one, but it supports multiple) and the users ethereum address.

If you have worked with 3Box previously, you may have used a different authentication method, Box. openBox. This method is still supported, but there are a number of benefits to using Box.create and box.auth. Firstly you can create the 3Box instance earlier (you do not need to wait for the user to enable the ethereum provider in their wallet). Second you can authenticate with multiple spaces at once. For new projects, we recommend using the approach detailed in this tutorial.

Then we use box.openSpace to open the space which has been authenticated in the previous step. Finally setting the space to the component’s react state for use throughout the application.

When revisiting the app in a browser, you should now be prompted by MetaMask to sign access to the todo space.

Signing a request in MetaMask

We are creating a space first, as confidential threads live inside a user’s space. We will also be using this space to store public and private data later in this tutorial.

3. Joining a personal confidential thread

Now we will start working on personal todos. Here we will be working with the pages/Personal.js component. First we will pass the space we just opened in to the component as a property (this is done in App.js)

<Personal 
accounts={this.state.accounts}
space={this.state.space}/>

We can also wrap the Personal, Team, Profile routes in a conditional preventing them loading before the space. This will allow for easier logic in the components, as they will not render without the space.

{this.state.space && (
<>
<Route path="/personal">
<Personal
accounts={this.state.accounts}
space={this.state.space}/>
</Route>
<Route path="/team">
<Team
accounts={this.state.accounts} />
</Route>
<Route path="/profile">
<Profile />
</Route>
</>
)}

Similarly we can wrap the navigation in the same logic.

{this.state.space && (
<>
<Nav.Item><Link to="/team">Team TODOs</Link></Nav.Item>
<Nav.Item><Link to="/personal">Personal TODOs</Link></Nav.Item>
<Nav.Item><Link to="/profile">Profile Update</Link></Nav.Item>
</>)}

Now opening the Personal Component. Before we start coding, it’s useful to describe the flow in which we will create and join the Confidential Thread.

  • Create a personal confidential thread
  • Save thread address to the user’s private space
  • When revisiting the user will check for the thread address in their private space
  • If the thread address is saved in space, they will join using joinThreadByAddress method

In componentDidMount we will add the following code to achieve the flow described above:

async componentDidMount() {

const threadName = "personal-list"
const confidentialThreadAddress = await this.props.space.private.get(threadName)
let personalThread
if (confidentialThreadAddress) {
// the personal confidential list has been created already
// use it's adddress to join
personalThread = await this.props.space.joinThreadByAddress(confidentialThreadAddress)
}
if (!confidentialThreadAddress) {
// the personal confidential list does not exist
// create it and save the address to the user's private space
personalThread = await this.props.space.createConfidentialThread(threadName)
await this.props.space.private.set(threadName, personalThread._address)
}
this.setState({ personalThread })
// TODO add getPosts
}

Once this is done, you should be able to see the personal thread saved to the component’s react state.

4. Saving and getting todos from a confidential thread

For the personal todo list we will be using a personal confidential thread. Each post within a thread comprises of a single string. We would like to store data for our todo app in the form of a JavaScript object. This can be achieved by using json stringify to turn the object in to a JSON string for storage as a thread post.

First lets look at an example todo object:

const demoToDos = [{
completed: false, // check true when done
id: "1",
order: 1, // used to order the todo in the UI
postedBy: "0xab74207ee35fbe1fb949bdcf676899e9e72ec530",
// user's address
show: true,
text: "Laundry " // todo text
}
]

Now we are going to add the following two methods to retrieve posts:

parsePosts = (postArr) => {
return postArr.map((rawPost) => {
let post = JSON.parse(rawPost.message)
post.id = rawPost.postId
return post
})
}
async getPosts() {
const rawPosts = await this.state.personalThread.getPosts()
const posts = this.parsePosts(rawPosts)
this.setState({ posts })
}

Here we can see we are calling the getPosts method on the personal confidential thread to retrieve the posts. We wont have any posts saved yet, so we won’t see any content. However after calling getPosts in componentDidMount after we set the personal thread to state (see example), you should see an empty array of posts saved in the component’s react state.

// TODO add getPosts
this.getPosts()

Next let’s add some boilerplate code to create a modal to add todos. Inside the render method, after the h2, add the following (see link):

<ModalComponent
buttonText={"Add a ToDo"}
ModalHeader={"Add a Todo"}
ModalBodyText={"One more thing on the list."}
submitFunc={()=>(console.log(`submit a new TODO ${this.state.newTodo}`))} >
<Container>
<Form>
<Form.Group controlId="formBasicEmail">
<Form.Label>New Item</Form.Label>
<Form.Control
type="text"
value={this.state.newTodo}
onChange={(e) => (this.setState({ newTodo: e.target.value }))}
/>
</Form.Group>
</Form>
</Container>
</ModalComponent>

The modal component is already imported in to the top of the Personal Component, so the modal should work. Check it by adding some text to the input inside the modal, clicking submit you should be able to see the text logged out in the browser console.

The modal component adds the new todo to react state (this.state.newTodo), so far however, it is not being saved to 3Box. To fix this we need to modify the submitFunc which is passed as a property to the modal component. To handle this, let’s create the following method:

onSubmit = async () => {
// only submit to 3Box if there is a new todo
if (this.state.newTodo) {
// keeps a consistent order of the todo posts in the UI
const orderNumber = this.state.posts.length > 0 ? (this.state.posts[this.state.posts.length - 1].order + 1 ): (1)
const post = JSON.stringify({
text: this.state.newTodo,
completed: false,
show: true,
order : orderNumber,
postedBy: this.props.accounts[0]})
await this.state.personalThread.post(post)
this.setState({ newTodo: "" })
this.getPosts()
}
}

On submit we add an order number to keep a consistent order of the todo posts, stringify the object so it can be saved as a post in the thread, use thread.posts method to post to 3box, set to react state and finally call the thread.getPosts method we have just made.

Now lets replace the submitFun property in the modal component with the onSubmit method:

submitFunc={this.onSubmit}

Once has been added, you will be able to save a personal todo to the users personal confidential thread. You can check this is working by adding a new todo and checking react state that it is being saved in there. We can render the posts in the UI by adding the code below:

{this.state.posts &&
<TODO
posts={this.state.posts}
deletePost={()=>(console.log("deletePost"))}
toggleDone={()=>(console.log("toggleDone"))}
accounts={this.props.accounts} />
}

5. Checking done and deleting todos

As you can see, we still need to add the code to check done / undone (toggleDone) and delete the todos (deletePost). To do this add the following methods:

toggleDone = async(todo)=> {
const post = JSON.stringify({
text: todo.text,
completed: !todo.completed,
show: true,
order : todo.order,
postedBy: todo.postedBy
})
await this.state.personalThread.post(post)
await this.state.personalThread.deletePost(todo.id)
this.getPosts()
}
deletePost = async (postId) => {
await this.state.personalThread.deletePost(postId)
await this.getPosts()
}

Here we can see we are creating a new todo object with the inverse completed property of the selected one. We then use the thread.deletePost method to remove the original post. Similarly in deletePost we also make use of this method to remove the todo. In both cases we are calling the getPosts method to update the todos in the app.

Now let’s add the TODO component, so we can view in the UI.

<h2>Personal TODOs</h2>
{this.state.posts &&
<TODO
posts={this.state.posts}
deletePost={this.deletePost}
toggleDone={this.toggleDone}
accounts={this.props.accounts} />
}

Checking it out in the browser, you should be able to add a todo, delete a todo and toggle the status of a todo.

🎉Congrats, you have built your own decentralised, user controlled todo list! Join us for part two where we will building this out further to include team todos, stay tuned!

If you have any questions or feedback, jump in to our discord channel!

--

--