Skip to content

Commit

Permalink
fix: be more robust in beta registration when coordinator is down
Browse files Browse the repository at this point in the history
Previously we would just crash and never retry. Now, we don't crash, and do retry next boot of the app.
  • Loading branch information
Restioson committed Jun 18, 2024
1 parent 921e9ed commit bb6b936
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 45 deletions.
2 changes: 1 addition & 1 deletion mobile/lib/common/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ GoRouter createRoutes() {
path: LoadingScreen.route,
pageBuilder: (context, state) => NoTransitionPage<void>(
child: LoadingScreen(
future: state.extra as Future<void>?,
task: state.extra as LoadingScreenTask?,
),
),
),
Expand Down
115 changes: 79 additions & 36 deletions mobile/lib/features/welcome/loading_screen.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,9 +18,9 @@ import 'package:go_router/go_router.dart';
class LoadingScreen extends StatefulWidget {
static const route = "/loading";

final Future<void>? future;
final LoadingScreenTask? task;

const LoadingScreen({super.key, this.future});
const LoadingScreen({super.key, this.task});

@override
State<LoadingScreen> createState() => _LoadingScreenState();
Expand All @@ -29,42 +31,66 @@ class _LoadingScreenState extends State<LoadingScreen> {

@override
void initState() {
// Wait for the future to complete sequentially before running other futures concurrently
(widget.future ?? Future.value()).then((value) {
return Future.wait<dynamic>([
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<void> 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<dynamic>([
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) {
Expand Down Expand Up @@ -106,3 +132,20 @@ class _LoadingScreenState extends State<LoadingScreen> {
))));
}
}

/// Some operation carried out whilst the loading screen is displayed
class LoadingScreenTask {
/// The future of the task itself
final Future<void> 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});
}
10 changes: 4 additions & 6 deletions mobile/lib/features/welcome/seed_import_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,10 @@ class SeedPhraseImporterState extends State<SeedPhraseImporter> {
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);
Expand Down
18 changes: 16 additions & 2 deletions mobile/lib/features/welcome/welcome_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,11 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
_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(
Expand Down Expand Up @@ -255,7 +258,10 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
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
Expand All @@ -273,3 +279,11 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
}));
}
}

/// Resume a previously incomplete onboarding flow by registering for beta
Future<void> resumeRegisterForBeta() async {
await api.registerBeta(
contact: await Preferences.instance.getContactDetails(),
referralCode: await Preferences.instance.getReferralCode());
await Preferences.instance.setRegisteredForBeta();
}
24 changes: 24 additions & 0 deletions mobile/lib/util/preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> setLogLevelTrace(bool trace) async {
SharedPreferences preferences = await SharedPreferences.getInstance();
Expand Down Expand Up @@ -54,6 +58,26 @@ class Preferences {
preferences.remove(openPosition);
}

Future<bool> setRegisteredForBeta() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
return preferences.setBool(registeredForBeta, true);
}

Future<bool> isRegisteredForBeta() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
return preferences.getBool(registeredForBeta) ?? false;
}

Future<bool> setReferralCode(String code) async {
SharedPreferences preferences = await SharedPreferences.getInstance();
return preferences.setString(referralCode, code);
}

Future<String> getReferralCode() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
return preferences.getString(referralCode) ?? "";
}

Future<bool> setContactDetails(String value) async {
SharedPreferences preferences = await SharedPreferences.getInstance();
return preferences.setString(contactDetails, value);
Expand Down

0 comments on commit bb6b936

Please sign in to comment.