Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Make onboarding a bit more robust when coordinator is down #2603

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading