Deep Links
Wallet flows start from URIs — QR codes render them, and other apps or web pages open them as deep links. The SDK handles two schemes:
| Scheme | Flow | Example |
|---|---|---|
openid-credential-offer:// | Issuance | openid-credential-offer://?credential_offer_uri=https%3A%2F%2F... |
openid4vp:// | Presentation | openid4vp://?request_uri=https%3A%2F%2F... |
WalletSdk.handleLink classifies an inbound URI and, for recognized links, performs the intake work (offer normalization, presentation resolution and matching) so your app only decides which screen to show.
Dispatching a URI
final result = await sdk.handleLink(
HandleLinkInput(
uri: inboundUri, // from a QR scan, app link, or paste field
walletExternalId: apiWalletId,
),
);
switch (result) {
case HandleLinkCredentialOffer(:final normalizedOfferUrl):
openOfferScreen(normalizedOfferUrl); // → acceptOffer
case HandleLinkPresentation prep:
openPresentationScreen(prep); // → present / presentation.start
case HandleLinkExpired():
showMessage('This request has expired.');
case HandleLinkUnsupported(:final message):
showMessage(message);
case HandleLinkFailed(:final message):
showMessage(message);
}
Result types
HandleLinkCredentialOffer
The URI is a credential offer. normalizedOfferUrl is ready to pass to acceptOffer as the offer value.
HandleLinkPresentation
The URI is an OpenID4VP request. The SDK has already resolved it and matched stored credentials:
| Field | Description |
|---|---|
preparedPresentation | PreparedPresentation — resolved request, definition, verifier name, trust verdict |
matchedCredentials | Stored credentials satisfying the definition |
requestedCredentialRawTypes | Credential types the verifier asked for |
originalPresentationUrl | The original inbound URL |
presentationResponseUri / presentationDefinitionUri | Endpoint details from the request |
queryState | OpenID4VP state value |
openId4VpClientMetadata | Verifier client_metadata |
resolvedVerifierMemberId | Trust-network member id, when resolvable |
Feed these into the session API:
case HandleLinkPresentation prep:
final session = await sdk.presentation!.start(
StartPresentationRequest(
walletExternalId: apiWalletId,
presentationOfferUrl: prep.preparedPresentation.resolvedPresentationRequest,
verifier: verifierFrom(prep.preparedPresentation),
prepared: prep.preparedPresentation,
did: holderDid,
originalPresentationUrl: prep.originalPresentationUrl,
verifierMemberId: prep.resolvedVerifierMemberId,
),
);
final result = await session.present();
Terminal results
HandleLinkExpired— the presentation request has expired; route to an expiry screen.HandleLinkUnsupported— the URI is not a wallet link; show themessage.HandleLinkFailed— a recognized link failed during intake; show themessage.
Classifying without dispatching
To validate scanner input before running intake (e.g. enabling a "Continue" button), use WalletUriClassifier:
switch (WalletUriClassifier.classify(scannedText)) {
case WalletLinkKind.credentialOffer:
case WalletLinkKind.openId4Vp:
enableContinue();
case WalletLinkKind.unsupported:
showHint('Not a wallet QR code');
}
Platform registration
Register the schemes so the OS delivers links to your app.
Android — AndroidManifest.xml
<activity android:name=".MainActivity" ...>
<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>
<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>
</activity>
iOS — Info.plist
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>openid-credential-offer</string>
<string>openid4vp</string>
</array>
</dict>
</array>
Listening in Flutter
Use a deep-link plugin such as app_links to receive URIs and forward them to handleLink:
final appLinks = AppLinks();
// Cold start.
final initial = await appLinks.getInitialLink();
if (initial != null) await dispatch(initial);
// While running.
appLinks.uriLinkStream.listen((uri) => dispatch(uri));
Future<void> dispatch(Uri uri) async {
final result = await sdk.handleLink(
HandleLinkInput(uri: uri.toString(), walletExternalId: apiWalletId),
);
// switch on result ...
}
A QR scanner yields the same URI a deep link delivers. Route both through one dispatcher so scan, deep link, and paste behave identically.