[push-deeplink-routing-contract] Generalize push tap routing to DeepLinkDestination

Replace order_status-only NotificationCenter path with a single
DeepLinkDestination enum + PushDeepLinkParser, decoded once in
PushNotificationCoordinator and dispatched via ContentView.route(to:).
Also fixes NotificationService reading userInfo["image"] instead of
the guide's stale "imageUrl" key.
This commit is contained in:
Daniel Arantes Loverde
2026-08-05 15:19:18 -03:00
parent 7dd4cda1a4
commit c4d6998595
4 changed files with 109 additions and 47 deletions

View File

@@ -15,7 +15,7 @@ struct ContentView: View {
@State private var sessionExpiredObserver: NSObjectProtocol?
@State var cartResetObserver: Any?
@State var appResumeObserver: Any?
@State var pushOrderTapObserver: Any?
@State var pushDeepLinkObserver: Any?
@StateObject var snackbarCenter = SnackbarCenter.shared
var body: some View {
@@ -91,13 +91,13 @@ struct ContentView: View {
attachCartResetObserverIfNeeded()
attachAppResumeObserverIfNeeded()
attachSessionExpiredObserverIfNeeded()
attachPushOrderTapObserverIfNeeded()
attachPushDeepLinkObserverIfNeeded()
}
.onDisappear {
detachCartResetObserver()
detachAppResumeObserver()
detachSessionExpiredObserver()
detachPushOrderTapObserver()
detachPushDeepLinkObserver()
}
}
@@ -244,36 +244,50 @@ struct ContentView: View {
self.appResumeObserver = nil
}
/// §6 of the push notifications guide an `order_status` push tap
/// (reported by `PushNotificationCoordinator`) routes here to the
/// Profile tab's Orders list, pre-targeted at that order.
private func attachPushOrderTapObserverIfNeeded() {
guard pushOrderTapObserver == nil else { return }
pushOrderTapObserver = NotificationCenter.default.addObserver(
forName: .pushTappedOrderStatus,
/// §6 of the push notifications guide, generalized: any tapped push that
/// resolves to a `DeepLinkDestination` (reported by
/// `PushNotificationCoordinator`) lands here. Adding a future promo/coupon
/// screen means adding one case to `route(to:)` below this observer,
/// the notification name, and the AppState plumbing for it stay put.
private func attachPushDeepLinkObserverIfNeeded() {
guard pushDeepLinkObserver == nil else { return }
pushDeepLinkObserver = NotificationCenter.default.addObserver(
forName: .pushDeepLinkReceived,
object: nil,
queue: nil
) { notification in
let orderId = notification.userInfo?["orderId"] as? String
let shortId = notification.userInfo?["shortId"] as? String
guard let destination = notification.userInfo?["destination"] as? DeepLinkDestination else { return }
Task { @MainActor in
guard root == .main, let orderId else { return }
appState.pendingOrderDeepLink = OrderRouteContext(
orderId: orderId,
shortId: shortId,
paymentMethod: nil,
total: nil,
intent: .auto
)
selectedTab = .profile
route(to: destination)
}
}
}
private func detachPushOrderTapObserver() {
guard let pushOrderTapObserver else { return }
NotificationCenter.default.removeObserver(pushOrderTapObserver)
self.pushOrderTapObserver = nil
private func detachPushDeepLinkObserver() {
guard let pushDeepLinkObserver else { return }
NotificationCenter.default.removeObserver(pushDeepLinkObserver)
self.pushDeepLinkObserver = nil
}
@MainActor
private func route(to destination: DeepLinkDestination) {
guard root == .main else { return }
switch destination {
case .orderTracking(let orderId, let shortId):
appState.pendingOrderDeepLink = OrderRouteContext(
orderId: orderId,
shortId: shortId,
paymentMethod: nil,
total: nil,
intent: .auto
)
selectedTab = .profile
case .screen(let name, let params):
// Promo/coupon screens (participating stores/products, §6's
// `targetScreen` convention) land here once they exist see
// decisions/2026-08-04-push-deeplink-routing-contract.md.
logger.debug("Unhandled deep-link screen: \(name, privacy: .public) params=\(params, privacy: .public)")
}
}
@MainActor