Push Notification Design
Push Notification Design
Project:
Drop — Fintech Payment App{{PROJECT_NAME}} Version:0.1.0{{VERSION}} Date:2026-02-23{{DATE}} Author:John (AI Director, ALAI){{AUTHOR}} Status: Draft | In Review | Approved Reviewers:Alem Bašić (CEO){{REVIEWERS}}
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | Initial draft |
1. Architecture Overview
Current state (Phase 1): Push notifications are NOT yet implemented in the mobile app. The /profile/notifications page on the web app supports toggling push/email notifications in user settings (PATCH /api/settings), and the notifications feature flag is true. The backend notification infrastructure is TBD.
Phase 2 target architecture:
sequenceDiagram
participant Backend
as Drop Backend\n(Hono v4)
participant EPNNotifService as ExpoNotification Push\nNotificationService\n(OneSignal Service/ Firebase)
participant APNs as APNs (iOS)
participant FCM as FCM (Android)
participant Device
as User Device\n(Drop Mobile App)
Note over Backend,Device: Transaction event triggers notification
Backend->>EPN:NotifService: POST /send-push-notifications\n{to:userId, expoPushToken, title, body,type, data}
EPN-NotifService->>APNs: Push payload (iOS users)
EPN-NotifService->>FCM: Push payload (Android users)
APNs-->>Device: Deliver notification (iOS)
FCM-->>Device: Deliver notification (Android)
Device->>Backend: OpenDelivery receipt (optional)
Device->>Backend: Open/click event (deep link handling)analytics)
Push service: Cloud Messaging (Expo{{OneSignal Push| NotificationFirebase ServiceEPN)unified) —| managedAWS bySNS Expo,| handles APNs and FCM routing. No separate OneSignal/Firebase setup needed with Expo managed workflow.Custom}}
2. Provider Setup
2.1 APNs (iOS)
| Property | Value |
|---|---|
| Auth method | {{APNs Auth Key (.p8) — |
| Key ID | {{KEY_ID — from Apple Developer Portal}} |
| Team ID | {{TEAM_ID}} |
| Bundle ID | |
| Environment | Dev: Sandbox |
| Key |
{{Vault |
Capabilities required in Expo app.config.ts:Xcode:
- Push Notifications
- Background Modes → Remote notifications
{{[x] Background Modes → Background fetch}}(if using background sync)
2.2 FCM (Android)
| Property | Value |
|---|---|
{{firebase-project-id}} |
|
| Server key | {{Vault reference}} |
| Sender ID | {{SENDER_ID}} |
| google-services.json | android/app/google-services.json |
|
Android Manifest additions:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- For Android 13+ — request at runtime -->
2.3 Unified Service Configuration
SDK: {{expo-notifications | react-native-firebase | onesignal-react-native}}
// src/drop-mobile/lib/services/notifications.ts — Phaseabstraction 2
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';layer
export async function registerForPushNotifications(): Promise<string | null> {
// Check existing permissions
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
// Request if not yet granted
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('Push notification permission denied');
return null;
}
// Get Expo push token
const token = await Notifications.getExpoPushTokenAsync({
projectId: Constants.expoConfig?.extra?.eas?.projectId,
});
// Register token with Drop backend
await api.post('/users/push-token', { token: token.data, platform: Platform.OSdata });
// Configure Android notification handler
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('transactional', {
name: 'Betalinger og overføringer',
importance: Notifications.AndroidImportance.HIGH,
vibrationPattern: [0, 250, 250, 250],
sound: 'default',
});
await Notifications.setNotificationChannelAsync('security', {
name: 'Sikkerhet',
importance: Notifications.AndroidImportance.HIGH,
});
await Notifications.setNotificationChannelAsync('marketing', {
name: 'Nyheter og tilbud',
importance: Notifications.AndroidImportance.DEFAULT,
});
}
return token.data;
}
3. Notification Types & Channels
3.1 Transactional Notifications (Drop Core)
| Type | Trigger | Priority | Sound | Badge | |
|---|---|---|---|---|---|
|
High | Default | +1 | ||
|
High | Default | +1 | ||
message.received |
New chat message | High | Custom | +1 | |
|
Default | +1 | |||
|
|||||
|
{{High/Normal/Low}} |
{{Default/Custom/None}} |
{{+1/None}} |
3.2 SecurityMarketing / Engagement Notifications (Always On — Cannot Disable)
| |||
| |||
|
3.3 Informational Notifications (User-Toggleable)
| Type | Description | Frequency Cap | Opt-out Channel |
|---|---|---|---|
|
Max |
||
|
New |
Max 1/month | Marketing channel |
|
Max 1/ |
Marketing channel |
3.43 Android Notification Channels
// CreatedCreate channels on app startup for (Android 8+ (/ API 26+)
constawait channels = [
{
id: Notifications.setNotificationChannelAsync('transactional', {
name: 'BetalingerOrders og& overføringer'Payments',
importance: Notifications.AndroidImportance.HIGH,
description:vibrationPattern: [0, 250, 250, 250],
lightColor: 'Varsler om dine betalinger og pengeoverføringer'#FF231F7C',
sound: 'default',
vibration:});
true,await }Notifications.setNotificationChannelAsync('messages', {
id:name: 'security'Messages',
importance: Notifications.AndroidImportance.HIGH,
sound: 'message.wav',
});
await Notifications.setNotificationChannelAsync('marketing', {
name: 'Sikkerhet'Promotions & Updates',
importance: AndroidImportance.HIGH,
description: 'Viktige sikkerhetsvarsler — kan ikke deaktiveres',
sound: 'default',
vibration: true,
},
{
id: 'marketing',
name: 'Nyheter og tilbud',
importance: Notifications.AndroidImportance.DEFAULT,
description: 'Nyheter om Drop og nye funksjoner',
sound: null,
vibration: false,
},
]);
4. Payload Format & Schema
4.1 StandardData DropPayload 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
{
"to"notification": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",{
"title": "Penger{{Notification sendt"title}}",
"body": "Du{{Notification sendtebody 5text}}",
000"image": NOK"{{optional tilimage AhmadURL}}"
Karimi},
i"data": Serbia"{
"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,
"data"content-available": 1
}
}
},
"android": {
"notificationType": "transaction.remittance_sent",
"entityType": "transaction",
"entityId": "txn_abc123def456",
"deepLinkPath": "/(tabs)/",
"amount": "5000",
"currency": "NOK",
"recipientName": "Ahmad Karimi",
"targetCurrency": "RSD",
"targetAmount": "58525",
"timestamp": "2026-02-23T14:30:00Z",
"version": "1"
},
"channelId": "transactional"{{transactional | messages | marketing}}",
"priority": "high"
}
}
4.23 Localization
- All notification text
inmustNorwegianbeBokmål (Phase 1 — Norwegian market only)localized - Approach:
{{Backendgeneratessends localized text based onuser's locale preference | App localizes using data payload keys}}user.language - Fallback locale:
nb Phase 2:en,bs,sqwhen language settings are implemented
5. Deep Linking Strategy
5.1 URL Scheme Configuration
URL scheme: drop:{{appname://
Expo Linking config: app.config.ts →
Universal links (iOS): scheme: "drop"}}{{https://getdrop.noapp.domain.com}} (Phase 2 — requires AASA file)
App links (Android): {{https://getdrop.noapp.domain.com}} (Phase 2 — requires assetlinks.json)
5.2 Deep Link Routing
| Notification Type | Deep Link | Screen |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
| | |
| | |
| |
5.3 Navigation on Notification Tap
// Phase 2 — notification response handler
import { useRouter } from 'expo-router';
export function useNotificationNavigation() {
const router = useRouter();
useEffect(() => {
// App in foreground — notification tap const subscription =handler
Notifications.addNotificationResponseReceivedListener((response) => {
const { notificationType, deepLinkPath } = response.notification.request.content.data ?? {};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);
}
});
return () => subscription.remove();
}, []);
}
6. Opt-In / Opt-Out Flow
Permission request timing: {{After onboarding, on first successfulmeaningful transactionaction — notNOT on app launch.launch}}
Norwegian pre-prompt text:
"Vil du få varsler når pengene dine er sendt eller mottatt?" [Ja, slå på varsler] [Ikke nå]
Permission flow:
App Launch
↓
Onboarding Complete
↓
First transactionRelevant completedAction (e.g., order placed)
↓
Pre-prompt modalModal (custom DropUI UI)— explain value)
"FåGet varselnotified nårwhen pengeneyour dineorder eris levert"ready"
"Ja,[Allow] slå[Not på varsler" | "Ikke nå"
↓ (Ja)
iOS/Android OS permission dialognow]
↓ (Allow)
OS Permission Dialog
↓
Register Expo push token with backend
POST /v1/users/push-token
Soft prompt rule:prompt: Always show custom pre-prompt before OS dialog. Users who dismiss the OS dialog permanently cannot be re-asked on iOS — theuse pre-prompt qualifiesto intentqualify first.
In-app settings: /profile/notifications page (web) — toggle push/email notifications.
Mobile settings: Via profile.js → Settings menu → Varsler (Phase 2).intent.
7. Notification Preferences Per User
interface NotificationPreferences {
pushEnabled: boolean; // Master toggle
pushToken: string | null; // Expo push token
channels: {
transactional: boolean; // PaymentOrders, alertspayments — default: true
messages: boolean; // Chat messages — default: true
marketing: boolean; // Promotions — default: false
security: boolean; // Security alerts — ALWAYSalways true, non-toggleable
marketing: boolean; // News and offers — default: false
};
quietHours: {
enabled: boolean; // default: false
start: string; // "22:00" HH:mm (Europe/Oslo)
end: string; // "08:00" HH:mm
timezone: string; // "Europe/Oslo"
};
}
API endpoint: PUT /v1/users/notification-preferences
Web sync:Sync: Preferences stored on server — synced viaon login /api/settingsendpoints (weband app PATCH).foreground.
8. Rate Limiting & Throttling
| Category | Limit | Window | Behavior at |
|---|---|---|---|
| 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 Expo Pushpush service. Excess notifications are logged but not sent.
9. Analytics & Tracking
| Event | Tracked How | Data Points |
|---|---|---|
| Backend log | notificationType, |
|
| Push service receipt | notificationType, userId, deliveredAt | |
| Opened | App handler | notificationType, |
{{Platform |
{{Android |
|
| App |
GDPRFunnel note: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. Push tokens are treated as personal data per GDPR.
10. Testing Strategy
| Test Type | Method | Environment |
|---|---|---|
| Payload validation | Unit test push | |
| Dev | ||
| Delivery smoke test | Send test notification via |
Staging |
| Deep link routing | ||
| Opt-in flow | ||
Test push tool: Expo{{OneSignal PushDashboard Notification Tool — send test notifications to specific| Expo push tokens.notification tool | Firebase console}}
Testing limitation:TODO: ExpoCreate GoMaestro apptest does not support push notifications — must use development build (eas build --profile development)flows for push notification testing.tap → deep link scenarios.
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | |||
| Mobile Lead | |||
| Backend Lead | |||
| Product Owner |