Providing a good user experience is fundamental to building good products. This is one of the most important aspects that drive user adoption and determine the products that people love. At Bloom, we aim to create amazing user experiences that satisfy the end-user (you)!
While it’s not obvious at first glance, it’s the little things that really count in driving amazing user experiences. User experience specialists focus on things like reducing page load by 50ms and turning “three click” product interactions into “two click” product interactions. If you look at conversion funnels, you’ll see that every additional bit of work the end user has to do, ultimately results in a portion of users giving up part way through the flow.
dApps have room to grow on this front. Most dapps are still clunky and hard-to-use.
For example, imagine you’re creating a decentralized professional social network. What steps does a user who doesn’t own Ethereum need to do in order to sign up and invite another user to connect?
As the cherry on top, each transaction they submit to the blockchain prompts the user with an intimidating and opaque popup:
That is optimistically a 30 minute process that involves installing an extension, signing up for another website, and spending money.
At Bloom, we’re focused on making our dApp user experience simple and easy. In this post, I share some techniques we’ve had success with and how we’re looking to improve even further.
Often times, the design needs of your dApp are at odds with the security requirements of third party wallets like Metamask. In some ways this is obvious — they can’t just share the private key with your website because malicious websites would use that to steal a user’s ETH. Other implications are less obvious — for example, your dApp isn’t allowed to display explainer text underneath the transaction dialogue because a malicious website could put whatever text they want there too. In other words, we can’t tell Metamask to say “This transaction casts a vote in Bloom’s poll” for our poll transactions because then a malicious site could say the same, but actually steal from us.
Whenever the user is going to use Metamask in a new way, our app should explain what we’re doing. Here is what it looks like for a user to vote in a Bloom poll:
Before Metamask opens, we are able to tell the user:
This is a big improvement! Plenty of users aren’t familiar with what gas is and whether they can edit it. Explaining before they see Metamask lets them know things are working correctly.
It is important to have the user click “continue” before they actually see Metamask. Showing the modal at the same time means the user might read Metamask first or miss the popup by focusing on the modal. Even if they see both, they might try to figure out the Metamask dialogue first and not read the instructions in time.
In our most recent product release for Bloom, we added the ability to verify your phone number via SMS and save the verification to the blockchain. This involves two Ethereum transactions, but the end user doesn’t pay anything in gas!
We do this by using the signTypedData API introduced in EIP #712. Metamask exposes this feature which means you can use it in your dApp.
Remember that professional social network example from the start? If we design that dApp around users signing data to approve operations, we can simplify the experience:
Fill out some basic signup information (email, name) like they would for a normal website
The user isn’t submitting the transactions, the backend of our app is, so they don’t have to wait for the Create Account transaction to mine before using the Invitation to Connect feature. As long as our backend verifies the signatures are correct and from the right user, we can ensure the transactions are submitted in the correct order on the backend. Using signTypedData also makes the Metamask dialogues comprehensible!
Implementing this is pretty straightforward in our smart contracts. Here are the important parts:
contract EthedIn {
// Digest describing the data the user signs.
// Needs to match what is passed to Metamask.
bytes32 public constant delegationHash =
keccak256("string Action", "address Address");
// Create account for a user using the signature they provided
//
// * Only approved transaction delegators are allowed to call this
// * _sender should be the address included in the signature as well
// as the address that would be submitting this transaction
// if it were not delegated
function createAccountFor(
bytes _delegationSig,
address _sender
) public onlyTxDelegator {
// Recreate the digest the user signed
bytes32 _delegationDigest =
keccak256(delegationHash, keccak256("Create Account", _sender));
// Recover the signer from the digest.
address _signer = recoverSigner(_delegationDigest, _delegationSig);
// Check that it matches the claimed sender
require(_sender == _signer);
// Call the actual create account function
createAccountForUser(_sender);
}
// Actually create the account
function createAccountForUser(address _address) private { /* ... */ }
}
Once you get it working once, it is easy to adapt to any user facing function you need. A few notes here:
Creating our createAccount in Metamask would look like this:
const user = web3.eth.accounts[0];
const sigInput = [
{ type: "string", name: "Action", value: "Create Account" },
{ type: "address", name: "Address", value: user }
];
web3.currentProvider.sendAsync(
{ method: "eth_signTypedData", params: [sigInput, user], from: user },
handleSignature
);
Building products for the crypto community is tricky — there are a lot of vocal and opinionated users with their own ideas on how things should work. One tough tradeoff is usability vs. purity of decentralization. You’ll see both of these types of users:
The signature oriented functionality makes the new-to-crypto user happy. You should add normal functions the end user can submit to the contracts so that the minority of hardcore users can do so. Adding the direct functionality for createAccount is simple:
function createAccount() public {
createAccountForUser(msg.sender);
}
At Bloom we’re trying to transition all of our existing dApp functionality to use what I’ve described in this post. Every wallet interaction should be explained and confirmed by the user. Smart contract interaction should be facilitated via signature oriented smart contract functionality with an escape hatch for the hardcore users.
Our north star for dApp development is to make our products feel like really satisfying non-blockchain products. Blockchain and asymmetric cryptography should only peek out from under the hood when it makes the app experience better. To give you an idea of what this means:
While we’re very happy with the user experience improvements we outlined in the previous sections, we could do better. Here is what we’re working towards long term:
By batching common product flows, moving transactions behind the scenes via mobile apps, and removing hard dependencies on browser extensions from the on boarding experience, we’ll get much closer to that north star we want.
Did you make it all the way to the end of this post? Are you interested in working on the frontier of dApp development with us? Reach out! [email protected]