diff --git a/Flipcash/Core/AppDelegate.swift b/Flipcash/Core/AppDelegate.swift
index 6e23da2d..123d3417 100644
--- a/Flipcash/Core/AppDelegate.swift
+++ b/Flipcash/Core/AppDelegate.swift
@@ -80,9 +80,27 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
object: nil
)
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(handleDeepLinkNotification(_:)),
+ name: .shortcutDeepLinkReceived,
+ object: nil
+ )
+
return true
}
+ /// Routes scene events to ``SceneDelegate``. SwiftUI's `App` lifecycle
+ /// uses its own implicit scene delegate by default, swallowing
+ /// `windowScene(_:performActionFor:)`; without this method, the
+ /// `UISceneDelegateClassName` declared in `Info.plist` is not honored
+ /// and quick actions are dropped.
+ func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
+ let config = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
+ config.delegateClass = SceneDelegate.self
+ return config
+ }
+
// MARK: - Lifecycle -
func scenePhaseChanged(_ phase: ScenePhase) {
diff --git a/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift b/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift
index 0a3d2acc..ee3a9dfe 100644
--- a/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift
+++ b/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift
@@ -93,11 +93,20 @@ final class DeepLinkController {
case .token(let mint):
return actionForCurrencyInfo(mint: mint)
-
+
+ case .give:
+ return actionForOpenSheet(.give)
+
+ case .wallet:
+ return actionForOpenSheet(.balance)
+
+ case .discover:
+ return actionForOpenSheet(.discover)
+
case .unknown:
break
}
-
+
return nil
}
@@ -134,6 +143,13 @@ final class DeepLinkController {
sessionAuthenticator: sessionAuthenticator
)
}
+
+ private func actionForOpenSheet(_ sheet: AppRouter.SheetPresentation) -> DeepLinkAction {
+ DeepLinkAction(
+ kind: .openSheet(sheet),
+ sessionAuthenticator: sessionAuthenticator
+ )
+ }
}
extension DeepLinkController {
@@ -212,6 +228,21 @@ struct DeepLinkAction {
Analytics.deeplinkRouted(kind: kind)
container.appRouter.navigate(to: .currencyInfo(mint))
}
+
+ case .openSheet(let sheet):
+ if case .loggedIn(let container) = sessionAuthenticator.state {
+ Analytics.deeplinkRouted(kind: kind)
+ if sheet == .give {
+ let rate = container.ratesController.rateForBalanceCurrency()
+ guard container.session.hasGiveableBalance(for: rate) else {
+ container.session.dialogItem = .noGiveableBalance(
+ onDiscover: { container.appRouter.present(.discover) }
+ )
+ return
+ }
+ }
+ container.appRouter.present(sheet)
+ }
}
}
}
@@ -224,6 +255,7 @@ extension DeepLinkAction {
case receiveCashLink(MnemonicPhrase)
case verifyEmail(VerificationDescription)
case currencyInfo(PublicKey)
+ case openSheet(AppRouter.SheetPresentation)
}
}
@@ -234,6 +266,7 @@ extension DeepLinkAction.Kind {
case .receiveCashLink: "CashLink"
case .verifyEmail: "EmailVerification"
case .currencyInfo: "TokenInfo"
+ case .openSheet(let sheet): "Sheet:\(sheet)"
}
}
}
diff --git a/Flipcash/Core/Controllers/Deep Links/Route.swift b/Flipcash/Core/Controllers/Deep Links/Route.swift
index 589e26af..80ad0836 100644
--- a/Flipcash/Core/Controllers/Deep Links/Route.swift
+++ b/Flipcash/Core/Controllers/Deep Links/Route.swift
@@ -85,6 +85,9 @@ nonisolated extension Route {
case cash
case verifyEmail
case token(PublicKey)
+ case give
+ case wallet
+ case discover
case unknown(String)
static func parse(path: String) -> Path? {
@@ -111,6 +114,12 @@ nonisolated extension Route {
return nil
}
return .token(mint)
+ case "give":
+ return .give
+ case "wallet":
+ return .wallet
+ case "discover":
+ return .discover
default:
return .unknown(url.lastPathComponent)
}
diff --git a/Flipcash/Core/Controllers/NotificationController.swift b/Flipcash/Core/Controllers/NotificationController.swift
index e067a90d..448fdb0d 100644
--- a/Flipcash/Core/Controllers/NotificationController.swift
+++ b/Flipcash/Core/Controllers/NotificationController.swift
@@ -69,5 +69,6 @@ extension NSNotification.Name {
static let pushNotificationWillPresent = Notification.Name("com.code.pushNotificationWillPresent")
static let pushDeepLinkReceived = Notification.Name("com.code.pushDeepLinkReceived")
static let qrDeepLinkReceived = Notification.Name("com.code.qrDeepLinkReceived")
+ static let shortcutDeepLinkReceived = Notification.Name("com.code.shortcutDeepLinkReceived")
static let messageNotificationReceived = Notification.Name("com.code.messageNotificationReceived")
}
diff --git a/Flipcash/Core/SceneDelegate.swift b/Flipcash/Core/SceneDelegate.swift
new file mode 100644
index 00000000..2282ccfa
--- /dev/null
+++ b/Flipcash/Core/SceneDelegate.swift
@@ -0,0 +1,43 @@
+//
+// SceneDelegate.swift
+// Flipcash
+//
+
+import UIKit
+import FlipcashCore
+
+private let logger = Logger(label: "flipcash.scene-delegate")
+
+/// Bridges quick-action taps into the existing deep-link pipeline. SwiftUI's
+/// `App` lifecycle doesn't forward `UIApplicationShortcutItem` events to
+/// `AppDelegate`, so we receive them here, pull the embedded URL out of the
+/// shortcut's `userInfo`, and post the same notification the rest of the
+/// app's URL handlers already observe.
+final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+ var window: UIWindow?
+
+ func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
+ if let shortcut = connectionOptions.shortcutItem {
+ postDeepLink(for: shortcut)
+ }
+ }
+
+ func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
+ completionHandler(postDeepLink(for: shortcutItem))
+ }
+
+ @discardableResult
+ private func postDeepLink(for shortcut: UIApplicationShortcutItem) -> Bool {
+ guard let urlString = shortcut.userInfo?["url"] as? String,
+ let url = URL(string: urlString) else {
+ logger.warning("Quick action missing url userInfo", metadata: ["type": "\(shortcut.type)"])
+ return false
+ }
+ NotificationCenter.default.post(
+ name: .shortcutDeepLinkReceived,
+ object: nil,
+ userInfo: ["url": url]
+ )
+ return true
+ }
+}
diff --git a/Flipcash/Core/Screens/Main/ScanViewModel.swift b/Flipcash/Core/Screens/Main/ScanViewModel.swift
index 931848c1..9099e3f2 100644
--- a/Flipcash/Core/Screens/Main/ScanViewModel.swift
+++ b/Flipcash/Core/Screens/Main/ScanViewModel.swift
@@ -116,7 +116,7 @@ class ScanViewModel {
switch route.path {
case .cash, .token:
return true
- case .login, .verifyEmail, .unknown:
+ case .login, .verifyEmail, .give, .wallet, .discover, .unknown:
return false
}
}
diff --git a/Flipcash/Core/Session/SessionAuthenticator.swift b/Flipcash/Core/Session/SessionAuthenticator.swift
index 3705aae0..1a30cec4 100644
--- a/Flipcash/Core/Session/SessionAuthenticator.swift
+++ b/Flipcash/Core/Session/SessionAuthenticator.swift
@@ -451,7 +451,6 @@ struct SessionContainer {
.environment(onrampCoordinator)
.environment(onrampDeeplinkInbox)
}
-
}
extension View {
diff --git a/Flipcash/Supporting Files/Info.plist b/Flipcash/Supporting Files/Info.plist
index ff9fc3be..e53c156c 100644
--- a/Flipcash/Supporting Files/Info.plist
+++ b/Flipcash/Supporting Files/Info.plist
@@ -21,6 +21,65 @@
SQLiteVersion
11
+ UIApplicationShortcutItems
+
+
+ UIApplicationShortcutItemType
+ com.flipcash.shortcut.give
+ UIApplicationShortcutItemTitle
+ Give
+ UIApplicationShortcutItemIconSymbolName
+ banknote
+ UIApplicationShortcutItemUserInfo
+
+ url
+ flipcash://give
+
+
+
+ UIApplicationShortcutItemType
+ com.flipcash.shortcut.wallet
+ UIApplicationShortcutItemTitle
+ Wallet
+ UIApplicationShortcutItemIconSymbolName
+ wallet.bifold
+ UIApplicationShortcutItemUserInfo
+
+ url
+ flipcash://wallet
+
+
+
+ UIApplicationShortcutItemType
+ com.flipcash.shortcut.discover
+ UIApplicationShortcutItemTitle
+ Discover
+ UIApplicationShortcutItemIconSymbolName
+ binoculars
+ UIApplicationShortcutItemUserInfo
+
+ url
+ flipcash://discover
+
+
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneConfigurationName
+ Default Configuration
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+
+
+
+
UILaunchScreen
UIColorName