mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
CW-1130-add-app-shortcuts (#2599)
* add app shortcuts * update icons * Improve deep link and quick action navigation * bypass auth when allowedCreate state is active * remove spaces from file names [skip ci] --------- Co-authored-by: Serhii-Borodenko <17529954+Serhii-Borodenko@users.noreply.github.com> Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
co-authored by
Serhii-Borodenko
OmarHatem
parent
cf795bcc48
commit
2dd6cdce1e
+69
-4
@@ -60,6 +60,7 @@ import 'package:flutter_daemon/flutter_daemon.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:cw_core/root_dir.dart';
|
||||
import 'package:quick_actions/quick_actions.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:cw_core/window_size.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -70,6 +71,7 @@ import 'package:trezor_connect/trezor_connect.dart';
|
||||
final navigatorKey = GlobalKey<NavigatorState>();
|
||||
final rootKey = GlobalKey<RootState>();
|
||||
final RouteObserver<PageRoute<dynamic>> routeObserver = RouteObserver<PageRoute<dynamic>>();
|
||||
final quickActionsStream = StreamController<Uri?>.broadcast();
|
||||
|
||||
Future<void> main({Key? topLevelKey}) async {
|
||||
await runAppWithZone(topLevelKey: topLevelKey);
|
||||
@@ -80,6 +82,43 @@ Future<void> runAppWithZone({Key? topLevelKey}) async {
|
||||
|
||||
await runZonedGuarded(() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final Completer<String?> initialShortcutCompleter = Completer<String?>();
|
||||
|
||||
const QuickActions quickActions = QuickActions();
|
||||
|
||||
quickActions.initialize((String shortcutType) {
|
||||
// Complete the completer only once (for cold starts)
|
||||
if (!initialShortcutCompleter.isCompleted) {
|
||||
initialShortcutCompleter.complete(shortcutType);
|
||||
}
|
||||
|
||||
// Convert the shortcut type to a URI and add it to the stream
|
||||
final uri = Uri.parse('cakewallet://quickaction/$shortcutType');
|
||||
quickActionsStream.sink.add(uri);
|
||||
});
|
||||
|
||||
quickActions.setShortcutItems(<ShortcutItem>[
|
||||
const ShortcutItem(
|
||||
type: 'send',
|
||||
icon: 'send',
|
||||
localizedTitle: 'Send',
|
||||
localizedSubtitle: 'Send funds'),
|
||||
const ShortcutItem(
|
||||
type: 'receive',
|
||||
icon: 'receive',
|
||||
localizedTitle: 'Receive',
|
||||
localizedSubtitle: 'Receive funds')
|
||||
]);
|
||||
|
||||
// Fallback in case the initial shortcut is not received (normal cold start)
|
||||
Future<void>.delayed(const Duration(milliseconds: 500), () {
|
||||
if (!initialShortcutCompleter.isCompleted) {
|
||||
initialShortcutCompleter.complete(null);
|
||||
}
|
||||
});
|
||||
|
||||
final initialQuickAction = await initialShortcutCompleter.future;
|
||||
FlutterError.onError = ExceptionHandler.onError;
|
||||
|
||||
/// A callback that is invoked when an unhandled error occurs in the root
|
||||
@@ -133,11 +172,11 @@ Future<void> runAppWithZone({Key? topLevelKey}) async {
|
||||
runApp(
|
||||
DefaultAssetBundle(
|
||||
bundle: TestAssetBundle(),
|
||||
child: App(key: topLevelKey),
|
||||
child: App(key: topLevelKey, initialQuickAction:initialQuickAction,quickActionsStream: quickActionsStream.stream),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
runApp(App(key: topLevelKey));
|
||||
runApp(App(key: topLevelKey, initialQuickAction: initialQuickAction, quickActionsStream: quickActionsStream.stream));
|
||||
}
|
||||
|
||||
isAppRunning = true;
|
||||
@@ -333,9 +372,11 @@ Future<void> initialSetup({
|
||||
}
|
||||
|
||||
class App extends StatefulWidget {
|
||||
App({this.key});
|
||||
App({this.key, this.initialQuickAction, required this.quickActionsStream});
|
||||
|
||||
final Key? key;
|
||||
final String? initialQuickAction;
|
||||
final Stream<Uri?> quickActionsStream;
|
||||
@override
|
||||
AppState createState() => AppState();
|
||||
}
|
||||
@@ -366,9 +407,13 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
|
||||
statusBarBrightness: statusBarBrightness,
|
||||
statusBarIconBrightness: statusBarIconBrightness));
|
||||
|
||||
final appRouteObserver = AppRouteObserver();
|
||||
|
||||
return Root(
|
||||
key: widget.key ?? rootKey,
|
||||
appStore: appStore,
|
||||
initialQuickAction: widget.initialQuickAction,
|
||||
quickActionsStream: widget.quickActionsStream,
|
||||
authenticationStore: authenticationStore,
|
||||
navigatorKey: navigatorKey,
|
||||
authService: authService,
|
||||
@@ -379,7 +424,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
|
||||
child: ThemeProvider(
|
||||
themeStore: appStore.themeStore,
|
||||
materialAppBuilder: (context, theme, darkTheme, themeMode) => MaterialApp(
|
||||
navigatorObservers: [routeObserver],
|
||||
navigatorObservers: [routeObserver, appRouteObserver],
|
||||
navigatorKey: navigatorKey,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: theme,
|
||||
@@ -505,3 +550,23 @@ Future<void> backgroundSync() async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppRouteObserver extends RouteObserver<PageRoute<dynamic>> {
|
||||
final AppStore appStore = getIt.get<AppStore>();
|
||||
|
||||
@override
|
||||
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
||||
super.didPush(route, previousRoute);
|
||||
if (route is PageRoute) {
|
||||
appStore.currentRouteName = route.settings.name;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
||||
super.didPop(route, previousRoute);
|
||||
if (previousRoute is PageRoute) {
|
||||
appStore.currentRouteName = previousRoute.settings.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -160,12 +160,13 @@ late RouteSettings currentRouteSettings;
|
||||
|
||||
Route<T> handleRouteWithPlatformAwareness<T>(
|
||||
Widget Function(BuildContext) builder, {
|
||||
RouteSettings? settings,
|
||||
bool fullscreenDialog = false,
|
||||
}) {
|
||||
if (Platform.isIOS) {
|
||||
return CupertinoPageRoute<T>(builder: builder, fullscreenDialog: fullscreenDialog);
|
||||
return CupertinoPageRoute<T>(builder: builder, fullscreenDialog: fullscreenDialog, settings: settings);
|
||||
} else {
|
||||
return MaterialPageRoute<T>(builder: builder, fullscreenDialog: fullscreenDialog);
|
||||
return MaterialPageRoute<T>(builder: builder, fullscreenDialog: fullscreenDialog, settings: settings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,6 +424,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||
param1: initialPaymentRequest,
|
||||
param2: coinTypeToSpendFrom,
|
||||
),
|
||||
settings: settings
|
||||
);
|
||||
|
||||
case Routes.sendTemplate:
|
||||
@@ -430,12 +432,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||
fullscreenDialog: true, builder: (_) => getIt.get<SendTemplatePage>());
|
||||
|
||||
case Routes.receive:
|
||||
return CupertinoPageRoute<void>(builder: (context) => getIt.get<ReceivePage>());
|
||||
return CupertinoPageRoute<void>(builder: (context) => getIt.get<ReceivePage>(), settings: settings);
|
||||
|
||||
case Routes.addressPage:
|
||||
return handleRouteWithPlatformAwareness(
|
||||
(context) => getIt.get<AddressPage>(),
|
||||
);
|
||||
(context) => getIt.get<AddressPage>(), settings: settings);
|
||||
|
||||
case Routes.transactionDetails:
|
||||
return CupertinoPageRoute<void>(
|
||||
|
||||
@@ -36,6 +36,8 @@ class Root extends StatefulWidget {
|
||||
required this.tradeMonitor,
|
||||
required this.nodeSwitchingService,
|
||||
required this.trezorConnect,
|
||||
this.initialQuickAction,
|
||||
required this.quickActionsStream,
|
||||
}) : super(key: key);
|
||||
|
||||
final AuthenticationStore authenticationStore;
|
||||
@@ -47,6 +49,8 @@ class Root extends StatefulWidget {
|
||||
final TradeMonitor tradeMonitor;
|
||||
final NodeSwitchingService nodeSwitchingService;
|
||||
final TrezorConnect trezorConnect;
|
||||
final String? initialQuickAction;
|
||||
final Stream<Uri?> quickActionsStream;
|
||||
|
||||
@override
|
||||
RootState createState() => RootState();
|
||||
@@ -103,7 +107,18 @@ class RootState extends State<Root> with WidgetsBindingObserver {
|
||||
handleDeepLinking(uri);
|
||||
});
|
||||
|
||||
// listen for quick actions
|
||||
widget.quickActionsStream.listen((Uri? uri) {
|
||||
handleDeepLinking(uri);
|
||||
});
|
||||
|
||||
handleDeepLinking(await getInitialUri());
|
||||
|
||||
if (widget.initialQuickAction != null) {
|
||||
final uri = Uri.parse('cakewallet://quickaction/${widget.initialQuickAction}');
|
||||
handleDeepLinking(uri);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
}
|
||||
@@ -121,7 +136,14 @@ class RootState extends State<Root> with WidgetsBindingObserver {
|
||||
|
||||
bool requireAuth = await widget.authService.requireAuth();
|
||||
|
||||
if (!requireAuth && widget.authenticationStore.state == AuthenticationState.allowed) {
|
||||
|
||||
if (widget.authenticationStore.state == AuthenticationState.allowedCreate) {
|
||||
requireAuth = false;
|
||||
}
|
||||
|
||||
if (!requireAuth &&
|
||||
(widget.authenticationStore.state == AuthenticationState.allowed ||
|
||||
widget.authenticationStore.state == AuthenticationState.allowedCreate)) {
|
||||
_navigateToDeepLinkScreen();
|
||||
return;
|
||||
}
|
||||
@@ -129,7 +151,7 @@ class RootState extends State<Root> with WidgetsBindingObserver {
|
||||
_deepLinksReactionDisposer = reaction(
|
||||
(_) => widget.authenticationStore.state,
|
||||
(AuthenticationState state) {
|
||||
if (state == AuthenticationState.allowed) {
|
||||
if (state == AuthenticationState.allowed || state == AuthenticationState.allowedCreate) {
|
||||
if (widget.appStore.wallet == null) {
|
||||
waitForWalletInstance(context);
|
||||
} else {
|
||||
|
||||
@@ -34,6 +34,9 @@ abstract class AppStoreBase with Store {
|
||||
@observable
|
||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet;
|
||||
|
||||
@observable
|
||||
String? currentRouteName;
|
||||
|
||||
WalletListStore walletList;
|
||||
|
||||
SettingsStore settingsStore;
|
||||
|
||||
@@ -28,6 +28,8 @@ class LinkViewModel {
|
||||
bool get _isValidPaymentUri => currentLink?.path.isNotEmpty ?? false;
|
||||
bool get isWalletConnectLink => currentLink?.authority == 'wc';
|
||||
bool get isNanoGptLink => currentLink?.scheme == 'nano-gpt';
|
||||
bool get isQuickActionLink =>
|
||||
currentLink?.scheme == 'cakewallet' && currentLink?.host == 'quickaction';
|
||||
|
||||
String? getRouteToGo() {
|
||||
if (isWalletConnectLink) {
|
||||
@@ -38,6 +40,19 @@ class LinkViewModel {
|
||||
return Routes.walletConnectConnectionsListing;
|
||||
}
|
||||
|
||||
// Check for a quick action first.
|
||||
if (isQuickActionLink) {
|
||||
final action = currentLink!.pathSegments.isNotEmpty ? currentLink!.pathSegments.first : null;
|
||||
switch (action) {
|
||||
case 'send':
|
||||
return Routes.send;
|
||||
case 'receive':
|
||||
return Routes.addressPage;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (authenticationStore.state == AuthenticationState.uninitialized) {
|
||||
return null;
|
||||
}
|
||||
@@ -65,6 +80,10 @@ class LinkViewModel {
|
||||
return currentLink;
|
||||
}
|
||||
|
||||
if (isQuickActionLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isNanoGptLink) {
|
||||
switch (currentLink?.authority ?? '') {
|
||||
case "exchange":
|
||||
@@ -113,16 +132,31 @@ class LinkViewModel {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent navigating to the same route again.
|
||||
if (appStore.currentRouteName == route) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNanoGptLink) {
|
||||
if (route == Routes.buySellPage || route == Routes.exchange) {
|
||||
await _errorToast(S.current.nano_gpt_thanks_message, fontSize: 14);
|
||||
}
|
||||
}
|
||||
|
||||
// Quick actions must reset navigation to Dashboard → TargetPage
|
||||
if (isQuickActionLink) {
|
||||
currentLink = null;
|
||||
navigatorKey.currentState?.pushNamedAndRemoveUntil(
|
||||
route,
|
||||
ModalRoute.withName(Routes.dashboard),
|
||||
arguments: args,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal navigation flow
|
||||
currentLink = null;
|
||||
navigatorKey.currentState?.pushNamed(
|
||||
route,
|
||||
arguments: args,
|
||||
);
|
||||
navigatorKey.currentState?.pushNamed(route, arguments: args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user