# Notifications — Borrower App FE Guide

What the backend sends, how the app receives it (push + in-app inbox), and where each notification should take the user.

- Base URL: `https://api.nextpaydayapp.com/api/v1`
- Auth headers: `Authorization: Bearer {token}`, `Accept: application/json`

Supersedes the FE parts of [fcm-notifications.md](fcm-notifications.md). Backend internals: [notifications-flow.md](notifications-flow.md).

---

## 1. How it works

Every notification is one **event** (e.g. `loan.disbursed`). When it fires, the server sends it on that event's channels:

| Channel | What the user sees | App work needed |
|---|---|---|
| **push** (FCM) | Phone banner / lock screen | Register the FCM token (§2) and handle taps (§4) |
| **in_app** | A row in the notifications screen | Call `GET /profile/notifications` (§3) |
| **email** | Email | None |
| **sms** | Text message | None |

The app only needs **push** and **in-app**. Email and SMS are server-only.

---

## 2. Register the device for push

### On login: send the token in the body

```
POST /auth/user/login
{ "email": "jane@example.com", "password": "••••••", "login_type": "borrower", "fcm_token": "<firebase token>" }
```

### On signup

```
POST /auth/create-password
{ "signupToken": "...", "password": "...", "fcm_token": "<firebase token>" }
```

### When Firebase rotates the token, and on every app start while logged in

```
PUT /profile/fcm-token
{ "fcm_token": "<firebase token>", "platform": "android" }
```
`platform` is optional: `android`, `ios` or `web`.

→ `200 { "message": "FCM token updated successfully." }`

### On logout: send this device's token

```
POST /auth/logout
{ "fcm_token": "<this device's firebase token>" }
```
Only this device stops getting pushes. The user's other logged-in phones keep receiving them. If you leave out `fcm_token`, the server removes the most recently registered device.

> **Every device gets push.** A user logged in on two phones receives the push on both. If Firebase reports a token as invalid (app uninstalled, etc.), the server deletes it automatically.

---

## 3. Notifications screen (in-app inbox)

```
GET /profile/notifications?page=1
```

`200` (20 per page, newest first):

```json
{
  "data": [
    {
      "id": "9b1f0c1e-3c2a-4a55-9a53-1f3c8f3f2a10",
      "title": "Repayment reminder — 3 days",
      "description": "Hi Jane, your repayment of ₦11,000.00 is due in 3 days (Oct 17, 2026).",
      "event": "loan.repayment_reminder_3d",
      "data": {
        "type": "loan.repayment_reminder_3d",
        "event": "loan.repayment_reminder_3d",
        "loan_reference": "LN-LIFVZBSIWR",
        "loan_id": "42",
        "amount": "11000.00",
        "due_date": "Oct 17, 2026",
        "schedule_id": "102"
      },
      "read": false,
      "date": "2026-10-14 09:00:03"
    }
  ],
  "current_page": 1,
  "last_page": 3,
  "unread_count": 4
}
```

`unread_count` is the total unread across all pages.

- Show `title`, `description` and `date`, and style rows with `read: false` as unread (bold or dot).
- Tapping a row: call **mark as read** (below), then route using §5 with the top-level `event` and `data.loan_reference`.
- Infinite scroll: load `page + 1` while `current_page < last_page`.
- `event` can be `null` for a few older system messages (e.g. "Partial Loan Payment Collected"). Those just open the inbox, with no deep link.

### Bell badge: unread count

```
GET /profile/notifications/unread-count
```
→ `200 { "unread_count": 4 }`

Call it on app start, when the home screen appears, and whenever a push arrives in the foreground.

### Mark one as read

```
POST /profile/notifications/{id}/read
```
→ `200 { "message": "Notification marked as read.", "unread_count": 3 }`

Safe to call again. Returns `404` if the id isn't this user's. Update the badge from `unread_count`.

A push tap doesn't carry the inbox `id`, so for push taps just refresh the unread count after navigating.

### Mark all as read

```
POST /profile/notifications/read-all
```
→ `200 { "message": "All notifications marked as read.", "marked": 4, "unread_count": 0 }`

Affiliate app: the same four endpoints exist under `/affiliate/notifications`.

---

## 4. Push payload (FCM)

Each push has a `notification` block (`title` and `body`, the same text as the inbox) and a `data` map. All values are strings:

```json
{
  "notification": { "title": "Loan disbursed", "body": "Hi Jane, ₦50,000.00 from loan LN-LIFVZBSIWR has been transferred to your bank account." },
  "data": {
    "type": "loan.disbursed",
    "event": "loan.disbursed",
    "loan_reference": "LN-LIFVZBSIWR",
    "loan_id": "42",
    "amount": "50000"
  }
}
```

