Skip to main content

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 | Approved Reviewers: {{REVIEWERS}}Alem Bašić (CEO)

Document History

Version Date Author Changes
0.1 {{DATE}}2026-02-23 {{AUTHOR}}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 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: {{OneSignalExpo |Push Notification Service (EPN) — managed by Expo, handles APNs and FCM routing. No separate OneSignal/Firebase Cloudsetup Messagingneeded (unified)with |Expo AWSmanaged SNS | Custom}}workflow.


2. Provider Setup

2.1 APNs (iOS)

Property Value
Auth method {{APNs Auth Key (.p8) — preferredmanaged overby cert}}
Key ID{{KEY_ID — from Apple Developer Portal}}
Team ID{{TEAM_ID}}Expo
Bundle ID {{com.company.app}}no.getdrop.app
Environment Dev: Sandbox / Prod: Production
Key locationmanagement {{VaultExpo referencemanaged credentialsneverstored commit}}in EAS

Capabilities required in Xcode:Expo app.config.ts:

  •  Push Notifications
  •  Background Modes → Remote notifications
  • {{[x] Background Modes → Background fetch}} (if using background sync)

2.2 FCM (Android)

ViaExpomanaged
Property Value
Project IDIntegration {{firebase-project-id}}
Server key{{Vault reference}}
Sender ID{{SENDER_ID}}workflow
google-services.json Managed by EAS — not committed to repo
Android permissionandroid/app/google-services.jsonPOST_NOTIFICATIONS (gitignoredAndroid 13+ / API 33+)CIExpo injected)handles

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
order.confirmedtransaction.remittance_sent OrderRemittance placedpayment initiated High Default +1 "Penger sendt"
payment.receivedtransaction.remittance_delivered PaymentRemittance processeddelivered to recipient High Default +1
"Penger
message.receivedNew chat messageHighCustom+1levert"
account.securitytransaction.qr_payment PasswordQR changed,payment new logincompleted CriticalHigh Default +1 "Betaling fullført"
{{TYPE}}transaction.failed Payment failedHighDefault+1"Betaling mislyktes"
{{TRIGGER}}account.balance_update {{High/Normal/Low}}Balance change detected via AISP {{Default/Custom/None}}Normal {{+1/None}}NoneNone"Saldo oppdatert"

3.2 Marketing / EngagementSecurity Notifications (Always On — Cannot Disable)

TypeTriggerPriorityNorwegian Title Example
security.new_loginNew device loginCritical"Ny pålogging oppdaget"
security.bankid_consentBankID consent grantedHigh"BankID-tillatelse gitt"
security.suspicious_activityUnusual transaction patternCritical"Uvanlig aktivitet oppdaget"

3.3 Informational Notifications (User-Toggleable)

Type Description Frequency Cap Opt-out Channel
promo.offerinfo.rate_update DiscountFavorable orexchange limited-timerate offeralert Max 2/week1/day MarketingInformational channel
feature.announceinfo.feature_update New Drop feature announcement Max 1/month Marketing channel
re_engagementinfo.new_corridor Win-backNew inactiveremittance userscountry added Max 1/weekmonth 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

TypeShown by OS?CustomizableUse when
Notification payloadYes (auto)LimitedSimple alerts
Data payloadNoFull controlApp handles display
CombinedYes + dataFullStandard approach

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 mustin beNorwegian localizedBokmål (Phase 1 — Norwegian market only)
  • Approach: {{Backend sendsgenerates localized text based on user's localeuser.language preference | App localizes using data payload keys}}
  • Fallback locale: nb
  • Phase 2: en, bs, sq when language settings are implemented

5. Deep Linking Strategy

5.1 URL Scheme Configuration

URL scheme: {{appname:drop://}} Expo Linking config: app.config.ts → scheme: "drop" Universal links (iOS): {{https://app.domain.com}}getdrop.no (Phase 2 — requires AASA file) App links (Android): {{https://app.domain.com}}getdrop.no (Phase 2 — requires assetlinks.json)

Notification Type Deep Link Screen
order.confirmedtransaction.remittance_sent appname:drop://orders/{{orderId}} (tabs home) Order DetailDashboard
message.receivedtransaction.remittance_delivered appname:drop://chats/{{chatId}} Chat ScreenDashboard
payment.receivedtransaction.qr_payment appname:drop://wallet Wallet ScreenDashboard
promo.offertransaction.failed appname:drop://offers/{{offerId}}send OfferSend DetailMoney
security.new_logindrop://profileProfile / Security
security.suspicious_activitydrop://profileProfile / Security
info.rate_updatedrop://sendSend 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: {{After onboarding, on first meaningfulsuccessful actiontransactionNOTnot 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 Relevanttransaction Action (e.g., order placed)completed
    ↓
Pre-prompt Modalmodal (custom UIDrop — explain value)UI)
    "Get 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 limitLimit
Transactional Unlimited Always delivered
Messages1001 hourBatch: "X new messages"
Marketing27 daysDrop excess, no queue
Security Unlimited Always delivered
Rate alerts124 hoursDrop excess
Marketing130 daysDrop excess

Backend enforcement: Rate limit checked before sending to pushExpo Push service. Excess notifications are logged but not sent.


9. Analytics & Tracking

Notification
Event Tracked How Data Points
SentToken registeredBackend loguserId (hashed), platform, timestamp
Notification sent Backend log notificationType, userId,userId (hashed), timestamp
Delivered Push service receiptnotificationType, userId, deliveredAt
Openedopened App handler notificationType, userId, openedAt,deepLinkPath, timeToOpen
DismissedPermission granted {{PlatformApp support}}event {{Androidplatform, only — iOS limited}}timestamp
ActionPermission tapdenied App handlerevent notificationType,platform, actionId, userIdtimestamp

FunnelGDPR metrics:

  • Delivery rate = delivered / sent
  • Open rate = opened / delivered
  • CTR (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 servicepayload builderDev
Token registrationIntegration test with mock API Dev
Delivery smoke test Send test notification via dashboardExpo push tool Staging
Deep link routing E2EManual: test:receive notification → tap notification → verify screen StagingPhysical device
Opt-in flow E2E test:Manual: full permission request flow Staging /Physical device
RateAndroid limitingchannels Integration test: exceed limit →Manual: verify dropchannels in Android settings StagingPhysical device

Test push tool: {{OneSignalExpo DashboardPush |Notification Tool — send test notifications to specific Expo push notification tool | Firebase console}}tokens.

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