[push/opt-in-prompts] Implement push notifications client integration

Build the full client half of docs/api/push-notifications-integration-guide.md:
OS permission + APNs device-token registration and pipeline wiring, profile
notifications/biometric-login toggles on the Ver Perfil screen reflecting
server truth, order-tracking opt-in fallback prompt, profile-cache refresh
on every mutation, a Notification Service Extension for rich/image push,
the Push Notifications capability, targeting-attributes sync, campaign open
tracking, and tap-to-order deep linking with foreground notification display.
This commit is contained in:
Daniel Arantes Loverde
2026-08-04 11:33:56 -03:00
parent 32f12d6c0e
commit 7dd4cda1a4
20 changed files with 943 additions and 12 deletions

View File

@@ -15,6 +15,7 @@ struct ContentView: View {
@State private var sessionExpiredObserver: NSObjectProtocol?
@State var cartResetObserver: Any?
@State var appResumeObserver: Any?
@State var pushOrderTapObserver: Any?
@StateObject var snackbarCenter = SnackbarCenter.shared
var body: some View {
@@ -90,11 +91,13 @@ struct ContentView: View {
attachCartResetObserverIfNeeded()
attachAppResumeObserverIfNeeded()
attachSessionExpiredObserverIfNeeded()
attachPushOrderTapObserverIfNeeded()
}
.onDisappear {
detachCartResetObserver()
detachAppResumeObserver()
detachSessionExpiredObserver()
detachPushOrderTapObserver()
}
}
@@ -241,6 +244,38 @@ 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,
object: nil,
queue: nil
) { notification in
let orderId = notification.userInfo?["orderId"] as? String
let shortId = notification.userInfo?["shortId"] as? String
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
}
}
}
private func detachPushOrderTapObserver() {
guard let pushOrderTapObserver else { return }
NotificationCenter.default.removeObserver(pushOrderTapObserver)
self.pushOrderTapObserver = nil
}
@MainActor
private func hydrateAppState(with customer: CustomerProfile) {
appState.profile.id = customer.id