Onboarding
Initialize the PasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClient, then run the onboarding flow that fits your platform.
Initialization
PasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClient is the entry point for every passkey wallet operation. Construct one per app session and reuse it.
On web, iOS, Android, Flutter, and React Native it wires the built-in PasskeyProviderPasskeyProviderPasskeyProviderPasskeyProviderPasskeyProviderPasskeyProviderPasskeyProviderPasskeyProviderPasskeyProvider for you, defaulting to the Breez shared RP (keys.breez.technology): a Breez-registered app needs only its Breez API key. Set provider_optionsprovider_optionsproviderOptionsproviderOptionsproviderOptionsproviderOptionsproviderOptionsProviderOptionsProviderOptions on the config to use your own RP or customize the picker identity. On other platforms, or for a custom PRF backend (hardware key, file-backed), implement PrfProviderPrfProviderPrfProviderPrfProviderPrfProviderPrfProviderPrfProviderPrfProviderPrfProvider and inject it:
let prf_provider = Arc::new(CustomPrfProvider);
PasskeyClient::new(prf_provider, Some("<breez api key>".to_string()), None)
let passkey = PasskeyClient(
breezApiKey: "<breez api key>",
config: PasskeyConfig(
providerOptions: PasskeyProviderOptions(rpId: "<your-rp-domain>", rpName: "Your App")
)
)
val passkey = PasskeyClient(
breezApiKey = "<breez api key>",
activityProvider = { activity },
config = PasskeyConfig(
providerOptions = PasskeyProviderOptions(rpId = "<your-rp-domain>", rpName = "Your App"),
),
)
var prfProvider = new CustomPrfProvider();
return new PasskeyClient(prfProvider, "<breez api key>", null);
const passkey = new PasskeyClient('<breez api key>', {
providerOptions: { rpId: '<your-rp-domain>', rpName: 'Your App' }
})
const passkey = new PasskeyClient(
'<breez api key>',
PasskeyConfig.create({
providerOptions: PasskeyProviderOptions.create({ rpId: '<your-rp-domain>', rpName: 'Your App' })
})
)
final passkey = PasskeyClient(
breezApiKey: '<breez api key>',
config: PasskeyConfig(
providerOptions: PasskeyProviderOptions(rpId: '<your-rp-domain>', rpName: 'Your App'),
),
);
prf_provider = CustomPrfProvider()
passkey = PasskeyClient(prf_provider, "<breez api key>", None)
prfProvider := &CustomPrfProvider{}
apiKey := "<breez api key>"
return breez_sdk_spark.NewPasskeyClient(prfProvider, &apiKey, nil)
Parameters:
| Parameter | Default | Description |
|---|---|---|
breez_api_keybreez_api_keybreezApiKeybreezApiKeybreezApiKeybreezApiKeybreezApiKeyBreezApiKeyBreezApiKey | required | Your Breez API key, used to authenticate to the Breez relay for label storage. |
default_labeldefault_labeldefaultLabeldefaultLabeldefaultLabeldefaultLabeldefaultLabelDefaultLabelDefaultLabel | "Default" | Wallet label used when PasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.RegisterPasskeyClient.Register / PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn receive none. Set on passkey_configpasskey_configpasskeyConfigpasskeyConfigpasskeyConfigpasskeyConfigpasskeyConfigPasskeyConfigPasskeyConfig. |
Configure the built-in provider through provider_optionsprovider_optionsproviderOptionsproviderOptionsproviderOptionsproviderOptionsproviderOptionsProviderOptionsProviderOptions on passkey_configpasskey_configpasskeyConfigpasskeyConfigpasskeyConfigpasskeyConfigpasskeyConfigPasskeyConfigPasskeyConfig (a PasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptions):
| Field | Default | Description |
|---|---|---|
rp_idrp_idrpIdrpIdrpIdrpIdrpIdRpIdRpId | Breez shared RP | Relying Party ID: your app's domain, or unset for the Breez shared RP (keys.breez.technology) if your app is Breez-registered. Changing it later strands existing credentials. |
rp_namerp_namerpNamerpNamerpNamerpNamerpNameRpNameRpName | "Breez" | Display name for your app, shown in some authenticator UIs. |
user_nameuser_nameuserNameuserNameuserNameuserNameuserNameUserNameUserName | rp_namerp_namerpNamerpNamerpNamerpNamerpNameRpNameRpName | Account identifier the OS sign-in picker shows beneath the display name, e.g. john@doe.com. Set a stable per-user value to keep each registration a distinct entry. |
user_display_nameuser_display_nameuserDisplayNameuserDisplayNameuserDisplayNameuserDisplayNameuserDisplayNameUserDisplayNameUserDisplayName | user_nameuser_nameuserNameuserNameuserNameuserNameuserNameUserNameUserName | Human-friendly name the picker shows most prominently, e.g. John Doe. |
For platform-specific provider options (iOS URLSession / presentation anchor, Android Activity, web authenticatorAttachment) or a custom PRF backend, build the provider yourself and inject it. See PRF providers.
Checking passkey availability
Call PasskeyClient.check_availabilityPasskeyClient.check_availabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.CheckAvailabilityPasskeyClient.CheckAvailability before showing the passkey button. One call covers device support and your domain config, so you can hide the option on unsupported devices (older Android / iOS) or surface a configuration error (missing entitlement, undeployed AASA) before the user runs into an opaque WebAuthn failure.
match passkey.check_availability().await? {
PasskeyAvailability::Available => {
// Show passkey as primary option.
}
PasskeyAvailability::PrfUnsupported => {
// Fall back to mnemonic flow.
}
PasskeyAvailability::NotAssociated { source, reason } => {
eprintln!("Domain association failed (source={source}): {reason}");
}
PasskeyAvailability::Skipped { reason: _ } => {
// No verification source on this platform; proceed normally.
}
}
switch try await passkey.checkAvailability() {
case .available:
// Show passkey as primary option.
break
case .prfUnsupported:
// Fall back to mnemonic flow.
break
case .notAssociated(let source, let reason):
print("Domain association failed (source=\(source)): \(reason)")
case .skipped:
// No verification source on this platform; proceed normally.
break
}
when (val availability = passkey.checkAvailability()) {
is PasskeyAvailability.Available -> Unit
is PasskeyAvailability.PrfUnsupported -> Unit
is PasskeyAvailability.NotAssociated -> {
// Log.e("Breez", "Domain association failed
// (source=${availability.source}): ${availability.reason}")
}
is PasskeyAvailability.Skipped -> Unit
}
switch (await passkey.CheckAvailability())
{
case PasskeyAvailability.Available:
break;
case PasskeyAvailability.PrfUnsupported:
break;
case PasskeyAvailability.NotAssociated notAssociated:
Console.WriteLine($"Domain association failed (source={notAssociated.source}): " +
$"{notAssociated.reason}");
break;
case PasskeyAvailability.Skipped:
break;
}
const availability = await passkey.checkAvailability()
switch (availability.type) {
case 'available':
// Show passkey as primary option.
break
case 'prfUnsupported':
// Fall back to mnemonic flow.
break
case 'notAssociated':
console.error(
`Domain association failed (source=${availability.source}): ${availability.reason}`
)
break
case 'skipped':
// No verification source on this platform; proceed normally.
break
}
const availability = await passkey.checkAvailability()
switch (availability.tag) {
case PasskeyAvailability_Tags.Available:
// Show passkey as primary option.
break
case PasskeyAvailability_Tags.PrfUnsupported:
// Fall back to mnemonic flow.
break
case PasskeyAvailability_Tags.NotAssociated:
console.error(
`Domain association failed (source=${availability.inner.source}): ` +
`${availability.inner.reason}`
)
break
case PasskeyAvailability_Tags.Skipped:
// No verification source on this platform; proceed normally.
break
}
final availability = await passkey.checkAvailability();
if (availability is PasskeyAvailability_Available) {
// Show passkey as primary option.
} else if (availability is PasskeyAvailability_PrfUnsupported) {
// Fall back to mnemonic flow.
} else if (availability is PasskeyAvailability_NotAssociated) {
print("Domain association failed (source=${availability.source}): ${availability.reason}");
} else if (availability is PasskeyAvailability_Skipped) {
// No verification source on this platform; proceed normally.
}
availability = await passkey.check_availability()
if isinstance(availability, PasskeyAvailability.AVAILABLE):
# Show passkey as primary option.
pass
elif isinstance(availability, PasskeyAvailability.PRF_UNSUPPORTED):
# Fall back to mnemonic flow.
pass
elif isinstance(availability, PasskeyAvailability.NOT_ASSOCIATED):
print(f"Domain association failed (source={availability.source}): {availability.reason}")
elif isinstance(availability, PasskeyAvailability.SKIPPED):
# No verification source on this platform; proceed normally.
pass
availability, err := passkey.CheckAvailability()
if err != nil {
return
}
switch r := availability.(type) {
case breez_sdk_spark.PasskeyAvailabilityAvailable:
// Show passkey as primary option.
_ = r
case breez_sdk_spark.PasskeyAvailabilityPrfUnsupported:
// Fall back to mnemonic flow.
_ = r
case breez_sdk_spark.PasskeyAvailabilityNotAssociated:
log.Printf("Domain association failed (source=%s): %s", r.Source, r.Reason)
case breez_sdk_spark.PasskeyAvailabilitySkipped:
// No verification source on this platform; proceed normally.
_ = r
}
Choosing a flow
The right flow depends on the platform:
- iOS / Android use a single-call unified flow backed by
PasskeyClient.connect_with_passkeyPasskeyClient.connect_with_passkeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.ConnectWithPasskeyPasskeyClient.ConnectWithPasskey. - Web uses two buttons ("Create a new passkey" and "Sign in with a passkey") and lets the user pick.
For explicit control over each path, call PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn and PasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.RegisterPasskeyClient.Register directly.
Unified flow (iOS / Android)
One "Use Passkey" button: a silent sign-in for returning users, with automatic fall-through to registration on a fresh device.
The response's credentialcredentialcredentialcredentialcredentialcredentialcredentialCredentialCredential field carries whichever credential signed in or was registered. See Credential metadata for using it.
// Single-CTA onboarding: silent sign-in, fall through to register.
let response = passkey
.connect_with_passkey(ConnectWithPasskeyRequest {
label: Some("personal".to_string()),
..Default::default()
})
.await?;
let config = default_config(Network::Mainnet);
let sdk = connect(ConnectRequest {
config,
seed: response.wallet.seed,
storage_dir: "./.data".to_string(),
})
.await?;
// Single-CTA onboarding: silent sign-in, fall through to register.
var config = defaultConfig(network: .mainnet)
config.apiKey = "<breez api key>"
let response = try await passkey.connectWithPasskey(
request: ConnectWithPasskeyRequest(label: "personal")
)
let sdk = try await connect(
request: ConnectRequest(
config: config,
seed: response.wallet.seed,
storageDir: "./.data"
))
// Single-CTA onboarding: silent sign-in, fall through to register.
val config = defaultConfig(Network.MAINNET).apply { apiKey = "<breez api key>" }
val response = passkey.connectWithPasskey(
ConnectWithPasskeyRequest(label = "personal")
)
val sdk = connect(ConnectRequest(config, response.wallet.seed, "./.data"))
// Single-CTA onboarding: silent sign-in for a returning user,
// fall-through to register on a fresh device.
var response = await passkey.ConnectWithPasskey(
new ConnectWithPasskeyRequest(label: "personal")
);
var config = BreezSdkSparkMethods.DefaultConfig(network: Network.Mainnet);
var sdk = await BreezSdkSparkMethods.Connect(new ConnectRequest(
config: config,
seed: response.wallet.seed,
storageDir: "./.data"
));
// Not available on web; use two buttons (signIn / register) instead.
const response = await passkey.signIn({ label: 'personal' })
const config = defaultConfig('mainnet')
const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' })
// Silent sign-in, fall through to register.
const config = { ...defaultConfig(Network.Mainnet), apiKey: '<breez api key>' }
const response = await passkey.connectWithPasskey({
label: 'personal',
allowCredentials: undefined,
excludeCredentials: undefined
})
const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' })
// Single-CTA onboarding: silent sign-in, fall through to register.
final config = defaultConfig(network: Network.mainnet)
.copyWith(apiKey: '<breez api key>');
final response = await passkey.connectWithPasskey(
request: ConnectWithPasskeyRequest(label: 'personal'),
);
final sdk = await connect(
request: ConnectRequest(
config: config, seed: response.wallet.seed, storageDir: "./.data"));
# Silent sign-in for a returning user, fall-through to register on a fresh device.
response = await passkey.connect_with_passkey(
ConnectWithPasskeyRequest(label="personal")
)
config = default_config(network=Network.MAINNET)
sdk = await connect(
ConnectRequest(config=config, seed=response.wallet.seed, storage_dir="./.data")
)
// Silent sign-in for a returning user, fall-through to register on a fresh device.
label := "personal"
response, err := passkey.ConnectWithPasskey(breez_sdk_spark.ConnectWithPasskeyRequest{
Label: &label,
})
if err != nil {
return nil, err
}
config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet)
sdk, err := breez_sdk_spark.Connect(breez_sdk_spark.ConnectRequest{
Config: config,
Seed: response.Wallet.Seed,
StorageDir: "./.data",
})
if err != nil {
return nil, err
}
Two-button flow (Web)
On web, present two buttons: Create a new passkey (calls PasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.RegisterPasskeyClient.Register) and Sign in with a passkey (calls PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn).
PasskeyClient.connect_with_passkeyPasskeyClient.connect_with_passkeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.ConnectWithPasskeyPasskeyClient.ConnectWithPasskey is not available on the WASM target. WebAuthn reports "no credential" and "user cancelled" identically, so the SDK can't auto-detect the flow. Let the user choose.
Sign in and register
Call PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn and PasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.RegisterPasskeyClient.Register directly for explicit control: the two web buttons, separate create-a-passkey and sign-in screens, or adding a new label for a returning user. Pass wallet.seed to connectconnectconnectconnectconnectconnectconnectConnectConnect in either case.
Sign in
Sign in to an existing credential:
// Returning-user-only sign-in. No fall-through to register.
Ok(passkey
.sign_in(SignInRequest {
label: Some("personal".to_string()),
..Default::default()
})
.await?)
// Returning-user sign-in. No fall-through to register.
return try await passkey.signIn(request: SignInRequest(label: "personal"))
// Returning-user sign-in. No fall-through to register.
return passkey.signIn(SignInRequest(label = "personal"))
// Returning-user sign-in. No fall-through to register.
return await passkey.signIn({ label: 'personal' })
// Returning-user sign-in. No fall-through to register.
return await passkey.signIn({
label: 'personal',
allowCredentials: undefined,
preferImmediatelyAvailableCredentials: undefined
})
// Returning-user sign-in. No fall-through to register.
return await passkey.signIn(request: SignInRequest(label: 'personal'));
Register
Register a fresh credential:
let response = passkey
.register(RegisterRequest {
label: Some("personal".to_string()),
..Default::default()
})
.await?;
let config = default_config(Network::Mainnet);
let sdk = connect(ConnectRequest {
config,
seed: response.wallet.seed,
storage_dir: "./.data".to_string(),
})
.await?;
var config = defaultConfig(network: .mainnet)
config.apiKey = "<breez api key>"
let response = try await passkey.register(
request: RegisterRequest(label: "personal")
)
let sdk = try await connect(
request: ConnectRequest(
config: config,
seed: response.wallet.seed,
storageDir: "./.data"
))
val config = defaultConfig(Network.MAINNET).apply { apiKey = "<breez api key>" }
val response = passkey.register(RegisterRequest(label = "personal"))
val sdk = connect(ConnectRequest(config, response.wallet.seed, "./.data"))
var response = await passkey.Register(new RegisterRequest(label: "personal"));
var config = BreezSdkSparkMethods.DefaultConfig(network: Network.Mainnet);
var sdk = await BreezSdkSparkMethods.Connect(new ConnectRequest(
config: config,
seed: response.wallet.seed,
storageDir: "./.data"
));
const response = await passkey.register({ label: 'personal' })
const config = defaultConfig('mainnet')
const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' })
const config = { ...defaultConfig(Network.Mainnet), apiKey: '<breez api key>' }
const response = await passkey.register({ label: 'personal', excludeCredentials: undefined })
const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' })
final config = defaultConfig(network: Network.mainnet)
.copyWith(apiKey: '<breez api key>');
final response = await passkey.register(
request: RegisterRequest(label: 'personal'),
);
final sdk = await connect(
request: ConnectRequest(
config: config, seed: response.wallet.seed, storageDir: "./.data"));
response = await passkey.register(RegisterRequest(label="personal"))
config = default_config(network=Network.MAINNET)
sdk = await connect(
ConnectRequest(config=config, seed=response.wallet.seed, storage_dir="./.data")
)
label := "personal"
response, err := passkey.Register(breez_sdk_spark.RegisterRequest{Label: &label})
if err != nil {
return nil, err
}
config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet)
sdk, err := breez_sdk_spark.Connect(breez_sdk_spark.ConnectRequest{
Config: config,
Seed: response.Wallet.Seed,
StorageDir: "./.data",
})
if err != nil {
return nil, err
}
Error recovery
Every passkey failure normalizes to a PrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderError variant. Match on the variant to drive recovery:
| Variant | What it means | Recommended action |
|---|---|---|
PrfProviderError::UserCancelledPrfProviderError.USER_CANCELLEDPrfProviderError.userCancelledPrfProviderError.UserCancelledPrfProviderError.UserCancelledPrfProviderError.UserCancelledPrfProviderError.UserCancelledPrfProviderErrorUserCancelledPrfProviderError.UserCancelled | User dismissed the OS prompt | Sticky retry UI with "Try Again". |
PrfProviderError::CredentialNotFoundPrfProviderError.CREDENTIAL_NOT_FOUNDPrfProviderError.credentialNotFoundPrfProviderError.CredentialNotFoundPrfProviderError.CredentialNotFoundPrfProviderError.CredentialNotFoundPrfProviderError.CredentialNotFoundPrfProviderErrorCredentialNotFoundPrfProviderError.CredentialNotFound | No matching credential on this device | Fall through to PasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.RegisterPasskeyClient.Register. |
PrfProviderError::CredentialAlreadyExistsPrfProviderError.CREDENTIAL_ALREADY_EXISTSPrfProviderError.credentialAlreadyExistsPrfProviderError.CredentialAlreadyExistsPrfProviderError.CredentialAlreadyExistsPrfProviderError.CredentialAlreadyExistsPrfProviderError.CredentialAlreadyExistsPrfProviderErrorCredentialAlreadyExistsPrfProviderError.CredentialAlreadyExists | Register hit a credential in exclude_credentialsexclude_credentialsexcludeCredentialsexcludeCredentialsexcludeCredentialsexcludeCredentialsexcludeCredentialsExcludeCredentialsExcludeCredentials | Flip to PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn; the OS picker surfaces the existing credential. |
PrfProviderError::UserTimedOutPrfProviderError.USER_TIMED_OUTPrfProviderError.userTimedOutPrfProviderError.UserTimedOutPrfProviderError.UserTimedOutPrfProviderError.UserTimedOutPrfProviderError.UserTimedOutPrfProviderErrorUserTimedOutPrfProviderError.UserTimedOut | OS biometric inactivity timeout, distinct from a cancel | Sticky retry with timeout-specific copy. Do not auto-retry. |
PrfProviderError::PrfNotSupportedPrfProviderError.PRF_NOT_SUPPORTEDPrfProviderError.prfNotSupportedPrfProviderError.PrfNotSupportedPrfProviderError.PrfNotSupportedPrfProviderError.PrfNotSupportedPrfProviderError.PrfNotSupportedPrfProviderErrorPrfNotSupportedPrfProviderError.PrfNotSupported | Authenticator lacks the PRF extension | Fall back to mnemonic onboarding. |
PrfProviderError::ConfigurationPrfProviderError.CONFIGURATIONPrfProviderError.configurationPrfProviderError.ConfigurationPrfProviderError.ConfigurationPrfProviderError.ConfigurationPrfProviderError.ConfigurationPrfProviderErrorConfigurationPrfProviderError.Configuration | Entitlement missing, AASA stale, or assetlinks malformed | Developer-facing error; surface the PasskeyAvailability::NotAssociatedPasskeyAvailability.NOT_ASSOCIATEDPasskeyAvailability.notAssociatedPasskeyAvailability.NotAssociatedPasskeyAvailability.NotAssociatedPasskeyAvailability.NotAssociatedPasskeyAvailability.NotAssociatedPasskeyAvailabilityNotAssociatedPasskeyAvailability.NotAssociated reason. |
PrfProviderError::GenericPrfProviderError.GENERICPrfProviderError.genericPrfProviderError.GenericPrfProviderError.GenericPrfProviderError.GenericPrfProviderError.GenericPrfProviderErrorGenericPrfProviderError.Generic | Network or generic failure | Generic "try again later" UI. |
Web exposes typed exception classes (PasskeyAlreadyExistsError, PasskeyTimedOutError, PasskeyCredentialNotFoundError) for instanceof matching. Rust callers can branch on the collapsed error.kind() instead of every variant.
Two recovery paths are common enough to show in full.
Flip to sign-in when register hits an existing credential:
match passkey
.register(RegisterRequest {
label: Some("personal".to_string()),
exclude_credentials: Some(vec![
// app-persisted credential IDs from prior registrations
]),
})
.await
{
Ok(response) => Ok(response.wallet),
Err(e) if e.kind() == ErrorKind::AlreadyExists => {
// A matching credential already exists; sign in to it instead.
let response = passkey
.sign_in(SignInRequest {
label: Some("personal".to_string()),
..Default::default()
})
.await?;
Ok(response.wallet)
}
Err(e) => Err(e.into()),
}
do {
let response = try await passkey.register(
request: RegisterRequest(
label: "personal",
excludeCredentials: [
// app-persisted credential IDs from prior registrations
]
)
)
return response.wallet
} catch PrfProviderError.CredentialAlreadyExists {
// A matching credential already exists; sign in instead.
let response = try await passkey.signIn(
request: SignInRequest(label: "personal")
)
return response.wallet
}
return try {
val response = passkey.register(
RegisterRequest(
label = "personal",
// app-persisted credential IDs from prior registrations
excludeCredentials = emptyList(),
)
)
response.wallet
} catch (e: PrfProviderException.CredentialAlreadyExists) {
// A matching credential already exists; sign in to it instead.
val response = passkey.signIn(SignInRequest(label = "personal"))
response.wallet
}
try
{
var response = await passkey.Register(new RegisterRequest(
label: "personal",
excludeCredentials: new byte[][]
{
// app-persisted credential IDs from prior registrations
}
));
return response.wallet;
}
catch (PrfProviderException.CredentialAlreadyExists)
{
var response = await passkey.SignIn(new SignInRequest(label: "personal"));
return response.wallet;
}
try {
const response = await passkey.register({
label: 'personal',
excludeCredentials: [
// app-persisted credential IDs from prior registrations
]
})
return response.wallet
} catch (error) {
if (error instanceof PasskeyAlreadyExistsError) {
// A matching credential already exists; sign in to it instead.
const response = await passkey.signIn({ label: 'personal' })
return response.wallet
}
throw error
}
try {
const response = await passkey.register({
label: 'personal',
excludeCredentials: [
// app-persisted credential IDs from prior registrations
]
})
return response.wallet
} catch (error) {
if (error instanceof PasskeyPrfException && error.code === 'credentialAlreadyExists') {
// A matching credential already exists; sign in to it instead.
const response = await passkey.signIn({
label: 'personal',
allowCredentials: undefined,
preferImmediatelyAvailableCredentials: undefined
})
return response.wallet
}
throw error
}
try {
final response = await passkey.register(
request: RegisterRequest(
label: 'personal',
excludeCredentials: [
// app-persisted credential IDs from prior registrations
],
),
);
return response.wallet;
} on PasskeyPrfException catch (e) {
if (e.code != 'credentialAlreadyExists') rethrow;
// A matching credential already exists; sign in instead.
final response = await passkey.signIn(
request: SignInRequest(label: 'personal'),
);
return response.wallet;
}
try:
await passkey.register(
RegisterRequest(
label="personal",
exclude_credentials=[
# app-persisted credential IDs from prior registrations
],
)
)
except PrfProviderError.CredentialAlreadyExists:
# A matching credential already exists; sign in to it instead.
response = await passkey.sign_in(SignInRequest(label="personal"))
return response.wallet
label := "personal"
registerResponse, err := passkey.Register(breez_sdk_spark.RegisterRequest{
Label: &label,
ExcludeCredentials: &[][]byte{
// app-persisted credential IDs from prior registrations
},
})
if err == nil {
return ®isterResponse.Wallet, nil
}
if !errors.Is(err, breez_sdk_spark.ErrPrfProviderErrorCredentialAlreadyExists) {
return nil, err
}
// A matching credential already exists; sign in to it instead.
signInResponse, err := passkey.SignIn(breez_sdk_spark.SignInRequest{Label: &label})
if err != nil {
return nil, err
}
return &signInResponse.Wallet, nil
Show a sticky retry when the biometric timeout fires:
// Biometric inactivity timeout, distinct from a user cancel.
match passkey
.sign_in(SignInRequest {
label: Some("personal".to_string()),
..Default::default()
})
.await
{
Ok(response) => Ok(response),
Err(e) if e.kind() == ErrorKind::Timeout => {
// Show a retry UI. Do NOT auto-retry without user input.
println!("Sign-in timed out: show \"Try Again\" UI.");
Err(e.into())
}
Err(e) => Err(e.into()),
}
do {
return try await passkey.signIn(
request: SignInRequest(label: "personal")
)
} catch PrfProviderError.UserTimedOut {
// Show a retry UI. Do NOT auto-retry without user input.
print("Sign-in timed out: show \"Try Again\" UI.")
throw PrfProviderError.UserTimedOut
}
return try {
passkey.signIn(SignInRequest(label = "personal"))
} catch (e: PrfProviderException.UserTimedOut) {
// Show a retry UI. Do NOT auto-retry without user input.
// Log.v("Breez", "Sign-in timed out: show \"Try Again\" UI.")
throw e
}
try
{
return await passkey.SignIn(new SignInRequest(label: "personal"));
}
catch (PrfProviderException.UserTimedOut)
{
Console.WriteLine("Sign-in timed out: show \"Try Again\" UI.");
throw;
}
// Biometric inactivity timeout, distinct from a user cancel.
try {
const response = await passkey.signIn({ label: 'personal' })
return response
} catch (error) {
if (error instanceof PasskeyTimedOutError) {
// Show a retry UI. Do NOT auto-retry without user input.
console.log('Sign-in timed out: show "Try Again" UI.')
}
throw error
}
// Biometric inactivity timeout, distinct from a user cancel.
try {
const response = await passkey.signIn({
label: 'personal',
allowCredentials: undefined,
preferImmediatelyAvailableCredentials: undefined
})
return response
} catch (error) {
if (error instanceof PasskeyPrfException && error.code === 'userTimedOut') {
// Show a retry UI. Do NOT auto-retry without user input.
console.log('Sign-in timed out: show "Try Again" UI.')
}
throw error
}
// Timeout is distinct from a cancel: surface a re-prompt UI.
try {
return await passkey.signIn(
request: SignInRequest(label: 'personal'),
);
} on PasskeyPrfException catch (e) {
if (e.code == 'userTimedOut') {
// Show a retry UI. Do NOT auto-retry without user input.
print("Sign-in timed out: show \"Try Again\" UI.");
}
rethrow;
}
# Biometric inactivity timeout, distinct from a user cancel.
try:
return await passkey.sign_in(SignInRequest(label="personal"))
except PrfProviderError.UserTimedOut:
# Show a retry UI. Do NOT auto-retry without user input.
print("Sign-in timed out: show \"Try Again\" UI.")
raise
// Biometric inactivity timeout, distinct from a user cancel.
label := "personal"
response, err := passkey.SignIn(breez_sdk_spark.SignInRequest{Label: &label})
if err != nil {
if errors.Is(err, breez_sdk_spark.ErrPrfProviderErrorUserTimedOut) {
// Show a retry UI. Do NOT auto-retry without user input.
log.Print("Sign-in timed out: show \"Try Again\" UI.")
}
return nil, err
}
return &response, nil
See the UX guide for the recommended recovery UX.
Supported specs
- Seedless Restore: passkey-based wallet derivation and discovery
- Nostr: relay-based event protocol for label storage
- NIP-42: authentication of clients to relays
- NIP-65: relay list metadata