startCreate method

Future<void> startCreate()

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);
  }
}