diff --git a/android/app/src/main/res/drawable/receive.xml b/android/app/src/main/res/drawable/receive.xml
new file mode 100644
index 000000000..6d54fd70a
--- /dev/null
+++ b/android/app/src/main/res/drawable/receive.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/android/app/src/main/res/drawable/send.xml b/android/app/src/main/res/drawable/send.xml
new file mode 100644
index 000000000..c83ea31d4
--- /dev/null
+++ b/android/app/src/main/res/drawable/send.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/ios/Runner/Assets.xcassets/Contents.json b/ios/Runner/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/receive.imageset/Contents.json b/ios/Runner/Assets.xcassets/receive.imageset/Contents.json
new file mode 100644
index 000000000..b562e7622
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/receive.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "receive.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "receive-1.svg",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "receive-2.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/receive.imageset/receive-1.svg b/ios/Runner/Assets.xcassets/receive.imageset/receive-1.svg
new file mode 100644
index 000000000..14c4f7f14
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/receive.imageset/receive-1.svg
@@ -0,0 +1,4 @@
+
diff --git a/ios/Runner/Assets.xcassets/receive.imageset/receive-2.svg b/ios/Runner/Assets.xcassets/receive.imageset/receive-2.svg
new file mode 100644
index 000000000..14c4f7f14
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/receive.imageset/receive-2.svg
@@ -0,0 +1,4 @@
+
diff --git a/ios/Runner/Assets.xcassets/receive.imageset/receive.svg b/ios/Runner/Assets.xcassets/receive.imageset/receive.svg
new file mode 100644
index 000000000..14c4f7f14
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/receive.imageset/receive.svg
@@ -0,0 +1,4 @@
+
diff --git a/ios/Runner/Assets.xcassets/send.imageset/Contents.json b/ios/Runner/Assets.xcassets/send.imageset/Contents.json
new file mode 100644
index 000000000..b1fc4bad2
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/send.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "send.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "send-1.svg",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "send-2.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/send.imageset/send-1.svg b/ios/Runner/Assets.xcassets/send.imageset/send-1.svg
new file mode 100644
index 000000000..63d78e94b
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/send.imageset/send-1.svg
@@ -0,0 +1,4 @@
+
diff --git a/ios/Runner/Assets.xcassets/send.imageset/send-2.svg b/ios/Runner/Assets.xcassets/send.imageset/send-2.svg
new file mode 100644
index 000000000..63d78e94b
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/send.imageset/send-2.svg
@@ -0,0 +1,4 @@
+
diff --git a/ios/Runner/Assets.xcassets/send.imageset/send.svg b/ios/Runner/Assets.xcassets/send.imageset/send.svg
new file mode 100644
index 000000000..63d78e94b
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/send.imageset/send.svg
@@ -0,0 +1,4 @@
+
diff --git a/lib/main.dart b/lib/main.dart
index f30f09e95..21e1d4c19 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -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();
final rootKey = GlobalKey();
final RouteObserver> routeObserver = RouteObserver>();
+final quickActionsStream = StreamController.broadcast();
Future main({Key? topLevelKey}) async {
await runAppWithZone(topLevelKey: topLevelKey);
@@ -80,6 +82,43 @@ Future runAppWithZone({Key? topLevelKey}) async {
await runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
+
+ final Completer initialShortcutCompleter = Completer();
+
+ 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([
+ 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.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 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 initialSetup({
}
class App extends StatefulWidget {
- App({this.key});
+ App({this.key, this.initialQuickAction, required this.quickActionsStream});
final Key? key;
+ final String? initialQuickAction;
+ final Stream quickActionsStream;
@override
AppState createState() => AppState();
}
@@ -366,9 +407,13 @@ class AppState extends State 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 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 backgroundSync() async {
}
}
}
+
+class AppRouteObserver extends RouteObserver> {
+ final AppStore appStore = getIt.get();
+
+ @override
+ void didPush(Route route, Route? previousRoute) {
+ super.didPush(route, previousRoute);
+ if (route is PageRoute) {
+ appStore.currentRouteName = route.settings.name;
+ }
+ }
+
+ @override
+ void didPop(Route route, Route? previousRoute) {
+ super.didPop(route, previousRoute);
+ if (previousRoute is PageRoute) {
+ appStore.currentRouteName = previousRoute.settings.name;
+ }
+ }
+}
diff --git a/lib/router.dart b/lib/router.dart
index 93d1eb395..7ef811ce2 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -160,12 +160,13 @@ late RouteSettings currentRouteSettings;
Route handleRouteWithPlatformAwareness(
Widget Function(BuildContext) builder, {
+ RouteSettings? settings,
bool fullscreenDialog = false,
}) {
if (Platform.isIOS) {
- return CupertinoPageRoute(builder: builder, fullscreenDialog: fullscreenDialog);
+ return CupertinoPageRoute(builder: builder, fullscreenDialog: fullscreenDialog, settings: settings);
} else {
- return MaterialPageRoute(builder: builder, fullscreenDialog: fullscreenDialog);
+ return MaterialPageRoute(builder: builder, fullscreenDialog: fullscreenDialog, settings: settings);
}
}
@@ -423,6 +424,7 @@ Route createRoute(RouteSettings settings) {
param1: initialPaymentRequest,
param2: coinTypeToSpendFrom,
),
+ settings: settings
);
case Routes.sendTemplate:
@@ -430,12 +432,11 @@ Route createRoute(RouteSettings settings) {
fullscreenDialog: true, builder: (_) => getIt.get());
case Routes.receive:
- return CupertinoPageRoute(builder: (context) => getIt.get());
+ return CupertinoPageRoute(builder: (context) => getIt.get(), settings: settings);
case Routes.addressPage:
return handleRouteWithPlatformAwareness(
- (context) => getIt.get(),
- );
+ (context) => getIt.get(), settings: settings);
case Routes.transactionDetails:
return CupertinoPageRoute(
diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart
index 7f71dcb46..08ec7504c 100644
--- a/lib/src/screens/root/root.dart
+++ b/lib/src/screens/root/root.dart
@@ -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 quickActionsStream;
@override
RootState createState() => RootState();
@@ -103,7 +107,18 @@ class RootState extends State 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 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 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 {
diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart
index c3e7cc456..183b723ea 100644
--- a/lib/store/app_store.dart
+++ b/lib/store/app_store.dart
@@ -34,6 +34,9 @@ abstract class AppStoreBase with Store {
@observable
WalletBase, TransactionInfo>? wallet;
+ @observable
+ String? currentRouteName;
+
WalletListStore walletList;
SettingsStore settingsStore;
diff --git a/lib/view_model/link_view_model.dart b/lib/view_model/link_view_model.dart
index 513f8d58e..3320348e7 100644
--- a/lib/view_model/link_view_model.dart
+++ b/lib/view_model/link_view_model.dart
@@ -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);
}
}
}
diff --git a/pubspec_base.yaml b/pubspec_base.yaml
index f5ecd6e38..d0c6090f1 100644
--- a/pubspec_base.yaml
+++ b/pubspec_base.yaml
@@ -152,6 +152,7 @@ dependencies:
ref: b48a9defddce036048bae02e2069ebcbd6824afc
torch_dart:
path: ./scripts/torch_dart
+ quick_actions: ^1.1.0
dev_dependencies:
flutter_test: