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