Getting Started
This page takes you from an empty Flutter app to a working wallet: SDK installed, initialized, a user signed in with a passkey, and a credential accepted from a QR code.
Prerequisites
- Flutter
>= 3.4with iOS and/or Android tooling - Early-access package credentials from support
- A staging environment base URL (e.g.
https://api.stg.iden2.app) - Your app identity registered for passkeys on the target environment (Android asset links / iOS associated domains)
1. Install
Add the package to pubspec.yaml. During early access the package is distributed as a Git dependency or path package:
dependencies:
wallet_sdk:
path: packages/wallet_sdk # or the Git source provided with your access
dio: ^5.9.0 # recommended: shared HTTP stack
Then fetch dependencies:
flutter pub get
The package ships native plugins for both platforms — FIDO2 and encrypted secure storage on Android, passkeys and Keychain on iOS. No extra native code is required beyond the platform setup below.
Android
Set minSdkVersion 26 or higher and ensure your app is signed with a certificate whose fingerprint is registered in the environment's assetlinks.json (required for passkeys).
iOS
Set the deployment target to iOS 15 or higher and add an Associated Domains entitlement (webcredentials:<your-environment-domain>) so passkey ceremonies can complete.
2. Initialize the SDK
Call WalletSdk.initialize once at startup — typically before runApp. Initialization is idempotent and safe to await concurrently.
import 'package:dio/dio.dart';
import 'package:wallet_sdk/dio.dart';
import 'package:wallet_sdk/wallet_sdk.dart';
late final WalletSdk walletSdk;
Future<void> bootstrapWalletSdk() async {
final dio = Dio(BaseOptions(baseUrl: 'https://api.stg.iden2.app'));
walletSdk = await WalletSdk.initialize(
WalletSdkOptions(
baseUrl: 'https://api.stg.iden2.app',
// Reuse your app's HTTP stack (recommended).
platformHttpClient: DioPlatformHttpClient(dio),
// Returns the SQLCipher key that encrypts wallet storage.
// Generate once, keep in platform secure storage.
storageEncryptionKeyProvider: () => mySecureKeyStore.getOrCreateKey(),
// Your prompt handler renders consent UI when the SDK asks.
promptHandler: myPromptHandler,
),
);
}
Two of these parameters deserve attention before you go further:
storageEncryptionKeyProvidermust return a stable key across launches — losing the key means losing access to stored credentials. Store it in Android EncryptedSharedPreferences / iOS Keychain.promptHandleris required for issuance and presentation. It is how the SDK asks your app to render consent screens. See Prompts — most apps useWalletPromptRegistryrather than implementing the interface by hand.
All other options are covered in Configuration.
3. Authenticate a user
The SDK authenticates users with passkeys — no passwords. Register once, then sign in:
switch (result) {
case AuthSuccess(:final session, :final recoveryCodes):
// Show recoveryCodes to the user ONCE — they are not retrievable later.
print('Signed in as ${session.username}');
case AuthFailure(:final code, :final cancelled):
// Typed failure — e.g. AuthErrorCode.userAlreadyRegistered.
print(cancelled ? 'User cancelled' : 'Failed: $code');
case UsernameRecovered():
break; // Only returned by recovery flows.
}
On subsequent launches, restore the persisted session instead of prompting again:
final session = await walletSdk.auth.restoreSession();
if (session != null) {
print('Welcome back, ${session.username}');
}
See Authentication for login, recovery codes, and logout.
4. Register the wallet locally
Credentials are grouped under a wallet identified by your API wallet id (walletExternalId). After sign-in, mirror the wallet record and holder DID from your backend into SDK storage:
await walletSdk.credentials.ensureWallet(
externalId: apiWalletId, // wallet id from your Platform API
userId: userId,
name: 'Primary wallet',
);
await walletSdk.credentials.upsertDid(
walletExternalId: apiWalletId,
did: holderDid, // did:key / did:jwk from your backend
);
5. Accept your first credential offer
Scan an issuer QR code (or receive a deep link), then hand the offer URI to the SDK:
final result = await walletSdk.acceptOffer(
AcceptOfferRequest(
walletExternalId: apiWalletId,
offer: scannedOfferUri, // openid-credential-offer://...
did: holderDid,
),
);
switch (result) {
case AcceptOfferSuccess(:final credential):
print('Stored credential ${credential.id}');
case AcceptOfferDeclined():
print('User declined the offer');
case AcceptOfferFailure(:final message):
print('Failed: $message');
}
During this call the SDK resolves the offer, checks issuer trust, and raises a CredentialAcceptancePrompt through your prompt handler so the user can review the claims before anything is stored.
6. List stored credentials
final credentials = await walletSdk.listCredentials(
walletExternalId: apiWalletId,
);
for (final credential in credentials) {
print('${credential.id} — ${credential.format}'); // e.g. vc+sd-jwt
}
Complete example
The early-access bundle includes a demo app showing every flow end to end — passkey auth, QR scanning, deep links, issuance, and presentation with selective disclosure. Its bootstrap looks like this:
class SdkBootstrap {
static late final WalletSdk sdk;
static final navigatorKey = GlobalKey<NavigatorState>();
static Future<void> init() async {
final dio = createDio(Env.baseUrl);
sdk = await WalletSdk.initialize(
WalletSdkOptions(
baseUrl: Env.baseUrl,
platformHttpClient: DioPlatformHttpClient(dio),
storageEncryptionKeyProvider: SecureStorageKey.get,
trustNetworkModeProvider: trustNetworkModeFromSettings,
promptHandler: AppPromptHandler(navigatorKey),
),
);
}
}
Next steps
- Configuration — every option explained
- Credential Issuance — the full
acceptOfferflow - Credential Presentation — answering OpenID4VP requests
- Deep Links — routing
openid-credential-offer://andopenid4vp://URIs