Skip to content

Present

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:

HolderApi.kt
package samples
import com.wallet.WalletSdk
import com.wallet.model.PresentmentState
import com.wallet.model.SdkResult
import 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 -> {}
}
}
}
}
  • 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.

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:

WalletApp.swift
import SwiftUI
import Wallet
/// The whole wallet UI: the card list, details, presenting, and scanning credential offers.
@main
struct 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.