Build your own dApp for iOS
The short answer to this question is yes, it is possible.
In this article, I will tell you how we have implemented it with my colleagues from Custom App, released the W3dding application (currently iPhone X and later supported), what we had to go through and how to make your own native mobile dApp for iOS. If you can’t wait to make your own mobile dApp, here are links to the open-source Wallet Connect library modified by us and example app.
This article will tell mostly about the development of dApp specifically on the iOS platform. For better understanding, you should be familiar with Swift 5 and have general blockchain knowledge.
A couple of months ago, one day, my boss, who has a fairly wide blockchain context, asked me: “Lev, there seem to be some talks about mobile web3 applications, have you heard anything about this?”. After that, I went to research this issue, while having only a general understanding of how the crypto world works (before that, mobile developers did not participate in our crypto projects).
Of course, after a bit of searching for various solutions, I managed to find numerous Swift libraries that work with web3. However, sending transactions directly requires a private key, which no user in their right mind would provide to a 3rd party application. Therefore, using only such web3 libraries may be suitable for writing your own wallet application, but not for implementing a dApp.
I also learned about the possibility of using built-in browsers in wallet applications, but this did not satisfy me, as I was interested in learning about the possibility of implementing a native mobile application.
Further, it was decided to look for any examples of dApp with the specific functionality we need. I looked at many large applications (nft marketplaces, exchanges, etc.), but initially, I could not find a single native mobile application in which it would be possible to send transactions through a wallet. There were applications in which you could only connect to the wallet (for example, OpenSea). It seemed absurd to me that in the mobile application of the largest NFT marketplace it is impossible to make transactions and directly buy NFT. I thought — maybe it’s just not possible?
As I found out later, most of the blockchain browser applications use Wallet Connect technology to connect to wallets and send transactions. Wallet Connect is the protocol by which dApp and wallet interact. In the process of interaction, the dApp and the wallet use an intermediate bridge. To create a new session, dApp sends a request to the selected bridge, after which confirmation of the connection is required from the wallet side. After creating a session, the application can send requests for transactions, which will need to be confirmed in the wallet. In a browser, this connection works as follows: the web application sends a request to create a session and generates a QR code with the information necessary to connect (bridge url, session id, etc.), and the wallet application scans this code.
The Wallet Connect site has a large amount of documentation and examples of working with the protocol when building web applications, but this information is not available for mobile native applications. There is only a Mobile Linking section that says that Wallet Connect can work in mobile applications using deep links. When creating a session, the application sends a request to the bridge server and directs the user to the selected wallet via a deep link, where he confirms the creation of the session.
Next, I found the official Swift Wallet Connect library and an example of its usage. It would seem that all the pieces are put together and you can create your own mobile native dApp. However, after running the example and playing with it for a while, I realized that it is not working as expected. Problems that were found while working with the example:
- It was not possible to connect to some wallets — some wallets simply did not react after they were launched via a deep link, and some others crashed the application after connecting (of course I’m talking about wallets that support Wallet Connect).
- If the wallet is connected successfully, then the session was not managed correctly. It could easily be lost from the dApp side, while it is displayed as active in the wallet app.
- If the wallet is connected successfully, then sending the transaction directly might not work.
In general, the example did not work in its current form. After that, I started looking for any examples or articles about building a mobile dApp. As before, I saw a lot of guides on how to achieve this success when implementing a web application, but there were no materials regarding mobile applications 🙁
I looked at the activity in the above repositories — the last commits were several months ago, a large number of open issues with no response. I researched issues and saw several in which library users themselves solve the problems found, without the help of the authors. Then I found out that at the moment the developers are focused on Wallet Connect v2 (currently in beta), and the first version was left without attention. Therefore, I realized that the only way to use Wallet Connect and have proper functionality is to fork the library and try to fix the problems.
Before making changes, the library was tested using several wallets that support Wallet Connect and the Polygon blockchain: Trust Wallet, MetaMask, TokenPocket, SafePal, Unstoppable Wallet, AlphaWallet, and MathWallet. The following will briefly describe the changes made to the Wallet Connect library. Small fixes will not be described here, such as the implementation of the Identifiable protocol by the Session struct. If desired, absolutely all commits can be viewed in the repository. Changes made:
- Wallet icon made optional
Let’s look at the
ClientMeta and when we receive
WalletInfo JSON without an
icons field the app is gonna crash. That was the case with connecting with a few wallets (for example, Safepal).
2. Added missing percent-encoding
In the original library, there was only bridge field percent-encoding, while the protocol requires a fully percent-encoded deep link. Metamask can handle this case and encode it for you, but you will not be able to connect to other wallets.
3. Disconnect callback when disconnected from wallet side
In most wallet apps users can disconnect sessions. So if there is no handling of this case your app will think that you have an active session, but this is not true.
When you want to connect to a wallet you first need to connect to the bridge and send a new session request. When a request is sent you are ready to open a deep link and prompt the user to the wallet app. In the original library, there was a constant delay before opening the deep link. So if your session request wasn’t sent for any reason, you will not see any message in the wallet app.
5. Added reconnect flag to disconnect callback
Before adding this flag, when receiving a disconnect callback, there was no way to understand if the disconnect was initiated by the wallet/user or if the connection was lost for other reasons (bad internet) and reconnection attempts are in progress.
6. Added id to disconnect request
When disconnecting from some wallets (for example, Trust Wallet) there is a required id field that was missing.
7. Added 1 second reconnect timeout
Wallet Connect library uses Starscream library to manage websocket connections. And when a user disconnects for any reason (for example, bad internet connection) it starts retrying to reconnect very very frequently, which is not good for device resources.
Now let’s move on to a more technical level. In this large section, we will look at using the library with an example and build a standalone dApp. The application will be written using SwiftUI and MVVM architecture. The main points of creating such an application will be considered, but some parts (for example, the View component and some auxiliary functions) will be omitted. The full application code can be viewed in the repository.
The first step is to install a modified version of the Wallet Connect library. It can be done using the Swift Package Manager:
- Using Xcode 13 go to File > Add packages > Click on the top right search bar
- Paste the project URL: https://github.com/penachett/WalletConnectSwift
- Click on next and select the project target
Our application will include all the basic functionality of working with Wallet Connect:
- Ability to connect to one of the wallets (Trust Wallet or Metamask);
- Ability to send a transaction;
- Ability to disconnect;
- Handling all session update events.
Let’s begin with our
Here you see some common fields like name, main wallet url, and App Store url.
We also have 2 types of deep link schemes — universal and native. While Apple recommends using universal schemes, some wallets have only native scheme implementation. Also, native deep link schemes are useful for checking the availability of the wallet app on the user’s device.
Deep link schemes can be hard to find, and not all developers explicitly mention them in their documentation.
Deep link schemes can be found in the repository of one of the Gnosis Safe open source projects. We will also need to direct the user to the wallet app not only when a connection needs to be created, but also when a transaction needs to be confirmed.
linkForOpenOnly function is required for this purpose because some wallets (Trust Wallet in our example) show an error alert that deep-link can’t be parsed and it confuses the user. In this example, I provided a link that opens Polygon in Trust Wallet.
Next, let’s move on to the consideration of our logic, we will have it represented by two classes:
WalletConnect class, we will directly access the library and pass callbacks to the
ViewModel using a delegate that we will implement.
Let’s consider some of the
WalletConnect methods that are not related to library callbacks.
Here in the connect method we are constructing a wallet connect URL, connecting to the bridge server, sending a request to create a session, and returning the url to put it in a deep link later.
Note that here we are using a public bridge https://safe-walletconnect.gnosis.io, there is also another official public bridge — https://bridge.walletconnect.org. Any bridge can sometimes go down and you will be unable to use Wallet Connect.
For big commercial projects, I recommend making your own bridge (how to keep it private is a good question in a decentralized web3 world:)). Also useful is that you can specify
chainId that the user should connect to.
But for MetaMask it does nothing — the user connects to the previously chosen network and needs to switch it. Trust wallet uses provided
chainId by default, but the user can change the network anyway. So you need to always control what
chainId will be in the
Session object after connecting.
Also, remember that Wallet Connect v1 only supports EVM-compatible blockchains. The next method
reconnectIfNeeded resurrects our old session if we have it saved in UserDefaults. It will be called on every app initialization.
Now let’s move to Wallet Connect callbacks.
Here for example, we define our
WalletConnectDelegate (that is made of original
ClientDelegate excluding some info) to callback to our ViewModel that will modify the state.
Session to UserDefaults if we connected successfully and do the opposite if disconnected. We ignore
didConnect with URL because here we are not interested in the moment when we connected to the bridge.
Let’s check this delegate implementation in our ViewModel.
The interesting part here is the
didSubscribe callback. So, when we’ve connected to the bridge and sent a session initialization request, we’re ready to open a deep link and prompt the user to the wallet app. Let’s move forward.
Here are the general ViewModel methods.
disconnect which can be called from the UI,
initWalletConnect which is called every time this application is started,
openWallet which simply opens the wallet (to send a transaction), and the aforementioned
triggerPendingDeepLink. Our last block of code is the transaction sending logic:
Be careful with setting
gasPrice in your transactions. Our wallets (Trust Wallet and MetaMask) will calculate it without us, but for example, Safepal will throw you an unknown error if you didn’t set
It is also a good practice to fetch the user’s balance before sending a transaction, as some wallets will simply return an unknown error to you when trying to send a transaction for which there are not enough funds. Yes, things are not so smooth in this young web3 mobile world 🙂
- Apple doesn’t understand web3 apps. You should be ready that your submissions to the App Store will be rejected multiple times and you will need to explain some things to Apple reviewers. In the beginning, we had to prove that our application is not a wallet (we had a “Wallet” tab with connecting wallet functionality). Later, while sending one of the updates, the problem was that we use authorization through 3rd party apps (wallets). You can always try to explain the essence to them in response, but in case of failure, you will have to make some changes.
- You should always test with all the wallets you support, as they often have quite individual behavior.
- When you send a transaction and open a deep link, your app will have a few seconds before it goes into the background. Therefore, if the user does not confirm the transaction quickly, then you will not be able to receive a response about sending the transaction. To solve this problem, you can use background tasks, which will give an additional minimum of 30 seconds to the life of an application with an active websocket connection.
- There are several “features” when working with MetaMask that you need to know about:
1. Even when the transaction was successfully sent, the user may receive a push notification that something went wrong. This can confuse the user, so it would be good to warn him about it.
2. You cannot force the user to select a specific network. After connecting, it will always have the previously selected network, so you need to watch the chainId in the Session object.
3. By default, metamask works only with Ethereum, but you can add other networks. There is currently no way to use the magic button for adding a network in the native application, so you need to post any instructions for this.
This is how the cycle of creating a native mobile web3 application turned out. Yes, now all these technologies do not work perfectly, but everyone understands that in recent years our life has been striving to move as much functionality as possible to smartphones. Therefore, the web3 mobile revolution will happen sooner or later, and we can only try to bring it closer 🙂
Further, perhaps, I will write an article about the development of a full-fledged web3 application and there will be information about working with web3 libraries, calling smart contracts, and so on, if it is interesting (however, this information is much easier to find than information about Wallet Connect).
You can also see my colleague’s article on integrating NFT smart contracts with OpenSea.