The push `data` map only ever contains `type`, `event`, `loan_reference`, `loan_id`, `schedule_id`, `reference`, `amount` and `audience`, and only the ones that apply. The inbox row has the full context (e.g. `due_date`, `reason`).

**Route on `data.event`, not `data.type`.** They're equal for almost every event, but admin announcements send `type: "admin.broadcast"` in the push and `type: "admin_broadcast"` in the inbox, while `event` is `admin.broadcast` in both.

Handle all three app states:

| App state | Firebase callback | Do |
|---|---|---|
| Foreground | `FirebaseMessaging.onMessage` | Show an in-app banner/snackbar, refresh the unread count, and refresh whatever the event affects (§5) |
| Background, user taps | `FirebaseMessaging.onMessageOpenedApp` | Navigate with §5 |
| Terminated, user taps | `FirebaseMessaging.instance.getInitialMessage()` on startup | Navigate with §5 after login is restored |

---

## 5. Events the borrower can receive → where to go

`{ref}` = `data.loan_reference`.

### Loan journey

| `event` | When it fires | Channels | Tap → screen | Also refresh |
|---|---|---|---|---|
| `auth.welcome` | Signup completed | email, in-app, push | Home | — |
| `auth.password_changed` | Password reset done | email, in-app, push | Profile / security | — |
| `bank_statement.processed` | Statement analysis finished | email, in-app, push | Loan offer / eligibility screen | verification steps |
| `bank_statement.failed` | Statement analysis failed (inbox `data.reason`) | email, in-app, push | Bank statement upload screen | verification steps |
| `payment.direct_debit_pending_activation` | Paystack DD received, waiting for the bank | email, in-app, push | Repayment setup status | verification steps |
| `payment.direct_debit_activated` | Direct debit is active | email, in-app, push | Loan booking / apply screen | verification steps, `can_apply` |
| `loan.submitted` | Loan applied, pending review | email, in-app, push | Loan detail `{ref}` | loans list |
| `loan.approved` | Loan approved | email, in-app, push | Loan detail `{ref}` | loans list |
| `loan.declined` | Loan declined (inbox `data.reason`) | email, in-app, push | Loan detail `{ref}` | loans list, `active-check` |
| `loan.payout_processing` | Bank payout started | email, in-app, push | Loan detail `{ref}` | — |
| `loan.payout_delayed` | Bank payout delayed | email, in-app, push | Loan detail `{ref}` | — |
| `loan.disbursed` | Money sent to the bank account | email, in-app, push | Loan detail `{ref}` (show schedule) | loans list |

### Repayments (reminders, not paid, paid)

| `event` | When it fires | Channels | Tap → screen | Also refresh |
|---|---|---|---|---|
| `loan.repayment_reminder_7d` | 09:00, installment due in 7 days | email, in-app, push | Loan detail `{ref}` → repayment schedule | — |
| `loan.repayment_reminder_3d` | 09:00, due in 3 days | email, in-app, push | Loan detail `{ref}` | — |
| `loan.repayment_reminder_1d` | 09:00, due tomorrow | email, in-app, push | Loan detail `{ref}` | — |
| `loan.repayment_due_today` | 09:00, due today | email, in-app, push | Loan detail `{ref}` + highlight **Pay installment** | wallet |
| `loan.repayment_failed` | An auto-debit attempt failed because of the borrower's account (e.g. insufficient funds). At most once a day per installment | email, in-app, push | Loan detail `{ref}` + highlight **Pay installment** | wallet |
| `loan.overdue_reminder` | 09:00 every day from the day **after** the due date while the installment is unpaid | in-app + push daily; **email + SMS** on days 1, 3 and 7 overdue, then weekly | Loan detail `{ref}` + highlight **Pay installment** / **Settle loan** | wallet |
| `loan.repayment_successful` | An installment was collected (auto-debit or wallet) | email, in-app, push | Loan detail `{ref}` | loan, wallet |
| `loan.fully_repaid` | Last installment collected by auto-debit | email, in-app, push | Loan detail `{ref}` (paid state) | loans list, `active-check` |
| `loan.settled` | Early settlement completed (wallet or Paystack) | email, in-app, push | Loan detail `{ref}` (paid state) | loans list, wallet, `active-check` |
| `wallet.funded` | Wallet top-up credited, or a settlement payment was put in the wallet | email, in-app, push | Wallet | wallet |

