Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
64a1974
EWM-617: root alert screen
knightsforce Dec 24, 2025
fd96f03
EWM-617: check root on device
knightsforce Dec 24, 2025
cb8ec24
EWM-617: after analyze
knightsforce Dec 24, 2025
0484a34
Merge branch 'refs/heads/dev' into feature/EWM-617-check_root
knightsforce Jan 5, 2026
91e044e
EWM-617: after merge
knightsforce Jan 5, 2026
7c404b3
Merge branch 'refs/heads/dev' into feature/EWM-617-check_root
knightsforce Jan 19, 2026
0551c10
EWM-617: use RootCheckerPlus
knightsforce Jan 19, 2026
c0772d8
Merge branch 'refs/heads/dev' into feature/EWM-617-check_root
knightsforce Jan 27, 2026
d978523
EWM-617: After merge
knightsforce Jan 27, 2026
9511104
EWM-617: UI for Root device screen
knightsforce Feb 2, 2026
cef57d0
EWM-617: Show root info bottom sheet
knightsforce Feb 2, 2026
e2df018
EWM-617: Show root info bottom sheet
knightsforce Feb 2, 2026
2abce2f
Merge remote-tracking branch 'origin/feature/EWM-617-check_root' into…
knightsforce Feb 2, 2026
0254ad8
EWM-617: format colors_v2.dart
knightsforce Feb 2, 2026
8390f58
Merge branch 'refs/heads/dev' into feature/EWM-617-check_root
knightsforce Feb 2, 2026
04340eb
Merge branch 'refs/heads/dev' into feature/EWM-617-check_root
knightsforce Feb 11, 2026
117cbbb
feature/EWM-617 isRootChecker for Android, isJailbreak for iOS, alway…
knightsforce Feb 19, 2026
71c5f15
Merge branch 'dev' into feature/EWM-617-check_root
knightsforce Feb 23, 2026
0b2d8f0
feat/EWM-617 root_device_alert_screen.dart remove TODO
knightsforce Feb 23, 2026
2e16a69
feat/EWM-617 root update files
knightsforce Feb 23, 2026
682f027
feat/EWM-617 add root_device_delegate_test
knightsforce Feb 23, 2026
d1d6b9e
Merge branch 'dev' into feature/EWM-617-check_root
knightsforce Feb 27, 2026
892ce3e
fix: root_device_delegate_test.dart
knightsforce Mar 2, 2026
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
Binary file added assets/images/lock_bg/1.5x/lock.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/lock_bg/2x/lock.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/lock_bg/3x/lock.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/lock_bg/lock.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 11 additions & 1 deletion assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -791,5 +791,15 @@
"passwordLockedUntil": "Password locked until {}",
"invalidPayloadError": "Payload is invalid",
"invalidStateInitError": "StateInit is invalid",
"unsupportedWalletTypeError": "Unsupported wallet type"
"unsupportedWalletTypeError": "Unsupported wallet type",
"rootDetectedTitle": "Root / Jailbreak detected",
"rootDetectedDescription": "This device has a modified security environment. Wallet security cannot be guaranteed.",
"rootDetectedRisk": "By continuing, you acknowledge\nand accept the associated risks",
"rootDetectedAccept": "I understand and accept the risks",
"rootInfoTitle": "Why this matters",
"rootInfoDescription": "Root/Jailbreak weakens system security restrictions, allowing other apps or system modifications to interfere with the wallet’s operation",
"whatYouCanDoTitle": "What you can do:",
"whatYouCanDoDescription0": "Use a device without root/jailbreak",
"whatYouCanDoDescription1": "Disable root/jailbreak and reboot the device",
"whatYouCanDoDescription2": "Use a separate “clean” device for the wallet"
}
13 changes: 12 additions & 1 deletion assets/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -791,5 +791,16 @@
"passwordLockedUntil": "Password locked until {}",
"invalidPayloadError": "Payload is invalid",
"invalidStateInitError": "StateInit is invalid",
"unsupportedWalletTypeError": "Unsupported wallet type"
"unsupportedWalletTypeError": "Unsupported wallet type",
"rootDetectedTitle": "Root / Jailbreak detected",
"rootDetectedDescription": "This device has a modified security environment. Wallet security cannot be guaranteed.",
"rootDetectedRisk": "By continuing, you acknowledge\nand accept the associated risks",
"rootDetectedAccept": "I understand and accept the risks",
"rootInfoTitle": "Why this matters",
"rootInfoDescription": "Root/Jailbreak weakens system security restrictions, allowing other apps or system modifications to interfere with the wallet’s operation",
"whatYouCanDo": "What you can do:",
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate key detected: whatYouCanDo on line 801 and whatYouCanDoTitle on line 802 both have the same value "What you can do:". Only one key should be used. Looking at the English translation file, only whatYouCanDoTitle is present. The whatYouCanDo key should be removed from this file.

Suggested change
"whatYouCanDo": "What you can do:",

