fix: onboarding restore no longer traps on an abandoned identity or a missing pin
This commit is contained in:
parent
7d3c8b423c
commit
eaaa3f2ede
3 changed files with 164 additions and 91 deletions
|
|
@ -31,6 +31,10 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||
_Step step = _Step.welcome;
|
||||
Uint8List? createdSeed;
|
||||
bool busy = false;
|
||||
// The register step doubles as "pin a key to finish recalling" when a
|
||||
// restore's recall can't proceed without one yet — same fields, different
|
||||
// framing and default action, not the generic "register a new address" copy.
|
||||
bool recallIntent = false;
|
||||
|
||||
final seedController = TextEditingController();
|
||||
final restoreAddressController = TextEditingController();
|
||||
|
|
@ -57,13 +61,33 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||
context, error is SmolError ? error.message : error.toString());
|
||||
}
|
||||
|
||||
void _createIdentity() {
|
||||
// Reaching onboarding at all means HomeGuard already found no complete
|
||||
// identity+account, so a seed still sitting in the store here can only be
|
||||
// an abandoned attempt from earlier in this same flow (wrong seed, failed
|
||||
// recall, "Back") — safe to replace rather than reject.
|
||||
// wipe() is fire-and-forget here, like every other store write in this
|
||||
// screen (setIdentity, pinServer, ...) — Hive updates its in-memory state
|
||||
// synchronously and persists to disk in the background, so the identity
|
||||
// check right after is already consistent without awaiting the write.
|
||||
void _clearAbandonedIdentity() {
|
||||
final client = ref.read(clientProvider);
|
||||
final fresh = client.createIdentity();
|
||||
setState(() {
|
||||
createdSeed = fresh.seed;
|
||||
step = _Step.backup;
|
||||
});
|
||||
if (client.identity != null && client.accountAddress() == null) {
|
||||
ref.read(storeProvider).wipe();
|
||||
}
|
||||
}
|
||||
|
||||
void _createIdentity() {
|
||||
_clearAbandonedIdentity();
|
||||
final client = ref.read(clientProvider);
|
||||
try {
|
||||
final fresh = client.createIdentity();
|
||||
setState(() {
|
||||
createdSeed = fresh.seed;
|
||||
step = _Step.backup;
|
||||
});
|
||||
} on Exception catch (err) {
|
||||
_showError(err);
|
||||
}
|
||||
}
|
||||
|
||||
void _restoreIdentity() {
|
||||
|
|
@ -74,6 +98,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||
// pinned yet, offline, typo) — recall can always be retried from the
|
||||
// register step or settings afterward.
|
||||
void _submitRestore() {
|
||||
_clearAbandonedIdentity();
|
||||
final client = ref.read(clientProvider);
|
||||
try {
|
||||
client.restoreIdentity(seedController.text);
|
||||
|
|
@ -86,6 +111,34 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||
setState(() => step = _Step.register);
|
||||
return;
|
||||
}
|
||||
// The register step has its own address field (it also needs a server
|
||||
// key, which restore doesn't collect) — carry over what was already
|
||||
// typed rather than making the user re-enter it.
|
||||
addressController.text = addressText;
|
||||
final SmolAddress addr;
|
||||
try {
|
||||
addr = parseAddress(addressText);
|
||||
} on Exception catch (err) {
|
||||
// A genuine typo, distinct from the merely-unpinned case below — still
|
||||
// worth an error, but land on the same recall-oriented step to fix it.
|
||||
_showError(err);
|
||||
setState(() {
|
||||
recallIntent = true;
|
||||
step = _Step.register;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Recall needs the host pinned first (SPEC.md §4). That's the expected,
|
||||
// common state right after a restore — not an error — so check for it
|
||||
// up front instead of letting recallAccount fail and surfacing that as
|
||||
// one: this address just needs a key before its first recall can proceed.
|
||||
if (ref.read(storeProvider).serverPin(addr.host) == null) {
|
||||
setState(() {
|
||||
recallIntent = true;
|
||||
step = _Step.register;
|
||||
});
|
||||
return;
|
||||
}
|
||||
() async {
|
||||
try {
|
||||
await client.recallAccount(addressText);
|
||||
|
|
@ -95,7 +148,10 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||
} on Exception catch (err) {
|
||||
if (!mounted) return;
|
||||
_showError(err);
|
||||
setState(() => step = _Step.register);
|
||||
setState(() {
|
||||
recallIntent = true;
|
||||
step = _Step.register;
|
||||
});
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
|
@ -142,7 +198,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: switch (step) {
|
||||
_Step.welcome => _welcome(context),
|
||||
|
|
@ -176,7 +232,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||
children: [
|
||||
_header(context,
|
||||
"One keypair is the whole identity: an address, a key and a message."),
|
||||
const Spacer(),
|
||||
const SizedBox(height: 40),
|
||||
PrimaryButton(
|
||||
onPressed: _createIdentity,
|
||||
child: const Text("Create Identity"),
|
||||
|
|
@ -222,7 +278,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||
},
|
||||
child: const Text("Copy seed"),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(height: 40),
|
||||
PrimaryButton(
|
||||
onPressed: () => setState(() => step = _Step.register),
|
||||
child: const Text("I have backed it up"),
|
||||
|
|
@ -266,7 +322,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||
text: "Back",
|
||||
onPressed: () => setState(() => step = _Step.welcome),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -275,8 +331,12 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_header(context,
|
||||
"Register an address. Pin the server's public key first — obtain it from the operator through a trusted channel."),
|
||||
_header(
|
||||
context,
|
||||
recallIntent
|
||||
? "Pin this server's public key to finish restoring your address."
|
||||
: "Register an address. Pin the server's public key first — obtain it from the operator through a trusted channel.",
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
TextField(
|
||||
controller: addressController,
|
||||
|
|
@ -297,25 +357,33 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||
fillColor: Theme.of(context).extension<AppColors>()!.cardFill,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
TextField(
|
||||
controller: tokenController,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Invite token (optional)",
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).extension<AppColors>()!.cardFill,
|
||||
if (!recallIntent) ...[
|
||||
const SizedBox(height: 15),
|
||||
TextField(
|
||||
controller: tokenController,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Invite token (optional)",
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).extension<AppColors>()!.cardFill,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
const SizedBox(height: 40),
|
||||
PrimaryButton(
|
||||
onPressed: busy ? () {} : _submitRegistration,
|
||||
onPressed: busy
|
||||
? () {}
|
||||
: (recallIntent ? _submitRecall : _submitRegistration),
|
||||
child: busy
|
||||
? const SmallLoadingSpinner()
|
||||
: const Text("Pin and Register"),
|
||||
: Text(recallIntent ? "Pin and Recall" : "Pin and Register"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: busy ? () {} : _submitRecall,
|
||||
child: const Text("Already registered? Recall"),
|
||||
onPressed: busy
|
||||
? () {}
|
||||
: (recallIntent ? _submitRegistration : _submitRecall),
|
||||
child: Text(recallIntent
|
||||
? "Register a new address instead"
|
||||
: "Already registered? Recall"),
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue