Skip to content

Commit

Permalink
feat: add transport failed init state (#343)
Browse files Browse the repository at this point in the history
* feat: add bootstrap failed page

* feat: complete bootstrap rerun mechanism

* fix: router guard rules for bootstrap failed state

* fix: newline before return

---------

Co-authored-by: nesquikm <[email protected]>
  • Loading branch information
Alex-A4 and nesquikm authored Oct 26, 2023
1 parent 24239a7 commit 44861ef
Show file tree
Hide file tree
Showing 19 changed files with 482 additions and 91 deletions.
6 changes: 5 additions & 1 deletion assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,5 +456,9 @@
"goToWallet": "Go to wallet",
"goToOnboarding": "Go to onboarding",
"areYouSure": "Are you sure?",
"logoutConfirmText": "Make sure you saved the seed phrase, otherwise you will lose access to your seeds, keys and accounts."
"logoutConfirmText": "Make sure you saved the seed phrase, otherwise you will lose access to your seeds, keys and accounts.",
"initializationRerunFailed": "Initialization rerun failed",
"initializationFailedDescription": "App initialization failed, please, try again later",
"tryAgain": "Try again",
"connectingNetworkFailed": "Connecting network failed, please, try another network."
}
6 changes: 5 additions & 1 deletion assets/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,5 +456,9 @@
"goToWallet": "Go to wallet",
"goToOnboarding": "Go to onboarding",
"areYouSure": "Are you sure?",
"logoutConfirmText": "Make sure you saved the seed phrase, otherwise you will lose access to your seeds, keys and accounts."
"logoutConfirmText": "Make sure you saved the seed phrase, otherwise you will lose access to your seeds, keys and accounts.",
"initializationRerunFailed": "Initialization rerun failed",
"initializationFailedDescription": "App initialization failed, please, try again later",
"tryAgain": "Try again",
"connectingNetworkFailed": "Connecting network failed, please, try another network."
}
5 changes: 5 additions & 0 deletions lib/app/router/app_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import 'package:logging/logging.dart';
final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?');