Copilot uses AI. Check for mistakes.
"whatYouCanDoTitle": "What you can do:",
"whatYouCanDoDescription0": "Use a device without root/jailbreak",
"whatYouCanDoDescription1": "Disable root/jailbreak and reboot the device",
"whatYouCanDoDescription2": "Use a separate “clean” device for the wallet"
}
2 changes: 2 additions & 0 deletions lib/app/router/compass/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ abstract class CompassBaseRoute {
/// Top-level routes are registered directly with the root GoRouter instance.
final bool isTopLevel;
}

abstract interface class IndependentSeedRoute {}
46 changes: 46 additions & 0 deletions lib/app/service/bootstrap/bootstrap_navigation_delegate.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:app/app/router/compass/guard.dart';
import 'package:app/app/router/router.dart';
import 'package:app/app/service/navigation_service.dart';
import 'package:app/feature/onboarding/route.dart';
import 'package:app/feature/wallet/route.dart';
import 'package:injectable/injectable.dart';
import 'package:logging/logging.dart';
import 'package:nekoton_repository/nekoton_repository.dart';

@injectable
class BootstrapNavigationDelegate {
BootstrapNavigationDelegate(
this._nekotonRepository,
this._navigationService,
this._router,
);

final NekotonRepository _nekotonRepository;
final NavigationService _navigationService;
final CompassRouter _router;

bool? get hasSeeds => _nekotonRepository.hasSeeds.valueOrNull;

final _logger = Logger('BootstrapNavigationDelegate');

Future<void> goToResultBootstrap() async {
if (hasSeeds == false) {
_logger.info('Initial navigation. Navigate to onboarding');
_router.compassPoint(const OnBoardingRouteData());
return;
}

final savedNavigation = await _navigationService.getSavedState();

if (savedNavigation != null) {
_logger.info('Initial navigation. Navigate to $savedNavigation');
// Use CompassRouter methods for all navigation to maintain consistency
_router.compassPoint(
UnsafeRedirectCompassRouteData(route: savedNavigation),
);
} else {
_logger.info('Initial navigation. Navigate to wallet');
_router.compassPoint(const WalletRouteData());
}
}
}
15 changes: 8 additions & 7 deletions lib/app/view/app_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import 'package:app/app/router/router.dart';
import 'package:app/app/service/app_lifecycle_service.dart';
import 'package:app/app/service/app_links/app_links.dart';
import 'package:app/app/service/biometry_service.dart';
import 'package:app/app/service/bootstrap/bootstrap_navigation_delegate.dart';
import 'package:app/app/service/bootstrap/bootstrap_service.dart';
import 'package:app/app/service/bootstrap/bootstrap_steps.dart';
import 'package:app/app/service/bootstrap/configurators/logger.dart';
import 'package:app/app/service/navigation_service.dart';
import 'package:app/app/service/pending_deep_link_service.dart';
import 'package:app/app/view/app.dart';
import 'package:app/feature/browser/domain/browser_launcher.dart';
Expand All @@ -17,6 +17,7 @@ import 'package:app/feature/localization/localization.dart';
import 'package:app/feature/messenger/messenger.dart';
import 'package:app/feature/nft/route.dart';
import 'package:app/feature/profile/route.dart';
import 'package:app/feature/root_device_alert/domain/root_device_delegate.dart';
import 'package:app/feature/wallet/route.dart';
import 'package:app/generated/generated.dart';
import 'package:elementary/elementary.dart';
Expand All @@ -41,7 +42,8 @@ class AppModel extends ElementaryModel with WidgetsBindingObserver {
this._nekotonRepository,
this._bootstrapService,
this._pendingDeepLinkService,
this._navigationService,
this._rootDeviceDelegate,
this._bootstrapNavigationDelegate,
) : super(errorHandler: errorHandler);