### Account

| `event` | When it fires | Channels | Tap → screen |
|---|---|---|---|
| `account.activated` | Admin reactivated the account | email, in-app, push | Home |
| `account.deactivated` | Admin deactivated the account | email, in-app, push | Show a support message (user may be logged out) |
| `account.primary_bank_changed` | Primary payout account changed | email, in-app, push | Bank accounts |
| `admin.broadcast` | Admin announcement / campaign | in-app, push | Inbox (no deep link) |

Unknown or `null` `event` → open the notifications screen. Don't crash on new event names; the list will grow.

---

## 6. Payment-related timeline (what a borrower gets)

For one installment due on **Oct 17**:

| Day | Notification |
|---|---|
| Oct 10, 09:00 | `loan.repayment_reminder_7d` |
| Oct 14, 09:00 | `loan.repayment_reminder_3d` |
| Oct 16, 09:00 | `loan.repayment_reminder_1d` |
| Oct 17, 09:00 | `loan.repayment_due_today` |
| Auto-debit succeeds | `loan.repayment_successful` (or `loan.fully_repaid` if it was the last one) |
| Auto-debit fails (e.g. insufficient funds) | `loan.repayment_failed`, at most once a day while retries keep failing |
| Oct 18 (1 day overdue), 09:00 | `loan.overdue_reminder` push + in-app + **email + SMS** |
| Oct 19 (2 days overdue) | `loan.overdue_reminder` push + in-app only |
| Oct 20 (3 days) and Oct 24 (7 days) | push + in-app + **email + SMS** |
| Oct 31, Nov 7, … (every 7 days) | push + in-app + **email + SMS**; the other days push + in-app only, until paid |
| Borrower pays from the wallet | `loan.repayment_successful` |
| Borrower settles early | `loan.settled` |

Each reminder is sent at most once per installment per day.

---

## 7. Flutter checklist

1. Add `firebase_core` and `firebase_messaging`, and request notification permission (iOS + Android 13+).
2. `getToken()` → send it as `fcm_token` on login/signup.
3. On app start while logged in → `PUT /profile/fcm-token` (with `platform`).
4. `onTokenRefresh` → `PUT /profile/fcm-token`.
5. Logout → `POST /auth/logout` with this device's `fcm_token`.
6. Wire `onMessage`, `onMessageOpenedApp` and `getInitialMessage()` into one `handleNotification(data)` that routes on `data['event']` using §5.
7. Build the notifications screen on `GET /profile/notifications`. On row tap: `POST /profile/notifications/{id}/read`, then `handleNotification(row.event, row.data)`.
8. Bell badge from `GET /profile/notifications/unread-count` (app start, home screen, foreground push). Add "Mark all as read" → `POST /profile/notifications/read-all`.

```dart
void handleNotification(String? event, Map<String, dynamic> data) {
  final ref = data['loan_reference'] as String?;
  switch (event) {
    case 'loan.submitted':
    case 'loan.approved':
    case 'loan.declined':
    case 'loan.payout_processing':
    case 'loan.payout_delayed':
    case 'loan.disbursed':
    case 'loan.repayment_reminder_7d':
    case 'loan.repayment_reminder_3d':
    case 'loan.repayment_reminder_1d':
    case 'loan.repayment_successful':
    case 'loan.fully_repaid':
    case 'loan.settled':
      if (ref != null) return openLoanDetail(ref);
      break;
    case 'loan.repayment_due_today':
    case 'loan.repayment_failed':
    case 'loan.overdue_reminder':
      if (ref != null) return openLoanDetail(ref, highlightPay: true);
      break;
    case 'wallet.funded':
      return openWallet();
    case 'bank_statement.processed':
      return openLoanOffer();
    case 'bank_statement.failed':
      return openBankStatementUpload();
    case 'payment.direct_debit_pending_activation':
    case 'payment.direct_debit_activated':
      return openRepaymentSetup();
    case 'account.primary_bank_changed':
      return openBankAccounts();
  }
  openNotificationsInbox();
}
```

---

## 8. Quick reference

```
POST /auth/user/login          body: fcm_token (optional)
POST /auth/create-password     body: fcm_token (optional)
PUT  /profile/fcm-token        { "fcm_token": "...", "platform": "android|ios|web" }
POST /auth/logout              { "fcm_token": "..." }  removes this device only
GET  /profile/notifications?page=1               includes unread_count
GET  /profile/notifications/unread-count
POST /profile/notifications/{id}/read
POST /profile/notifications/read-all
```
