Skip to Content
Welcome to RitoSwap's documentation!
DAppWalletConnect Deeplinking

Mobile Wallet Deeplinking

Preface

In building the mobile workflow with wallet integration, Rito experimented with a variety of approaches to connecting mobile wallet apps with the RitoSwap dApp in mobile browsers, mostly deep-link schemes (MetaMask SDK, custom URIs from other wallets, etc.) and studied how leading dApps (like OpenSea) implement them. WalletConnect v2 using the WC URI deeplink proved universally supported and predictably reliable across major mobile browsers and wallet apps, and is already a popular choice among Web 3 users. Other methods proved unreliable or inconsistent so the decision was made to ONLY use WC for external mobile sessions. This minimizes surface area for bugs and allows a focused optimization effort on a single, battle-tested protocol.

This document provides an architectural breakdown of a solution to a notable workflow issue in using Wallet Connect deeplinking on mobile to ensure a smooth user experience. Given WC is the only method of mobile connection supported by RitoSwap, this architecture is critically important.

Overview

Quick Summary

We’ve built a small wrapper around Wagmi’s WalletConnect connector that captures, sanitizes, and persists your mobile deep-link URI so you can programmatically pop up the OS wallet chooser via openWallet().

  • Capture & sanitize the WalletConnect deep-link URI on pairing/tx request.
  • Persist only the session “topic” so you can rebuild wc:<topic>@2 across reloads.
  • Expose an openWallet() helper that does window.location.href = "wc:<topic>@2".
  • Decouple UI from session logic: React components read from a central Zustand store.

Deeper Dive

This integration layer extends Wagmi’s built-in WalletConnect connector to fill two critical gaps:

  1. URI capture & persistence
    While Wagmi fires a display_uri event whenever it generates or receives a WalletConnect deep-link URI, it doesn’t persist that URI or extract the session topic for later use—once you reload the page, all you know is “I’m connected,” not how to reconnect or re-invoke the wallet. Our hook listens for Wagmi’s display_uri events, sanitizes and normalizes the URI (stripping out unwanted HTTP redirects or blacklisted domains), extracts the topic (the 32-byte ID after wc:), and then stores only that topic in a persistent Zustand store. From that topic we can always reconstruct a clean wc:<topic>@2 URI across reloads.

  2. Clean, declarative deep-link helper
    We expose a simple openWallet() action from the store. Under the hood it always rebuilds the standard wc:<topic>@2 URI and assigns it to window.location.href, which triggers the OS “Choose an App” dialog on mobile and hands off to whichever WalletConnect-compliant wallet the user prefers. All of this happens outside of your React components—your UI only needs to read displayUri or call openWallet(), never worry about event listeners, sanitization, or persistence.

By decoupling the UI from session logic (using React components to read state from a central store, rather than manage WalletConnect events themselves), we keep the UX layer simple and focused while providing a rock-solid deep-link plumbing layer for mobile wallets.

UX Problem & Solution

Problem (general dApp pain point)

Most WalletConnect v2 integrations silently send the eth_sendTransaction RPC to the connected mobile wallet in the background. On mobile browsers this means:

  1. The user never leaves the browser automatically.
  2. They have to manually switch to their wallet app, approve the tx, then switch back to the browser to see confirmation.
  3. This manual app switching is confusing, easy to forget, and disrupts a smooth dApp interaction.

Wagmi’s connector helps by emitting display_uri events so your code can know when a URI is ready, but it doesn’t store that URI or topic anywhere—so after a reload you can’t reconstruct your deep-link, and you still have no way to programmatically trigger the wallet chooser at the right moment.

Injected wallets inside a mobile browser (e.g. MetaMask Mobile “in-app”) already avoid this issue—this deep-link layer is only for external WalletConnect wallets.

Solution

  1. Immediate deep-link invocation
    After you call your write/contract method (e.g. mint()), check if displayUri is set and immediately invoke openWallet(). That reliably pops up the OS app chooser so the user can tap their wallet and get straight into approval.
  2. Auto app switch-back
    Most mobile wallets (including MetaMask Mobile) automatically return focus to the browser once the transaction is confirmed, so the user lands back in your dApp without manual navigation.
  3. Cross-wallet compatibility
    Any WalletConnect v2-compliant wallet will listen for wc:<topic>@2. By persisting and normalizing just the topic, we ensure every major mobile wallet like Rainbow, Trust, OneKey, etc., works out of the box—no custom URIs, no edge-case redirects.

