startCreate method
Start the CREATE flow from OnboardingWelcome. Generates + seals a fresh seed in Rust and moves to OnboardingAwaitingBackup (NOT active — the recovery phrase must be backed up and confirmed first). A failure surfaces as OnboardingFailed (the screen renders it with the right next step); no rethrow, the state IS the surface.
Implementation
Future<void> startCreate() async {
if (state is! OnboardingWelcome) return; // only from welcome
// Reaching OnboardingWelcome implies a non-null provisioner+store AND a
// just-observed `walletExists() == false` (the probe sets Welcome only
// then). Both are structural — but a release build strips `assert`, so a
// bare `return` here would SILENTLY no-op if a future refactor broke the
// invariant (invariant 10). Surface it honestly instead, exactly as
// `retry()` does for the same missing-backend case.
final provisioner = _provisioner;
final store = _store;
if (provisioner == null || store == null) {
_set(const OnboardingUnavailable());
return;
}
// RE-ENTRANCY INTERLOCK: this synchronous transition (BEFORE the first
// await) is what makes a double-tap safe — a second startCreate sees
// `state != Welcome` and no-ops, so a wallet is created AT MOST once (pinned
// by `double-tap startCreate creates exactly ONE wallet`). It must stay
// synchronous-before-the-first-await; a future edit that inserts an await
// ahead of it — or the identity-keyed-flag restore/re-create path — needs an
// explicit in-flight latch instead (manager-flagged). The operation
// generation below is that latch, for every mutation.
final generation = _beginMutation();
if (generation == null) return;
_set(const OnboardingGenerating());
try {
// CRASH-SAFE ORDERING: durably CLOSE the backup gate for the
// about-to-exist wallet BEFORE it is written to disk. If a crash lands
// anywhere after this, the next boot reads `confirmed == false` and
// re-forces the backup; a stale `true` from a wiped prior wallet can't
// leak the new one past the gate.
//
// SAFE ONLY because startCreate runs from OnboardingWelcome (no wallet on
// disk yet): the false-write touches the gate for a wallet that does not
// exist. A FUTURE re-create/restore entry point must NOT reset the flag
// before confirming it is not clobbering an already-confirmed wallet
// (else it could re-force backup of a funded wallet) — manager-flagged;
// key the flag by wallet identity when that path lands.
await store.setBackupConfirmed(confirmed: false);
if (_stale(generation)) return;
// The gate is closed above; the wallet is written ONLY if that persist
// succeeded (sequential awaits). The single catch below covers BOTH the
// gate-reset failure and the create-write failure, in that order — either
// way nothing reaches OnboardingAwaitingBackup, so no ungated wallet is
// surfaced.
final session = await provisioner.createGenerated();
if (_stale(generation)) return;
_set(OnboardingAwaitingBackup(session));
} catch (error) {
if (_stale(generation)) return;
_set(OnboardingFailed(classifyOnboardingFailure(error)));
} finally {
_endMutation(generation);
}
}