Present
In person
Section titled “In person”The wallet screen’s Present button does this end to end: a QR code (the engagement), the verifier
scans it, the phones connect over Bluetooth, a consent sheet names the verifier and lists the fields,
Share or Cancel. Only the approved fields leave the phone. The same flow on its own is
WalletPresentScreen (Android) or WalletPresentViewController (iOS). With the API:
package samples
import com.wallet.WalletSdkimport com.wallet.model.PresentmentStateimport com.wallet.model.SdkResultimport kotlinx.coroutines.flow.collect
/** The holder API behind `WalletScreen`, for apps that build their own screens. */class HolderApi(private val sdk: WalletSdk, private val ui: Ui) {
interface Ui { fun showCards(lines: List<String>) fun showQr(payload: String) fun askConsent(readerName: String?, requested: Any, onAnswer: (Boolean) -> Unit) fun showError(message: String?) fun done() }
/** List what the wallet holds; the flow updates as credentials come and go. */ suspend fun listCredentials() { sdk.wallet.credentials().collect { list -> ui.showCards(list.map { "${it.displayName} ${it.docType} valid until ${it.validUntil}" }) } }
/** One credential with its portrait and every field, labelled. */ suspend fun details(credentialId: String) { val detail = sdk.wallet.details(credentialId) detail?.claims?.forEach { println("${it.label}: ${it.value}") } }
/** Receive a credential from an issuer's offer (a scanned QR or a link). */ suspend fun issue(offerUri: String) { when (val preview = sdk.wallet.previewOffer(offerUri)) { // read-only, nothing redeemed yet is SdkResult.Success -> println("Offer from ${preview.value.issuerName}: ${preview.value.credentialName}") is SdkResult.Failure -> return ui.showError(preview.error.message) } when (val issued = sdk.wallet.issueFromOffer(offerUri)) { // after the holder confirms is SdkResult.Success -> println("Issued ${issued.value.displayName}") is SdkResult.Failure -> ui.showError(issued.error.message) } }
/** Present in person: show the QR, let the verifier connect, release only the approved fields. */ suspend fun present(credentialId: String? = null) { val session = sdk.wallet.startPresentment(credentialId) session.state.collect { state -> when (state) { is PresentmentState.Engaging -> state.qrPayload?.let(ui::showQr) // the verifier scans this is PresentmentState.Consent -> ui.askConsent(state.readerName, state.requested) { approved -> if (approved) session.approve(state.requested) else session.cancel() } is PresentmentState.Completed -> ui.done() is PresentmentState.Failed -> ui.showError(state.error.message) else -> {} } } }}import Wallet
/// What your own screens do; the samples leave it to you.protocol HolderUi { func showCards(_ lines: [String]) func showQr(_ payload: String) func askConsent(readerName: String?, requested: Set<String>, onAnswer: @escaping (Bool) -> Void) func showError(_ message: String?) func done()}
/// The holder API behind the wallet screen, for apps that build their own screens.final class HolderApi { private let sdk: WalletSdk private let ui: HolderUi init(sdk: WalletSdk, ui: HolderUi) { self.sdk = sdk; self.ui = ui }
/// List what the wallet holds; the flow updates as credentials come and go. func listCredentials() async throws { try await sdk.wallet.credentials().collect(collector: Collector<[CredentialSummary]> { list in self.ui.showCards(list.map { "\($0.displayName) \($0.docType) valid until \($0.validUntil ?? 0)" }) }) }
/// One credential with its portrait and every field, labelled. func details(credentialId: String) async throws { let detail = try await sdk.wallet.details(credentialId: credentialId) detail?.claims.forEach { print("\($0.label): \($0.value)") } }
/// Receive a credential from an issuer's offer (a scanned QR or a link). func issue(offerUri: String) async throws { let preview = try await sdk.wallet.previewOffer(offerUri: offerUri) // read-only, nothing redeemed yet if let failed = preview as? SdkResultFailure { return ui.showError(failed.error.message) } if let ok = preview as? SdkResultSuccess<OfferPreview>, let offer = ok.value { print("Offer from \(offer.issuerName): \(offer.credentialName)") } let issued = try await sdk.wallet.issueFromOffer(offerUri: offerUri) // after the holder confirms if let ok = issued as? SdkResultSuccess<CredentialSummary>, let credential = ok.value { print("Issued \(credential.displayName)") } else if let failed = issued as? SdkResultFailure { ui.showError(failed.error.message) } }
/// Present in person: show the QR, let the verifier connect, release only the approved fields. func present(credentialId: String? = nil) async throws { let session = sdk.wallet.startPresentment(credentialId: credentialId) try await session.state.collect(collector: Collector<PresentmentState> { state in switch state { case let s as PresentmentStateEngaging: if let qr = s.qrPayload { self.ui.showQr(qr) } // the verifier scans this case let s as PresentmentStateConsent: self.ui.askConsent(readerName: s.readerName, requested: s.requested) { approved in if approved { session.approve(disclosedAttributes: s.requested) } else { session.cancel() } } case is PresentmentStateCompleted: self.ui.done() case let s as PresentmentStateFailed: self.ui.showError(s.error.message) default: break } }) }}- One Bluetooth role per phone at a time: a wallet cannot present while it scans.
- Android wallets can offer the faster “central client” Bluetooth mode (
ProximityConfig). - Presenting over NFC is not available on iOS (Apple restricts it); QR + Bluetooth works on both.
- For credential types you protect, a face check runs after Share and before anything is sent (Prove it’s you); the verifier waits a few seconds longer.
To a website
Section titled “To a website”Websites that use the box reach a wallet three ways. The SDK handles the exchange once you register the entry points.
flowchart LR
S[Website] -- "openid4vp:// deep link" --> A[Your wallet app]
S -- "App Link / Universal Link (https)" --> A
S -- "Digital Credentials API" --> O[System ID sheet] --> A
A -- "signed answer" --> S
Android registers intent filters on the SDK’s activities:
<!-- Credential offers from issuers --><intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="openid-credential-offer" /></intent-filter>
<!-- Presentation requests from websites (deep link) --><intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="openid4vp" /></intent-filter>
<!-- Verified App Links from sites you agree with (opens with no chooser) --><intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="https" android:host="hakimwallet.abdulhakimsg.com" android:pathPrefix="/w/" /></intent-filter>
<!-- The browser's Digital Credentials API (Credential Manager) --><intent-filter> <action android:name="androidx.credentials.registry.provider.action.GET_CREDENTIAL" /> <action android:name="androidx.identitycredentials.action.GET_CREDENTIALS" /></intent-filter>iOS registers URL schemes, an Associated Domain, and (iOS 26) the Digital Credentials provider extension with an App Group:
<!-- Info.plist --><key>CFBundleURLTypes</key><array> <dict><key>CFBundleURLSchemes</key><array><string>openid4vp</string><string>mywallet</string></array></dict> <dict><key>CFBundleURLSchemes</key><array><string>openid-credential-offer</string></array></dict></array><!-- Entitlements --><key>com.apple.developer.associated-domains</key><array><string>applinks:wallet.example.com</string></array><key>com.apple.security.application-groups</key><array><string>group.com.example.wallet</string></array>Then forward the links that open the app to the wallet screen:
import SwiftUIimport Wallet
/// The whole wallet UI: the card list, details, presenting, and scanning credential offers.@mainstruct WalletApp: App { init() { WalletBranding.shared.appName = "My Wallet" } // the name on the consent sheet
var body: some Scene { WindowGroup { WalletScreen() .ignoresSafeArea() // Credential offers and website requests that open the app (URL schemes, Universal Links). .onOpenURL { url in _ = WalletViewControllersKt.handleWalletUrl(url: url.absoluteString, sameDevice: true) } } }}
/// The SDK's wallet screen, hosted in SwiftUI.struct WalletScreen: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { WalletViewControllersKt.WalletViewController(sdk: MyWallet.sdk) } func updateUIViewController(_ controller: UIViewController, context: Context) {}}Call sdk.registerAsIosDcProvider(...) once credentials exist, so Safari’s sheet lists your wallet.
After sharing, the wallet can show its own “shared” confirmation and send the holder back to the site.
Next: Go live.