Deeplinking is generally the default behavior of first connecting a mobile browser dApp with a mobile wallet app through Wallet Connect. But deeplinking thereafter to interact with a connected wallet is normally absent. Thus the user has experience using deeplinking during the connection process, so continuing to use it for other purposes maintains continuity rather than introducing new hurdles to learn.

Userflow



Screenshots of WC Deeplink Triggering OS Chooser Across Browsers

Chrome Example

Architecture

      • useWalletConnectSession.ts
      • walletConnectStore.ts
      • WalletConnectProvider.tsx

File Responsibility Matrix

File PathResponsibility
app/providers/WalletConnectProvider.tsxBootstraps the global session hook
app/hooks/useWalletConnectSession.tsListens for & sanitizes WalletConnect URIs, manages lifecycle
app/store/walletConnectStore.tsPersists session topic + URI, exposes openWallet() helper
app/mint/components/ButtonSection/ButtonSection.tsxConsumes the store to send txs and invoke openWallet()

1. Global Provider

// app/providers/WalletConnectProvider.tsx "use client"; import { useWalletConnectSession } from '@/app/hooks/useWalletConnectSession'; export function WalletConnectProvider({ children }: { children: React.ReactNode }) { // Activates the global WalletConnect session‐listening hook useWalletConnectSession(); return <>{children}</>; }

Purpose: Ensures useWalletConnectSession() runs once at the top level (e.g. root layout), so WalletConnect wiring is active on every page.

2. Session Hook (useWalletConnectSession)

// app/hooks/useWalletConnectSession.ts import { useEffect, useRef } from 'react'; import { useAccount } from 'wagmi'; import { useWalletConnectStore } from '@/app/store/walletConnectStore'; export function useWalletConnectSession() { const { connector, isConnected } = useAccount(); const { setDisplayUri, setSessionTopic, clearSession } = useWalletConnectStore(); const initRef = useRef(false); const cleanupRef = useRef<() => void>(null); // A) Prevent blacklisted popups/redirects on Android Chrome useEffect(() => { // …preventRedirect & window.open override… }, []); // B) When the WalletConnect connector is active: useEffect(() => { if (connector?.id !== 'walletConnect' || !isConnected) return; if (initRef.current) return; initRef.current = true; (async () => { const provider = await connector.getProvider(); const handleDisplayUri = (uri: string) => { // sanitize, extract topic, then save setSessionTopic(topic); setDisplayUri(cleanUri); }; provider.on('display_uri', handleDisplayUri); // restore existing session after reload if (provider.session?.topic) { setSessionTopic(provider.session.topic); setDisplayUri(`wc:${provider.session.topic}@2`); } cleanupRef.current = () => provider.off('display_uri', handleDisplayUri); })(); }, [connector?.id, isConnected]); // C) On disconnect, clear session useEffect(() => { if (!isConnected) clearSession(); }, [isConnected, clearSession]); return useWalletConnectStore(); }

Key Tasks:

  • Listen for display_uri events from the WC provider.
  • Sanitize & extract the 64‑hex topic.
  • Persist only the sessionTopic.
  • Block unwanted browser popups/redirects on mobile Chrome.
  • Cleanup event listeners on unmount.

3. Persisted Store (walletConnectStore)

// app/store/walletConnectStore.ts import create from 'zustand'; import { persist } from 'zustand/middleware'; export const useWalletConnectStore = create( persist( (set, get) => ({ displayUri: null as string | null, sessionTopic: null as string | null, setDisplayUri: (uri) => set({ displayUri: uri }), setSessionTopic: (topic) => set({ sessionTopic: topic }), clearSession: () => set({ displayUri: null, sessionTopic: null }), openWallet: () => { const { sessionTopic, displayUri } = get(); const uri = sessionTopic ? `wc:${sessionTopic}@2` : displayUri; if (uri) window.location.href = uri; else console.warn('No WalletConnect session found'); }, }), { name: 'wallet-connect-session', partialize: state => ({ sessionTopic: state.sessionTopic }), } ) );
MethodSignatureDescription
setDisplayUri`(uri: stringnull) => void`Store raw or sanitized WalletConnect URI.
setSessionTopic`(topic: stringnull) => void`Store 64‑hex WalletConnect session topic.
clearSession() => voidReset both displayUri and sessionTopic to null.
openWallet() => voidReconstructs wc:topic@2 or uses displayUri, then opens it.

4. Consuming UI (e.g. Mint/Burn Buttons)

Your on-chain components simply:

  1. Send a transaction via Wagmi’s useWriteContract.
  2. Immediately call openWallet() if a displayUri is available.

This keeps UI code decoupled from all session-management logic.


Workflow Diagram