final CompassRouter router;
Expand All @@ -56,7 +58,8 @@ class AppModel extends ElementaryModel with WidgetsBindingObserver {
final NekotonRepository _nekotonRepository;
final BootstrapService _bootstrapService;
final PendingDeepLinkService _pendingDeepLinkService;
final NavigationService _navigationService;
final RootDeviceDelegate _rootDeviceDelegate;
final BootstrapNavigationDelegate _bootstrapNavigationDelegate;

AppLifecycleListener? _listener;
StreamSubscription<BrowserAppLinksData>? _appLinksSubs;
Expand All @@ -69,7 +72,7 @@ class AppModel extends ElementaryModel with WidgetsBindingObserver {
Stream<BootstrapSteps> get bootstrapStepStream =>
_bootstrapService.bootstrapStepStream;

bool? get hasSeeds => _nekotonRepository.hasSeeds.valueOrNull;
Future<bool> get isShowRootScreen => _rootDeviceDelegate.isShowRootScreen;

@override
void init() {
Expand Down Expand Up @@ -101,9 +104,7 @@ class AppModel extends ElementaryModel with WidgetsBindingObserver {
Future<bool> checkCrashDetected() =>
_crashDetectorService.checkCrashDetected();

Future<String?> getSavedNavigation() {
return _navigationService.getSavedState();
}
void next() => _bootstrapNavigationDelegate.goToResultBootstrap();

void _onStateChanged(AppLifecycleState state) {
switch (state) {
Expand Down
26 changes: 4 additions & 22 deletions lib/app/view/app_wm.dart
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import 'dart:async';

import 'package:app/app/router/compass/compass.dart';
import 'package:app/app/service/bootstrap/bootstrap_steps.dart';
import 'package:app/app/view/app.dart';
import 'package:app/app/view/app_model.dart';
import 'package:app/app/view/message_viewer.dart';
import 'package:app/core/wm/custom_wm.dart';
import 'package:app/feature/onboarding/route.dart';
import 'package:app/feature/wallet/route.dart';
import 'package:app/feature/root_device_alert/route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:elementary/elementary.dart';
import 'package:flutter/widgets.dart';
import 'package:injectable/injectable.dart';
import 'package:logging/logging.dart';

/// [WidgetModel] для [App]
@injectable
class AppWidgetModel extends CustomWidgetModel<App, AppModel> {
AppWidgetModel(super.model);

final _logger = Logger('AppWidgetModel');

late final _messageViewer = MessageViewer(
messagesExistStream: model.messagesExistStream,
getRootContext: () => model.navContext,
Expand Down Expand Up @@ -62,23 +57,10 @@ class AppWidgetModel extends CustomWidgetModel<App, AppModel> {
await _bootstrapStepsSubs?.cancel();
_bootstrapStepsSubs = null;

if (model.hasSeeds == false) {
_logger.info('Initial navigation. Navigate to onboarding');
router.compassPoint(const OnBoardingRouteData());
return;
}

final savedNavigation = await model.getSavedNavigation();

if (savedNavigation != null) {
_logger.info('Initial navigation. Navigate to $savedNavigation');
// Use CompassRouter methods for all navigation to maintain consistency
router.compassPoint(
UnsafeRedirectCompassRouteData(route: savedNavigation),
);
if (await model.isShowRootScreen) {
router.compassPoint(const RootDeviceAlertRouteData());
} else {
_logger.info('Initial navigation. Navigate to wallet');
router.compassPoint(const WalletRouteData());
model.next();
}
}
Comment on lines 57 to 65
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The navigation logic has been simplified, but the removed code that handled saved navigation state is now in BootstrapNavigationDelegate.goToResultBootstrap(). While this refactoring improves code organization by extracting navigation logic into a delegate, verify that all edge cases previously handled (crash detection, saved state restoration) still work correctly in the new flow, especially when combined with the root detection check.

Copilot uses AI. Check for mistakes.
}
58 changes: 45 additions & 13 deletions lib/di/di.config.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions lib/feature/onboarding/guard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class OnboardingGuard extends CompassGuard {
return null;
}

if (!hasSeeds && rootRoute is! OnBoardingRoute) {
if (!hasSeeds && rootRoute is! IndependentSeedRoute) {
// Not onboarded, redirect to onboarding
_log.info('No seeds, redirecting to onboarding');
return const OnBoardingRouteData();
Expand All @@ -65,7 +65,7 @@ class OnboardingGuard extends CompassGuard {

if (!_bootstrapService.isConfigured) return;

if (!hasSeeds && rootRoutePath is! OnBoardingRoute) {
if (!hasSeeds && rootRoutePath is! IndependentSeedRoute) {
_log.info('No seeds, redirecting to onboarding');
_router?.compassPoint(const OnBoardingRouteData());
}
Expand Down
3 changes: 2 additions & 1 deletion lib/feature/onboarding/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import 'package:injectable/injectable.dart';

@named
@Singleton(as: CompassBaseRoute)
class OnBoardingRoute extends CompassRouteParameterless<OnBoardingRouteData> {
class OnBoardingRoute extends CompassRouteParameterless<OnBoardingRouteData>
implements IndependentSeedRoute {
OnBoardingRoute(
@Named.from(ChooseNetworkRoute) CompassBaseRoute chooseNetworkRoute,
) : super(
Expand Down
36 changes: 36 additions & 0 deletions lib/feature/root_device_alert/domain/root_device_delegate.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'dart:io';

import 'package:injectable/injectable.dart';
import 'package:root_checker_plus/root_checker_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';

@injectable
class RootDeviceDelegate {
static const userKnowsRootKey = 'user_know_root';

Future<bool> get isShowRootScreen async {
return await _isRootDevice && !(await _isUserKnowRoot);
}

Future<bool> get _isRootDevice async {
if (Platform.isAndroid) {
return (await RootCheckerPlus.isRootChecker()) ?? false;
} else if (Platform.isIOS) {
return (await RootCheckerPlus.isJailbreak()) ?? false;
}

return true;
}

Future<bool> get _isUserKnowRoot async {
final prefs = await SharedPreferences.getInstance();

return prefs.getBool(userKnowsRootKey) ?? false;
}

Future<bool> setUserKnowRoot() async {
final prefs = await SharedPreferences.getInstance();

return prefs.setBool(userKnowsRootKey, true);
}
}
Loading