From bb6b9364270e39b894d6302622e903f551048b4e Mon Sep 17 00:00:00 2001 From: Restioson Date: Tue, 18 Jun 2024 17:26:31 +0200 Subject: [PATCH] fix: be more robust in beta registration when coordinator is down Previously we would just crash and never retry. Now, we don't crash, and do retry next boot of the app. --- mobile/lib/common/routes.dart | 2 +- .../lib/features/welcome/loading_screen.dart | 115 ++++++++++++------ .../features/welcome/seed_import_screen.dart | 10 +- .../lib/features/welcome/welcome_screen.dart | 18 ++- mobile/lib/util/preferences.dart | 24 ++++ 5 files changed, 124 insertions(+), 45 deletions(-) diff --git a/mobile/lib/common/routes.dart b/mobile/lib/common/routes.dart index 6868b998e..ac981e706 100644 --- a/mobile/lib/common/routes.dart +++ b/mobile/lib/common/routes.dart @@ -39,7 +39,7 @@ GoRouter createRoutes() { path: LoadingScreen.route, pageBuilder: (context, state) => NoTransitionPage( child: LoadingScreen( - future: state.extra as Future?, + task: state.extra as LoadingScreenTask?, ), ), ), diff --git a/mobile/lib/features/welcome/loading_screen.dart b/mobile/lib/features/welcome/loading_screen.dart index ad2ebe53c..02a33032d 100644 --- a/mobile/lib/features/welcome/loading_screen.dart +++ b/mobile/lib/features/welcome/loading_screen.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:get_10101/common/global_keys.dart'; import 'package:get_10101/common/scrollable_safe_area.dart'; import 'package:get_10101/common/snack_bar.dart'; -import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:get_10101/backend.dart'; import 'package:get_10101/features/welcome/error_screen.dart'; import 'package:get_10101/features/welcome/onboarding.dart'; import 'package:get_10101/features/trade/trade_screen.dart'; import 'package:get_10101/features/wallet/wallet_screen.dart'; +import 'package:get_10101/features/welcome/welcome_screen.dart'; import 'package:get_10101/logger/logger.dart'; import 'package:get_10101/util/preferences.dart'; import 'package:get_10101/util/file.dart'; @@ -16,9 +18,9 @@ import 'package:go_router/go_router.dart'; class LoadingScreen extends StatefulWidget { static const route = "/loading"; - final Future? future; + final LoadingScreenTask? task; - const LoadingScreen({super.key, this.future}); + const LoadingScreen({super.key, this.task}); @override State createState() => _LoadingScreenState(); @@ -29,42 +31,66 @@ class _LoadingScreenState extends State { @override void initState() { - // Wait for the future to complete sequentially before running other futures concurrently - (widget.future ?? Future.value()).then((value) { - return Future.wait([ - Preferences.instance.getOpenPosition(), - isSeedFilePresent(), - Preferences.instance.isFullBackupRequired(), - ]); - }).then((value) { - final position = value[0]; - final isSeedFilePresent = value[1]; - final isFullBackupRequired = value[2]; - FlutterNativeSplash.remove(); - - if (isSeedFilePresent) { - if (isFullBackupRequired) { - setState(() => message = "Creating initial backup!"); - fullBackup().then((value) { - Preferences.instance.setFullBackupRequired(false).then((value) { - start(context, position); - }); - }).catchError((error) { - logger.e("Failed to run full backup. $error"); - showSnackBar(ScaffoldMessenger.of(context), "Failed to start 10101!"); - }); - } else { - start(context, position); + initAsync(); + super.initState(); + } + + Future initAsync() async { + var skipBetaRegistration = false; + try { + await widget.task?.future; + } catch (err, stackTrace) { + final task = widget.task!; + final taskErr = task.error(err); + skipBetaRegistration = task.skipBetaRegistrationOnFail; + logger.e(taskErr, error: err, stackTrace: stackTrace); + showSnackBar(ScaffoldMessenger.of(rootNavigatorKey.currentContext!), taskErr); + } + + final [position, seedPresent, backupRequired, registeredForBeta] = await Future.wait([ + Preferences.instance.getOpenPosition(), + isSeedFilePresent(), + Preferences.instance.isFullBackupRequired(), + Preferences.instance.isRegisteredForBeta(), + ]); + FlutterNativeSplash.remove(); + + if (seedPresent) { + if (!registeredForBeta && !skipBetaRegistration) { + logger.w("Registering for beta program despite having a seed; " + "onboarding flow was probably previously interrupted"); + setState(() => message = "Registering for beta program"); + + try { + await resumeRegisterForBeta(); + } catch (err, stackTrace) { + const failed = "Failed to register for beta program"; + showSnackBar(ScaffoldMessenger.of(rootNavigatorKey.currentContext!), "$failed."); + logger.e(failed, error: err, stackTrace: stackTrace); } - } else { - // No seed file: let the user choose whether they want to create a new - // wallet or import their old one - Preferences.instance.setFullBackupRequired(false).then((value) { - GoRouter.of(context).go(Onboarding.route); + } + + if (backupRequired) { + setState(() => message = "Creating initial backup!"); + fullBackup().then((value) { + Preferences.instance.setFullBackupRequired(false).then((value) { + start(rootNavigatorKey.currentContext!, position); + }); + }).catchError((error) { + logger.e("Failed to run full backup. $error"); + showSnackBar( + ScaffoldMessenger.of(rootNavigatorKey.currentContext!), "Failed to start 10101!"); }); + } else { + start(rootNavigatorKey.currentContext!, position); } - }); - super.initState(); + } else { + // No seed file: let the user choose whether they want to create a new + // wallet or import their old one + Preferences.instance.setFullBackupRequired(false).then((value) { + GoRouter.of(context).go(Onboarding.route); + }); + } } void start(BuildContext context, String? position) { @@ -106,3 +132,20 @@ class _LoadingScreenState extends State { )))); } } + +/// Some operation carried out whilst the loading screen is displayed +class LoadingScreenTask { + /// The future of the task itself + final Future future; + + /// Create the snackbar text error to display if the task fails + final String Function(dynamic) error; + + /// Whether to skip the beta registration if this task fails. This should + /// be `true` if the task's `future` did itself try to register the user for + /// beta. + final bool skipBetaRegistrationOnFail; + + LoadingScreenTask( + {required this.future, required this.error, this.skipBetaRegistrationOnFail = false}); +} diff --git a/mobile/lib/features/welcome/seed_import_screen.dart b/mobile/lib/features/welcome/seed_import_screen.dart index 670849b09..44c877bf8 100644 --- a/mobile/lib/features/welcome/seed_import_screen.dart +++ b/mobile/lib/features/welcome/seed_import_screen.dart @@ -141,12 +141,10 @@ class SeedPhraseImporterState extends State { getSeedFilePath().then((seedPath) { logger.i("Restoring seed into $seedPath"); - final restore = api - .restoreFromSeedPhrase( - seedPhrase: seedPhrase, targetSeedFilePath: seedPath) - .catchError((error) => showSnackBar( - ScaffoldMessenger.of(context), - "Failed to import from seed. $error")); + final restore = LoadingScreenTask( + future: api.restoreFromSeedPhrase( + seedPhrase: seedPhrase, targetSeedFilePath: seedPath), + error: (err) => "Failed to import from seed. $err"); // TODO(holzeis): Backup preferences and restore email from there. Preferences.instance.setContactDetails("restored"); GoRouter.of(context).go(LoadingScreen.route, extra: restore); diff --git a/mobile/lib/features/welcome/welcome_screen.dart b/mobile/lib/features/welcome/welcome_screen.dart index 1b1d01fdd..f63db7036 100644 --- a/mobile/lib/features/welcome/welcome_screen.dart +++ b/mobile/lib/features/welcome/welcome_screen.dart @@ -206,8 +206,11 @@ class _WelcomeScreenState extends State { _formKey.currentState!.save(); if (_formKey.currentState != null && _formKey.currentState!.validate()) { - GoRouter.of(context) - .go(LoadingScreen.route, extra: setupWallet()); + final task = LoadingScreenTask( + future: setupWallet(), + error: (_) => "Failed to register for beta program", + skipBetaRegistrationOnFail: true); + GoRouter.of(context).go(LoadingScreen.route, extra: task); } }, style: ButtonStyle( @@ -255,7 +258,10 @@ class _WelcomeScreenState extends State { logger.i("Successfully stored the contact: $_contact ."); await api.initNewMnemonic(targetSeedFilePath: seedPath); logger.d("Registering user with $_contact & $_referralCode"); + + await Preferences.instance.setReferralCode(_referralCode); await api.registerBeta(contact: _contact, referralCode: _referralCode); + await Preferences.instance.setRegisteredForBeta(); } @override @@ -273,3 +279,11 @@ class _WelcomeScreenState extends State { })); } } + +/// Resume a previously incomplete onboarding flow by registering for beta +Future resumeRegisterForBeta() async { + await api.registerBeta( + contact: await Preferences.instance.getContactDetails(), + referralCode: await Preferences.instance.getReferralCode()); + await Preferences.instance.setRegisteredForBeta(); +} diff --git a/mobile/lib/util/preferences.dart b/mobile/lib/util/preferences.dart index 4d15c02d2..4a268b90c 100644 --- a/mobile/lib/util/preferences.dart +++ b/mobile/lib/util/preferences.dart @@ -13,6 +13,10 @@ class Preferences { static const fullBackup = "fullBackup"; static const logLevelTrace = "logLevelTrace"; static const _hasSeenReferralDialogTimePassed = "hasSeenReferralDialogTimePassed"; + static const registeredForBeta = "registeredForBeta"; + + /// The referral code that the user signed up with + static const referralCode = "referralCode"; Future setLogLevelTrace(bool trace) async { SharedPreferences preferences = await SharedPreferences.getInstance(); @@ -54,6 +58,26 @@ class Preferences { preferences.remove(openPosition); } + Future setRegisteredForBeta() async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + return preferences.setBool(registeredForBeta, true); + } + + Future isRegisteredForBeta() async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + return preferences.getBool(registeredForBeta) ?? false; + } + + Future setReferralCode(String code) async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + return preferences.setString(referralCode, code); + } + + Future getReferralCode() async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + return preferences.getString(referralCode) ?? ""; + } + Future setContactDetails(String value) async { SharedPreferences preferences = await SharedPreferences.getInstance(); return preferences.setString(contactDetails, value);