enum AppRoute {
bootstrapFailedInit(
'bootstrapFailed',
'/bootstrapFailed/:$bootstrapFailedIndexPathParam',
),

onboarding(
'onboarding',
'/onboarding',
Expand Down
43 changes: 40 additions & 3 deletions lib/app/router/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,20 @@ GoRouter getRouter(BuildContext _) {

// Redirect to onboarding or wallet depending on the current location and if
// the user has any seeds.
String? shouldRedirect({required String fullPath, required bool hasSeeds}) {
String? shouldRedirect({
required String fullPath,
required bool hasSeeds,
required BootstrapSteps step,
}) {
final currentRoute = getRootAppRoute(fullPath: fullPath);

if (step != BootstrapSteps.completed &&
currentRoute != AppRoute.bootstrapFailedInit) {
return AppRoute.bootstrapFailedInit.pathWithData(
pathParameters: {bootstrapFailedIndexPathParam: step.index.toString()},
);
}

// If the user has seeds and is on onboarding, redirect to wallet
if (hasSeeds && currentRoute == AppRoute.onboarding) {
// Already onboarded, redirect to wallet
Expand Down Expand Up @@ -100,10 +111,14 @@ GoRouter getRouter(BuildContext _) {

// Check if the user has seeds
final hasSeeds = inject<NekotonRepository>().hasSeeds.value;
final step = inject<BootstrapService>().bootstrapStep;

// Check if the user should be redirected
final guardRedirect =
shouldRedirect(fullPath: fullPath, hasSeeds: hasSeeds);
final guardRedirect = shouldRedirect(
fullPath: fullPath,
hasSeeds: hasSeeds,
step: step,
);

// Redirect if needed
if (guardRedirect != null) {
Expand All @@ -125,6 +140,7 @@ GoRouter getRouter(BuildContext _) {
// Initial location from NavigationService
initialLocation: inject<NavigationService>().savedState.location,
routes: [
bootstrapFailedRoute,
GoRoute(
name: AppRoute.onboarding.name,
path: AppRoute.onboarding.path,
Expand Down Expand Up @@ -174,6 +190,27 @@ GoRouter getRouter(BuildContext _) {
final redirectLocation = shouldRedirect(
fullPath: inject<NavigationService>().state.fullPath,
hasSeeds: hasSeeds,
step: inject<BootstrapService>().bootstrapStep,
);

// Redirect if needed
if (redirectLocation != null) {
router.go(redirectLocation);
}
});

// Subscribe to bootstrapStep to redirect if needed
// This is a-la guard, it should redirect to onboarding or wallet depending
// on the current location and if the user has any seeds.
// This happends when user was sent to bootstrap failed screen to make some
// action and then bootstrap process was completed.
inject<BootstrapService>().bootstrapStepStream.listen((step) {
// Again, check if the user should be redirected depending on the current
// location and if the user has any seeds and bootstrap completed.
final redirectLocation = shouldRedirect(
fullPath: inject<NavigationService>().state.fullPath,
hasSeeds: inject<NekotonRepository>().hasSeeds.value,
step: step,
);

// Redirect if needed
Expand Down
21 changes: 21 additions & 0 deletions lib/app/router/routs/bootstrap_failed/bootstrap_failed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:app/app/router/router.dart';
import 'package:app/app/service/service.dart';
import 'package:app/feature/bootstrap_failed/bootstrap_failed.dart';
import 'package:go_router/go_router.dart';

const bootstrapFailedIndexPathParam = 'bootstrapStepIndexPathParam';

GoRoute get bootstrapFailedRoute {
return GoRoute(
name: AppRoute.bootstrapFailedInit.name,
path: AppRoute.bootstrapFailedInit.path,
builder: (_, state) => BootstrapFailedPage(
step: BootstrapSteps.values[int.parse(
state.pathParameters[bootstrapFailedIndexPathParam]!,
)],
),
routes: [
configureNetworksRoute,
],
);
}
1 change: 1 addition & 0 deletions lib/app/router/routs/routs.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export 'add_seed/add_seed.dart';
export 'bootstrap_failed/bootstrap_failed.dart';
export 'browser/browser.dart';
export 'profile/profile.dart';
export 'wallet/wallet.dart';
113 changes: 113 additions & 0 deletions lib/app/service/bootstrap_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import 'package:app/bootstrap.dart';
import 'package:app/bootstrap/bootstrap.dart';
import 'package:injectable/injectable.dart';
import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';

typedef AsyncFunc = Future<void> Function();

/// Steps that will be handled during bootstrap process.
/// If app fails during some step, then it will be easy to rerun this process.
/// [storage] - failed at storage step initialization
/// [connection] - failed during creating connection
/// [features] - failed during creating features
/// [completed] - everything is ok, app works normally
enum BootstrapSteps {
storage,
connection,
features,
completed,
}

/// Service that allows initialize app step by step and re-run some operations
/// if they failed.
@singleton
class BootstrapService {
final _log = Logger('bootstrap');

final _bootstrapStepSubject = BehaviorSubject<BootstrapSteps>();

Stream<BootstrapSteps> get bootstrapStepStream => _bootstrapStepSubject;

BootstrapSteps get bootstrapStep => _bootstrapStepSubject.value;

Future<void> init(AppBuildType appBuildType) async {
try {
await coreStep(appBuildType);

_bootstrapStepSubject.add(BootstrapSteps.storage);
await storageStep();

_bootstrapStepSubject.add(BootstrapSteps.connection);
await connectionStep();

_bootstrapStepSubject.add(BootstrapSteps.features);
await featureStep();

_bootstrapStepSubject.add(BootstrapSteps.completed);
} catch (e, t) {
_log.severe('init', e, t);
}
}

Future<void> rerunFailedSteps() async {
var failedStep = bootstrapStep;
final failedStepIndex = failedStep.index;

if (failedStep == BootstrapSteps.completed) return;

// ignore: avoid-missing-enum-constant-in-map
final steps = <BootstrapSteps, AsyncFunc>{
BootstrapSteps.storage: storageStep,
BootstrapSteps.connection: connectionStep,
BootstrapSteps.features: featureStep,
};

try {
for (var index = failedStepIndex;
index < BootstrapSteps.completed.index;
index++) {
final currentStep = BootstrapSteps.values[index];
// we do not update step during initialization to avoid changing page
// in ui
failedStep = currentStep;
await steps[currentStep]!();
}

_bootstrapStepSubject.add(BootstrapSteps.completed);
} catch (e, t) {
_log.severe('rerunFailedSteps:${failedStep.name}', e, t);
// update ui
_bootstrapStepSubject.add(failedStep);

// allow cubit catch this error
rethrow;
}
}

/// This step can not be failed during initialization, so we do not let
/// it to be re-runed (if failed - that's gg).
Future<void> coreStep(AppBuildType appBuildType) async {
await configureAppVersion();
await configureLogger(appBuildType);
}

Future<void> storageStep() async {
await configureEncryptedStorage();
await configureNavigationService();
await migrateStorage();
await configureStorageServices();
await configureNtpService();
// SetUp nekoton after storage migrations
await configureNekoton();
await configureBiometry();
}

Future<void> connectionStep() async {
await configureConnectionService();
}

Future<void> featureStep() async {
await configureFeatureServices();
}
}
Loading

0 comments on commit 44861ef

Please sign in to comment.