Skip to main content

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.language preference
  • Fallback locale: nb
  • Phase 2: en, bs, sq when language settings are implemented

5. Deep Linking Strategy

5.1 URL Scheme Configuration

URL scheme: drop:// Expo Linking config: app.config.tsscheme: "drop" Universal links (iOS): https://getdrop.no (Phase 2 — requires AASA file) App links (Android): https://getdrop.no (Phase 2 — requires assetlinks.json)

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