Push Notification Design
Push Notification Design
Project:
{{PROJECT_NAME}}Drop — Fintech Payment App Version:{{VERSION}}0.1.0 Date:{{DATE}}2026-02-23 Author:{{AUTHOR}}John (AI Director, ALAI) Status: Draft| In Review | ApprovedReviewers:{{REVIEWERS}}Alem Bašić (CEO)
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 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 NotifServiceEPN as NotificationExpo Service\n(OneSignalPush\nNotification / Firebase)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->>NotifService:EPN: POST /send-push-notifications\n{userId,to: type,expoPushToken, title, body, data}
NotifService-EPN->>APNs: Push payload (iOS users)
NotifService-EPN->>FCM: Push payload (Android users)
APNs-->>Device: Deliver notification (iOS)
FCM-->>Device: Deliver notification (Android)
Device->>Backend: Delivery receipt (optional)
Device->>Backend: Open/clickOpen event (analytics)deep link handling)
Push service: (EPN) — managed by Expo, handles APNs and FCM routing. No separate OneSignal/Firebase {{OneSignalExpo |Push Notification ServiceCloudsetup Messagingneeded (unified)with |Expo AWSmanaged SNS | Custom}}workflow.
2. Provider Setup
2.1 APNs (iOS)
| Property | Value |
|---|---|
| Auth method | |
| |
Expo |
|
| Bundle ID | |
| Environment | Dev: Sandbox / Prod: Production |
| Key |
in EAS |
Capabilities required in Xcode:Expo app.config.ts:
Push NotificationsBackground Modes → Remote notifications{{[x] Background Modes → Background fetch}}(if using background sync)
2.2 FCM (Android)
| Property | Value |
|---|---|
| Via |
| |
workflow |
|
| google-services.json | Managed by EAS — not committed to repo |
| Android permission | ( |
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/services/drop-mobile/lib/notifications.ts — abstractionPhase layer2
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.datadata, 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 |
|---|---|---|---|---|---|
|
High | Default | +1 | "Penger sendt" | |
|
High | Default | +1 | "Penger | |
| |||||
|
Default | +1 | "Betaling fullført" | ||
|
Payment failed | High | Default | +1 | "Betaling mislyktes" |
|
Balance change detected via AISP |
Normal |
None |
None | "Saldo oppdatert" |
3.2 Marketing / EngagementSecurity 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 |
|---|---|---|---|
|
Max |
||
|
New Drop feature announcement | Max 1/month | Marketing channel |
|
Max 1/ |
Marketing channel |
3.34 Android Notification Channels
// Create channelsCreated on app startup (for Android 8+ / (API 26+)
awaitconst Notifications.setNotificationChannelAsync(channels = [
{
id: 'transactional',
{
name: 'OrdersBetalinger &og Payments'overføringer',
importance: Notifications.AndroidImportance.HIGH,
vibrationPattern: [0, 250, 250, 250],
lightColor:description: '#FF231F7C'Varsler om dine betalinger og pengeoverføringer',
sound: 'default',
vibration: true,
});
await Notifications.setNotificationChannelAsync('messages',
{
id: 'security',
name: 'Messages'Sikkerhet',
importance: Notifications.AndroidImportance.HIGH,
description: 'Viktige sikkerhetsvarsler — kan ikke deaktiveres',
sound: 'message.wav'default',
vibration: true,
});,
await{
Notifications.setNotificationChannelAsync(id: 'marketing',
{
name: 'PromotionsNyheter &og Updates'tilbud',
importance: Notifications.AndroidImportance.DEFAULT,
description: 'Nyheter om Drop og nye funksjoner',
sound: null,
vibration: false,
}),
];
4. Payload Format & Schema
4.1 DataStandard Payload vsDrop Notification Payload
Recommended approach: Send both — notification payload for guaranteed delivery, data payload for app logic.
4.2 Standard Payload Schema
{
"notification"to": {"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
"title": "{{NotificationPenger title}}"sendt",
"body": "{{NotificationDu bodysendte text}}"5 000 NOK til Ahmad Karimi i Serbia",
"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"data": 1{
}"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"
},
"android": {
"channelId": "{{transactional | messages | marketing}}",
"priority": "high"
}transactional"
}
4.32 Localization
- All notification text
mustinbeNorwegianlocalizedBokmål (Phase 1 — Norwegian market only) Approach:{{Backendsendsgenerates localized text based onuser's localeuser.languagepreference| App localizes using data payload keys}}- Fallback locale:
nb - Phase 2:
en,bs,sqwhen language settings are implemented
5. Deep Linking Strategy
5.1 URL Scheme Configuration
URL scheme:
Expo Linking config: {{appname:drop://}}app.config.ts → scheme: "drop"
Universal links (iOS): (Phase 2 — requires AASA file)
App links (Android): {{https://app.domain.com}}getdrop.no (Phase 2 — requires assetlinks.json){{https://app.domain.com}}getdrop.no
5.2 Deep Link Routing
| Notification Type | Deep Link | Screen |
|---|---|---|
|
(tabs home) |
|
|
|
|
|
|
|
|
|
|
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
handlerconst subscription = 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: launch.{{After onboarding, on first meaningfulsuccessful actiontransaction — NOTnot 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:
App Launch
↓
Onboarding Complete
↓
First Relevanttransaction Action (e.g., order placed)completed
↓
Pre-prompt Modalmodal (custom UIDrop — explain value)UI)
"GetFå notifiedvarsel whennår yourpengene orderdine iser ready"levert"
[Allow]"Ja, [Notslå now]på varsler" | "Ikke nå"
↓ (Ja)
iOS/Android OS permission dialog
↓ (Allow)
OSRegister PermissionExpo Dialog
↓
Registerpush token with backend
POST /v1/users/push-token
Soft prompt:prompt rule: Always show custom pre-prompt before OS dialog. Users who dismiss the OS dialog permanently cannot be re-asked on iOS — usethe pre-prompt toqualifies qualifyintent 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; // Orders,Payment paymentsalerts — 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
Sync:Web sync: Preferences stored on server — synced onvia login/api/settings andendpoints (web app foreground.PATCH).
8. Rate Limiting & Throttling
| Category | Limit | Window | Behavior at |
|---|---|---|---|
| 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 pushExpo Push service. Excess notifications are logged but not sent.
9. Analytics & Tracking
| Event | Tracked |
Data Points |
|---|---|---|
| Backend log | userId (hashed), platform, timestamp | |
| Notification sent | Backend log | notificationType, |
| App handler | notificationType, |
|
event |
timestamp |
|
| App |
FunnelGDPR metrics:
Delivery rate = delivered / sentOpen rate = opened / deliveredCTR (click-through) = action taps / opened
Privacy: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 |
Dev |
| Token registration | Integration test with mock API | Dev |
| Delivery smoke test | Send test |
Staging |
| Deep link routing | ||
| Opt-in flow | ||
Test push tool: tokens.{{OneSignalExpo DashboardPush |Notification Tool — send test notifications to specific Expo push notification tool | Firebase console}}
TODO:Testing limitation: CreateExpo MaestroGo testapp flowsdoes not support push notifications — must use development build (eas build --profile development) for push notification tap → deep link scenarios.testing.
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | John (AI Director) | 2026-02-23 | |
| Mobile Lead | |||
| Backend Lead | |||
| Product Owner |