Push Notification Design Push Notification Design Project: {{PROJECT_NAME}} Version: {{VERSION}} Date: {{DATE}} Author: {{AUTHOR}} Status: Draft | In Review | Approved Reviewers: {{REVIEWERS}} Document History Version Date Author Changes 0.1 {{DATE}} {{AUTHOR}} Initial draft 1. Architecture Overview sequenceDiagram participant Backend participant NotifService as Notification Service\n(OneSignal / Firebase) participant APNs as APNs (iOS) participant FCM as FCM (Android) participant Device Backend->>NotifService: POST /notifications\n{userId, type, data} NotifService->>APNs: Push payload (iOS users) NotifService->>FCM: Push payload (Android users) APNs-->>Device: Deliver notification (iOS) FCM-->>Device: Deliver notification (Android) Device->>Backend: Delivery receipt (optional) Device->>Backend: Open/click event (analytics) Push service: {{OneSignal | Firebase Cloud Messaging (unified) | AWS SNS | Custom}} 2. Provider Setup 2.1 APNs (iOS) Property Value Auth method {{APNs Auth Key (.p8) — preferred over cert}} Key ID {{KEY_ID — from Apple Developer Portal}} Team ID {{TEAM_ID}} Bundle ID {{com.company.app}} Environment Dev: Sandbox Key location {{Vault reference — never commit}} Capabilities required in Xcode: Push Notifications Background Modes → Remote notifications {{[x] Background Modes → Background fetch}} (if using background sync) 2.2 FCM (Android) Property Value Project ID {{firebase-project-id}} Server key {{Vault reference}} Sender ID {{SENDER_ID}} google-services.json android/app/google-services.json (gitignored — CI injected) Android Manifest additions: 2.3 Unified Service Configuration SDK: {{expo-notifications | react-native-firebase | onesignal-react-native}} // src/services/notifications.ts — abstraction layer export async function registerForPushNotifications(): Promise { const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; if (existingStatus !== 'granted') { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } if (finalStatus !== 'granted') return null; const token = await Notifications.getExpoPushTokenAsync({ projectId: Constants.expoConfig?.extra?.eas?.projectId, }); // Register token with backend await api.post('/users/push-token', { token: token.data }); return token.data; } 3. Notification Types & Channels 3.1 Transactional Notifications Type Trigger Priority Sound Badge order.confirmed Order placed High Default +1 payment.received Payment processed High Default +1 message.received New chat message High Custom +1 account.security Password changed, new login Critical Default +1 {{TYPE}} {{TRIGGER}} {{High/Normal/Low}} {{Default/Custom/None}} {{+1/None}} 3.2 Marketing / Engagement Notifications Type Description Frequency Cap Opt-out Channel promo.offer Discount or limited-time offer Max 2/week Marketing channel feature.announce New feature announcement Max 1/month Marketing channel re_engagement Win-back inactive users Max 1/week Marketing channel 3.3 Android Notification Channels // Create channels on app startup (Android 8+ / API 26+) await Notifications.setNotificationChannelAsync('transactional', { name: 'Orders & Payments', importance: Notifications.AndroidImportance.HIGH, vibrationPattern: [0, 250, 250, 250], lightColor: '#FF231F7C', sound: 'default', }); await Notifications.setNotificationChannelAsync('messages', { name: 'Messages', importance: Notifications.AndroidImportance.HIGH, sound: 'message.wav', }); await Notifications.setNotificationChannelAsync('marketing', { name: 'Promotions & Updates', importance: Notifications.AndroidImportance.DEFAULT, sound: null, }); 4. Payload Format & Schema 4.1 Data Payload vs Notification Payload Type Shown by OS? Customizable Use when Notification payload Yes (auto) Limited Simple alerts Data payload No Full control App handles display Combined Yes + data Full Standard approach Recommended approach: Send both — notification payload for guaranteed delivery, data payload for app logic. 4.2 Standard Payload Schema { "notification": { "title": "{{Notification title}}", "body": "{{Notification body text}}", "image": "{{optional image URL}}" }, "data": { "notificationType": "{{order.confirmed | message.received | etc.}}", "entityType": "{{order | message | user | etc.}}", "entityId": "{{UUID}}", "deepLinkPath": "{{/orders/123}}", "timestamp": "{{ISO_8601}}", "version": "1" }, "apns": { "payload": { "aps": { "sound": "default", "badge": 1, "content-available": 1 } } }, "android": { "channelId": "{{transactional | messages | marketing}}", "priority": "high" } } 4.3 Localization All notification text must be localized Approach: {{Backend sends localized text based on user's locale preference | App localizes using data payload keys}} Fallback locale: en 5. Deep Linking Strategy 5.1 URL Scheme Configuration URL scheme: {{appname://}} Universal links (iOS): {{https://app.domain.com}} App links (Android): {{https://app.domain.com}} 5.2 Deep Link Routing Notification Type Deep Link Screen order.confirmed appname://orders/{{orderId}} Order Detail message.received appname://chats/{{chatId}} Chat Screen payment.received appname://wallet Wallet Screen promo.offer appname://offers/{{offerId}} Offer Detail 5.3 Navigation on Notification Tap // App foreground — notification tap handler Notifications.addNotificationResponseReceivedListener((response) => { const { notificationType, deepLinkPath } = response.notification.request.content.data; if (deepLinkPath) { // Use router.push for Expo Router, or navigation.navigate for React Navigation router.push(deepLinkPath); } }); // App killed — check initial notification useEffect(() => { Notifications.getLastNotificationResponseAsync().then((response) => { if (response?.notification.request.content.data?.deepLinkPath) { router.replace(response.notification.request.content.data.deepLinkPath); } }); }, []); 6. Opt-In / Opt-Out Flow Permission request timing: {{After onboarding, on first meaningful action — NOT on app launch}} Permission flow: App Launch ↓ Onboarding Complete ↓ First Relevant Action (e.g., order placed) ↓ Pre-prompt Modal (custom UI — explain value) "Get notified when your order is ready" [Allow] [Not now] ↓ (Allow) OS Permission Dialog ↓ Register token with backend Soft prompt: Always show custom pre-prompt before OS dialog. Users who dismiss OS dialog permanently cannot be re-asked — use pre-prompt to qualify intent. 7. Notification Preferences Per User interface NotificationPreferences { pushEnabled: boolean; // Master toggle channels: { transactional: boolean; // Orders, payments — default: true messages: boolean; // Chat messages — default: true marketing: boolean; // Promotions — default: false security: boolean; // Security alerts — always true, non-toggleable }; quietHours: { enabled: boolean; start: string; // "22:00" HH:mm end: string; // "08:00" HH:mm timezone: string; // "Europe/Oslo" }; } API endpoint: PUT /users/notification-preferences Sync: Preferences stored on server — synced on login and app foreground. 8. Rate Limiting & Throttling Category Limit Window Behavior at limit Transactional Unlimited — Always delivered Messages 100 1 hour Batch: "X new messages" Marketing 2 7 days Drop excess, no queue Security Unlimited — Always delivered Backend enforcement: Rate limit checked before sending to push service. Excess notifications are logged but not sent. 9. Analytics & Tracking Event Tracked How Data Points Sent Backend log notificationType, userId, timestamp Delivered Push service receipt notificationType, userId, deliveredAt Opened App handler notificationType, userId, openedAt, timeToOpen Dismissed {{Platform support}} {{Android only — iOS limited}} Action tap App handler notificationType, actionId, userId Funnel metrics: Delivery rate = delivered / sent Open rate = opened / delivered CTR (click-through) = action taps / opened Privacy: User IDs are hashed in analytics. No PII in event data. 10. Testing Strategy Test Type Method Environment Payload validation Unit test push service Dev Delivery smoke test Send test notification via dashboard Staging Deep link routing E2E test: tap notification → verify screen Staging Opt-in flow E2E test: full permission flow Staging / device Rate limiting Integration test: exceed limit → verify drop Staging Test push tool: {{OneSignal Dashboard | Expo push notification tool | Firebase console}} TODO: Create Maestro test flows for notification tap → deep link scenarios. Approval Role Name Date Signature Author Mobile Lead Backend Lead Product Owner