WC Deeplink Swimlane

API Reference

Below is the complete reference for the useWalletConnectStore hook and its returned store interface. Use this to understand how to import, invoke, and interact with the WalletConnect session state in your components.

Import

import { useWalletConnectStore } from '@/app/store/walletConnectStore';

Hook Signature & Description

/** * React hook for accessing and controlling the WalletConnect session. * * Internally backed by Zustand + persistence. * Exposes both session state (deep-link URI + session topic) and control actions. * * @returns {WalletConnectStore} The WalletConnect session store. */ export function useWalletConnectStore(): WalletConnectStore;

Return Type Definition

/** * WalletConnect session store interface. */ interface WalletConnectStore { /** * The sanitized WalletConnect URI for QR-code display or deep-linking. * On reload, this is rebuilt as `wc:${sessionTopic}@2`. */ displayUri: string | null; /** * The 64-hex session topic, persisted across reloads. */ sessionTopic: string | null; /** * Overwrites the stored deep-link URI. * @param uri - A valid WalletConnect URI (e.g. `wc:<topic>@2`) or `null` to clear. */ setDisplayUri(uri: string | null): void; /** * Overwrites the stored session topic. * @param topic - A 64-hex string representing the session topic, or `null` to clear. */ setSessionTopic(topic: string | null): void; /** * Clears both `displayUri` and `sessionTopic` from the store. */ clearSession(): void; /** * Triggers the OS wallet chooser by setting `window.location.href` to the deep-link URI. * Constructs `wc:${sessionTopic}@2` if `sessionTopic` is present; otherwise falls back to `displayUri`. */ openWallet(): void; }

Usage Example: Mint Button

"use client"; import { useAccount, useWriteContract } from 'wagmi'; import { useWalletConnectStore } from '@/app/store/walletConnectStore'; import { KEY_TOKEN_ADDRESS, fullKeyTokenAbi } from '@/app/config/contracts'; export function MintButton() { const { isConnected } = useAccount(); const { displayUri, openWallet } = useWalletConnectStore(); const { writeContract: mint, isPending } = useWriteContract({ address: KEY_TOKEN_ADDRESS, abi: fullKeyTokenAbi, functionName: 'mint', }); const handleMint = () => { if (!isConnected) return alert('Connect your wallet first'); mint(); if (displayUri) openWallet(); }; return ( <button onClick={handleMint} disabled={isPending}> {isPending ? 'Processing…' : 'Mint NFT'} </button> ); }

Error Handling & Edge Cases

openWallet() failures

Behavior

  • Returns true if it successfully set window.location.href.
  • Returns false (and logs a warning) if there’s no session or if the browser blocks the redirect.

How to use

const success = openWallet(); if (!success) { // show UI feedback, e.g. toast("Couldn't open wallet, please try again"); }

Documentation

### openWallet(): boolean Attempts to deep-link into the mobile wallet. - **Returns** - `true` if a URI was found and navigation was attempted - `false` if no session exists or navigation was blocked - **Logs** - `console.log('Opening wallet…')` on success - `console.warn('No WalletConnect session found')` or `console.error('Failed to open wallet')` on failure

Provider initialization errors

Hook exposes

const { hasProviderError } = useWalletConnectSession();

Behavior

  • hasProviderError flips to true if connector.getProvider() throws.
  • The hook will retry on the next render if isConnected or connector.id changes.

How to use

const { hasProviderError } = useWalletConnectSession(); if (hasProviderError) { return <ErrorBanner message="WalletConnect initialization failed. Try reconnecting." />; }

URI sanitization & parsing warnings

What happens

If the incoming URI can’t be parsed, we still store a “best-effort” clean URI and emit:

console.warn('Could not extract topic from URI:', originalUri);

Consumer tip

Watch the console in development; in production you can hook into a logging service (e.g. Sentry).


Best Practices & Notes

  • Wrap Root: Always place <WalletConnectProvider> at your app’s top level.
  • Single Hook: Don’t call useWalletConnectSession() elsewhere—components only use the store.
  • Session Persistence: Only sessionTopic is saved; full URI is rebuilt on mount.
  • Error Handling: Handle RPC errors via Wagmi’s hooks; this layer only manages deep-link UX.
  • Injected Wallets: Skip deep-link for in-browser injected providers (e.g. MetaMask Mobile in-app).

Default Flow: manual app switching → low conversion.

Deep-Linking ():

  • Launches wallet via OS chooser.
  • After tx approval, many wallets auto-return you to the browser.
  • Universal: any WalletConnect v2–compliant app will respond to wc:…@2.