Skip to main content

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.4 with 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:

  • storageEncryptionKeyProvider must return a stable key across launches — losing the key means losing access to stored credentials. Store it in Android EncryptedSharedPreferences / iOS Keychain.
  • promptHandler is required for issuance and presentation. It is how the SDK asks your app to render consent screens. See Prompts — most apps use WalletPromptRegistry rather 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:

final result = await walletSdk.auth.register(username: '[email protected]');

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​