Skip to content

Same device on phones

Most customers verify on the phone they are browsing with. On a phone the QR code is a button: a tap opens the wallet, the holder approves, and the site’s tab, still open, turns green. DriveNow is the worked example: a car-subscription site that confirms a driving licence before payment.

The DriveNow licence step with the RTA Wallet box
flowchart LR
    T([Tap on the QR]) --> D{Digital Credentials API?}
    D -- yes --> S[System ID sheet]
    D -- no --> L{Verified link for this wallet?}
    L -- Android --> AL[App Link https://your-site/w/id]
    L -- iOS --> UL[Universal Link https://wallet-host/open]
    L -- neither --> DL[openid4vp:// deep link]
    S & AL & UL & DL --> W([Wallet opens with the request])
Path What the customer sees What your site needs
Android App Link the wallet opens, no chooser a /w/<id> route on your site plus /.well-known/assetlinks.json naming the wallet
iOS Universal Link the wallet opens, no prompt the wallet’s link host in link-host; the wallet’s site serves the association file
Deep link openid4vp:// the wallet opens, sometimes after an app chooser nothing; the fallback
Digital Credentials API the phone’s own “share your ID” sheet dc-api on the box; the wallet must be registered with the OS

The box tries them in that order and falls back on its own. On a laptop the QR is scan-only.

  1. Give the box the wallet’s link settings.

    licence-step.tsx
    // DriveNow's licence step: the same box, with the wallet's link settings for same-device hand-off.
    'use client'
    import { useEffect, useRef, useState } from 'react'
    import type { WalletVerifyElement, VerifyResult } from '@wallet/verify'
    const ASK_FOR =
    'Given names=given_name, Family name=family_name, Driving licence number=document_number, Photo=portrait, Age over 21=age_over_21, Licence expiry=expiry_date'
    export function LicenceStep({ onVerified }: { onVerified: (claims: Record<string, unknown>) => void }) {
    const ref = useRef<WalletVerifyElement>(null)
    const [waiting, setWaiting] = useState(true)
    useEffect(() => { void import('@wallet/verify') }, [])
    useEffect(() => {
    const el = ref.current
    if (!el) return
    const handler = (e: Event) => {
    const r = (e as CustomEvent<VerifyResult>).detail
    if (r.state !== 'verified') return
    setWaiting(false)
    onVerified(r.claims) // then confirm on the server, as in tutorial 1
    }
    el.addEventListener('verified', handler)
    return () => el.removeEventListener('verified', handler)
    }, [onVerified])
    return (
    <section aria-busy={waiting}>
    <wallet-verify
    ref={ref}
    endpoint="https://hakim-verify-api.vercel.app/v1/pk_live_yourkey"
    wallet-name="RTA Wallet"
    wallet-logo="/rta-logo.png"
    fields={ASK_FOR}
    ios-scheme="abdulhakimsgwallet"
    link-host="hakimwallet.abdulhakimsg.com"
    app-link="true"
    />
    </section>
    )
    }
  2. Serve the App Link target (Android). A verified https link opens the wallet directly; the route turns it into the session’s deep link.

    app/w/[id]/route.ts
    // Android App Link target on YOUR site (Next.js: app/w/[id]/route.ts). A verified https link opens
    // the wallet directly; this route turns it into the session's deep link.
    import { api } from './verify-api'
    export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }): Promise<Response> {
    const { id } = await params
    const session = await api().describeSession(id)
    return Response.redirect(session.deepLink, 302)
    }
    public/.well-known/assetlinks.json
    [{ "relation": ["delegate_permission/common.handle_all_urls"],
    "target": { "namespace": "android_app", "package_name": "<the wallet's package>",
    "sha256_cert_fingerprints": ["<the wallet's signing certificate>"] } }]

    The wallet’s manifest lists your host, so this is agreed per wallet. For the demo wallet, send us your host name.

  3. Pass the Universal Link host (iOS). The box builds https://<link-host>/open?…; if the wallet is installed iOS opens it there, otherwise the wallet’s site bounces to its private scheme. You only set link-host.

  4. Turn on the Digital Credentials API (optional) with dc-api on the element. Where the browser supports it, the whole exchange happens in the system sheet. Where it does not, nothing changes.

Next: Go live.