Push Notification Design
Push Notification Design
Project: Drop — Fintech Payment App Version: 0.1.0 Date: 2026-02-23 Author: John (AI Director, ALAI) Status: Draft Reviewers: Alem Bašić (CEO)
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02-23 | John | Initial draft — current state (not implemented) + Phase 2 design |
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 EPN as Expo Push\nNotification Service
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: POST /send-push-notifications\n{to: expoPushToken, title, body, data}
EPN->>APNs: Push payload (iOS users)
EPN->>FCM: Push payload (Android users)
APNs-->>Device: Deliver notification (iOS)
FCM-->>Device: Deliver notification (Android)
Device->>Backend: Open event (deep link handling)
Push service: Expo Push Notification Service (EPN) — managed by Expo, handles APNs and FCM routing. No separate OneSignal/Firebase setup needed with Expo managed workflow.
2. Provider Setup
2.1 APNs (iOS)
| Property | Value |
|---|---|
| Auth method | APNs Auth Key (.p8) — managed by Expo |
| Bundle ID | no.getdrop.app |
| Environment | Dev: Sandbox / Prod: Production |
| Key management | Expo managed credentials — stored in EAS |
Capabilities required in Expo app.config.ts:
- Push Notifications
- Background Modes → Remote notifications
2.2 FCM (Android)
| Property | Value |
|---|---|
| Integration | Via Expo managed workflow |
| google-services.json | Managed by EAS — not committed to repo |
| Android permission | POST_NOTIFICATIONS (Android 13+ / API 33+) — Expo handles |
2.3 Unified Service Configuration
SDK: expo-notifications
// src/drop-mobile/lib/notifications.ts — Phase 2
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';
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.OS });
// 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 | Norwegian Title Example |
|---|---|---|---|---|---|
transaction.remittance_sent |
Remittance payment initiated | High | Default | +1 | "Penger sendt" |
transaction.remittance_delivered |
Remittance delivered to recipient | High | Default | +1 | "Penger levert" |
transaction.qr_payment |
QR payment completed | High | Default | +1 | "Betaling fullført" |
transaction.failed |
Payment failed | High | Default | +1 | "Betaling mislyktes" |
account.balance_update |
Balance change detected via AISP | Normal | None | None | "Saldo oppdatert" |
3.2 Security Notifications (Always On — Cannot Disable)
| Type | Trigger | Priority | Norwegian Title Example |
|---|---|---|---|
security.new_login |
New device login | Critical | "Ny pålogging oppdaget" |
security.bankid_consent |
BankID consent granted | High | "BankID-tillatelse gitt" |
security.suspicious_activity |
Unusual transaction pattern | Critical | "Uvanlig aktivitet oppdaget" |
3.3 Informational Notifications (User-Toggleable)
| Type | Description | Frequency Cap | Opt-out Channel |
|---|---|---|---|
info.rate_update |
Favorable exchange rate alert | Max 1/day | Informational channel |
info.feature_update |
New Drop feature announcement | Max 1/month | Marketing channel |
info.new_corridor |
New remittance country added | Max 1/month | Marketing channel |
3.4 Android Notification Channels
// Created on app startup for Android 8+ (API 26+)
const channels = [
{
id: 'transactional',
name: 'Betalinger og overføringer',
importance: AndroidImportance.HIGH,
description: 'Varsler om dine betalinger og pengeoverføringer',
sound: 'default',
vibration: true,
},
{
id: 'security',
name: 'Sikkerhet',
importance: AndroidImportance.HIGH,
description: 'Viktige sikkerhetsvarsler — kan ikke deaktiveres',
sound: 'default',
vibration: true,
},
{
id: 'marketing',
name: 'Nyheter og tilbud',
importance: AndroidImportance.DEFAULT,
description: 'Nyheter om Drop og nye funksjoner',
sound: null,
vibration: false,
},
];
4. Payload Format & Schema
4.1 Standard Drop Notification Payload
{
"to": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
"title": "Penger sendt",
"body": "Du sendte 5 000 NOK til Ahmad Karimi i Serbia",
"sound": "default",
"badge": 1,
"data": {
"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"
}
4.2 Localization
- All notification text in Norwegian Bokmål (Phase 1 — Norwegian market only)
- Backend generates localized text based on
user.languagepreference - Fallback locale:
nb - Phase 2:
en,bs,sqwhen language settings are implemented
5. Deep Linking Strategy
5.1 URL Scheme Configuration
URL scheme: drop://
Expo Linking config: app.config.ts → scheme: "drop"
Universal links (iOS): https://getdrop.no (Phase 2 — requires AASA file)
App links (Android): https://getdrop.no (Phase 2 — requires assetlinks.json)
5.2 Deep Link Routing
| Notification Type | Deep Link | Screen |
|---|---|---|
transaction.remittance_sent |
drop:// (tabs home) |
Dashboard |
transaction.remittance_delivered |
drop:// |
Dashboard |
transaction.qr_payment |
drop:// |
Dashboard |
transaction.failed |
drop://send |
Send Money |
security.new_login |
drop://profile |
Profile / Security |
security.suspicious_activity |
drop://profile |
Profile / Security |
info.rate_update |
drop://send |
Send Money |
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 = Notifications.addNotificationResponseReceivedListener((response) => {
const { deepLinkPath } = response.notification.request.content.data ?? {};
if (deepLinkPath) {
router.push(deepLinkPath);
}
});
// App killed — check initial notification
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 first successful transaction — not on app 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:
First transaction completed
↓
Pre-prompt modal (custom Drop UI)
"Få varsel når pengene dine er levert"
"Ja, slå på varsler" | "Ikke nå"
↓ (Ja)
iOS/Android OS permission dialog
↓ (Allow)
Register Expo push token with backend
POST /v1/users/push-token
Soft prompt rule: Always show custom pre-prompt before OS dialog. Users who dismiss the OS dialog permanently cannot be re-asked on iOS — the pre-prompt qualifies intent first.
In-app settings: /profile/notifications page (web) — toggle push/email notifications.
Mobile settings: Via profile.js → Settings menu → Varsler (Phase 2).
7. Notification Preferences Per User
interface NotificationPreferences {
pushEnabled: boolean; // Master toggle
pushToken: string | null; // Expo push token
channels: {
transactional: boolean; // Payment alerts — default: true
security: boolean; // Security alerts — ALWAYS 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: Preferences synced via /api/settings endpoints (web app PATCH).
8. Rate Limiting & Throttling
| Category | Limit | Window | Behavior at Limit |
|---|---|---|---|
| Transactional | Unlimited | — | Always delivered |
| Security | Unlimited | — | Always delivered |
| Rate alerts | 1 | 24 hours | Drop excess |
| Marketing | 1 | 30 days | Drop excess |
Backend enforcement: Rate limit checked before sending to Expo Push service. Excess notifications logged but not sent.
9. Analytics & Tracking
| Event | Tracked | Data Points |
|---|---|---|
| Token registered | Backend log | userId (hashed), platform, timestamp |
| Notification sent | Backend log | notificationType, userId (hashed), timestamp |
| Notification opened | App handler | notificationType, deepLinkPath, timeToOpen |
| Permission granted | App event | platform, timestamp |
| Permission denied | App event | platform, timestamp |
GDPR note: 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 payload builder | Dev |
| Token registration | Integration test with mock API | Dev |
| Delivery smoke test | Send test via Expo push tool | Staging |
| Deep link routing | Manual: receive notification → tap → verify screen | Physical device |
| Opt-in flow | Manual: full permission request flow | Physical device |
| Android channels | Manual: verify channels in Android settings | Physical device |
Test push tool: Expo Push Notification Tool — send test notifications to specific Expo push tokens.
Testing limitation: Expo Go app does not support push notifications — must use development build (eas build --profile development) for push notification testing.
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | John (AI Director) | 2026-02-23 | |
| Mobile Lead | |||
| Backend Lead | |||
| Product Owner |