# Lifecycle push flows (flow A) — mobile app contract

The backend sends the marketing sheet's lifecycle pushes (`Kapitano_Push_Notification_Flows_AR1`). **Flow A** is the
only one live: a phone that opened the app and did not finish registering (phone + OTP) gets up to three pushes —
A1 two hours after the first open, A2 the next day at 19:30, A3 on day 3 at 19:30 (Damascus time) — and the flow
stops the moment the phone has an account. The app does not decide anything: it only tells us about the phone,
shows the push, and reports a tap.

All endpoints need the usual headers (`System-Key`, `App-Language`; `Authorization: Bearer <sanctum token>` when a
customer is signed in on the phone, an empty `Bearer ` for a guest). Base: `/api/v2`. **No existing field changed**;
`quest/device-token` and `profile/update-device-token` stay exactly as they are and **must keep being called** — the
FCM topics and every existing campaign depend on them.

## 1. Tell us about the phone — `POST push/device`

Call it on **every launch** (after `OneSignal.login(...)` once the OneSignal SDK is in the app), after signing in or
out, and whenever the notification permission or the OneSignal subscription changes.

```json
{
  "temp_user_id": "0199a3c2-7f1e-7b44-9c2d-5e8f1a2b3c4d",
  "platform": "android",
  "lang": "ar",
  "app_version": "2.7.0",
  "timezone": "Asia/Damascus",
  "onesignal_id": "5d3b…",
  "subscription_id": "8f1c…",
  "push_token": "token the OneSignal SDK holds",
  "push_enabled": true
}
```

| key | meaning |
|---|---|
| `temp_user_id` | **required**, the device's permanent uuid — the same one sent to `quest/device-token` and carried by the guest's cart |
| `platform` | `ios` or `android` |
| `lang` | `ar` or `en` — the language of the push text (English only if marketing wrote it, otherwise Arabic) |
| `app_version` | free text, up to 32 characters |
| `timezone` | an IANA name |
| `onesignal_id`, `subscription_id` | from the OneSignal SDK; once `subscription_id` is sent the phone is reached through OneSignal |
| `push_token` | the token OneSignal holds; used only to recognise an old Android phone that registered without a permanent id |
| `push_enabled` | the notification permission **as the phone says it** (`false` = the person turned notifications off: nothing is sent, on any channel) |

Every field except `temp_user_id` is optional; a field you leave out keeps its last value. Who is signed in is read
from the bearer token, so after a logout call it again with the empty bearer.

`200 { "result": true, "device_id": 123, "channel": "onesignal" }` (`channel` is `fcm` until a `subscription_id` is
sent). The call never fails the app's launch: an unexpected server problem answers `500 { "result": false, ... }`
and the app just carries on.

| HTTP | when |
|---|---|
| 200 `{result:false,message:"Request not found!"}` | no / wrong `System-Key` (like every API route) |
| 422 `{result:false,errors:{…}}` | `temp_user_id` missing or not a uuid, `platform` / `lang` / `timezone` / `push_enabled` invalid |
| 429 | more than 30 calls a minute from one IP |

## 2. A tap — `POST push/opened`

When the person taps one of these pushes: `{ "msg": "<data.msg of the push>" }`. Always `200 { "result": true }`,
whether the id is known or not; calling it twice is harmless. Limit: 60 a minute.

## 3. The push

OneSignal pushes carry `additionalData`, the FCM pushes (phones whose app has no OneSignal yet) the same keys as FCM
`data`:

| key | meaning |
|---|---|
| `type` | `lifecycle` (OneSignal) |
| `flow`, `step` | `A`, `A1` / `A2` / `A3` |
| `msg` | unguessable id of this push: send it back to `push/opened` |
| `screen` | where a tap goes: `login` (flow A always) |
| `link` | the full link with its UTM tags as in the sheet, e.g. `https://kapitano.shop/login?utm_source=push&utm_medium=app_push&utm_campaign=flow_a&utm_content=a1` — log it; navigate by `screen` |
| `analytics_tag` | `flow_a_a1`, `flow_a_a2`, `flow_a_a3` — log it with the tap |

**There is no `item_type`** (it would make the app ask a guest to sign in). On tap: open the sign-in / registration
screen (`screen = login`) — never force a guest to sign in before that. An unknown `screen` opens the home screen.

## 4. Identity in OneSignal

A guest is `d_<temp_user_id>`; a signed-in customer `u_<users.id>`. `OneSignal.login("u_…")` after signing in or
registering; on logout `OneSignal.logout()` then `OneSignal.login("d_<temp_user_id>")`. Call `OneSignal.login`
**before** `push/device` on every launch.

## 5. Checklist for the app

1. Add the OneSignal SDK (the correct app id per environment) and ask for the notification permission early — flow A
   depends on it, and iOS asks only once.
2. Call `push/device` as in §1, with the right `push_enabled`.
3. **Keep** `quest/device-token` and `profile/update-device-token`.
4. On a tap: navigate by `screen`, call `push/opened` with `data.msg`, log `analytics_tag`.
5. Before shipping: on one Android and one iPhone, check that the existing FCM pushes (order rating, back in stock,
   campaigns, topics) still arrive and open the right screen after the OneSignal SDK is added. OneSignal warns
   that Firebase Messaging left in the app can swallow its messages; remove old `FirebaseMessagingService`
   overrides and do not call `getToken()` / `deleteToken()` yourselves. Until this is proven the backend keeps
   `push_flows_onesignal_enabled` off and sends flow A through FCM to every phone.
6. A phone that is not updated keeps receiving flow A over FCM with `link` and `screen` in `data` (no `item_type`).
   Tell us which `link` values that older version understands.

## 6. Every other push also arrives through OneSignal (updated app only)

Once the server switch `push_all_via_onesignal` is on, **every** push to a phone whose app registered a OneSignal subscription (`POST push/device` with `subscription_id`) arrives through OneSignal instead of FCM: order status, rating requests, return answers, back in stock, cart reminders, admin notifications and broadcasts. A phone that was not updated keeps receiving them through FCM, unchanged. A phone never gets the same push on both channels.

What the app must do:

- Read `additionalData` of these pushes exactly like the FCM `data` before: `item_type`, `item_type_id`, `link`, `click_action` (all strings). They carry no `type = lifecycle` / `flow` / `step` / `msg`; those exist only for the flows above.
- Keep calling `quest/device-token` and `profile/update-device-token` (the server needs the FCM token to know which phone a push is for).
- Keep Firebase in the app only for the token; the server removes an updated phone from the FCM topics, so a broadcast arrives once, through OneSignal.
- Calling `POST push/device` on every launch (and when the subscription changes) is what moves a phone from FCM to OneSignal.

## 7. The icon and the colour of an Android notification

A push shown by the OneSignal SDK looks like the **bell** (and has no brand colour) unless the app carries its own
small icon. This is how it was before OneSignal, when Firebase Messaging took the icon and colour from the app's
manifest (`com.google.firebase.messaging.default_notification_icon` / `_color`); **OneSignal does not read those**.
iOS shows the app icon by itself and needs nothing.

**In the Android app (once, in the build):**

1. **Small icon** (the status-bar icon, and the one tinted with the accent colour): a **white silhouette on a
   transparent background**, PNG, named exactly `ic_stat_onesignal_default.png` (OneSignal's default name, so no
   server setting is needed) in `android/app/src/main/res/` : `drawable-mdpi` 24×24, `drawable-hdpi` 36×36,
   `drawable-xhdpi` 48×48, `drawable-xxhdpi` 72×72, `drawable-xxxhdpi` 96×96. A colour logo here turns into a white
   square. Any other name works if the server sends it (`ONESIGNAL_SMALL_ICON`, below).
2. **Accent colour**, in `AndroidManifest.xml` inside `<application>` (the brand navy `#10164C`, ARGB):
   `<meta-data android:name="com.onesignal.NotificationAccentColor.DEFAULT" android:value="FF10164C" />`
3. **Large logo** (optional, the picture at the side of the notification): a drawable named
   `ic_onesignal_large_icon_default.png` in the same folders (the launcher logo, 192×192 or more), or a URL sent by
   the server (below).

**On the server** (`.env`, then `php artisan config:clear`; every push, flows and all the others, carries them
when set and usable; nothing is sent while empty):

| Variable | Value |
| `ONESIGNAL_SMALL_ICON` | the drawable's name in the app, lower case, `[a-z0-9_]` (no path, no `.png`); **default `ic_notification`** (already in every installed version of the app, so the bell is replaced with no app update) - set it empty to stop sending |
| `ONESIGNAL_SMALL_ICON` | the drawable's name in the app, lower case, `[a-z0-9_]` (no path, no `.png`) |
| `ONESIGNAL_LARGE_ICON` | a public `https://` URL of the logo, square, 256 px or more |
| `ONESIGNAL_ACCENT_COLOR` | `#RRGGBB` or ARGB; **default `FF10164C`** (the brand navy) - set it empty to stop sending |

A value that is not usable (a URL that is not https, a colour that is not hex, a name with a space or a dot) is left
out, never sent half-right. Admin → Push flows → Control → System check shows what is set. The small icon must exist
**inside the app**: naming one the app does not have gives the default bell again.

### 7.1 Best practice: the app is the one source of truth

Whoever sends a push - our server (flows and every other notification), a campaign or template composed in the
OneSignal dashboard, or Firebase itself - the phone builds the notification from **the message first, then the
app's defaults**. A message that carries no icon (a dashboard campaign never does unless someone remembers to fill
it) shows the app's default. So the defaults in the app are what makes "my icon, always" true; the server fields of
§7 are only a per-message override and a safety net, never the main mechanism.

Put **all** of these in the Android app (one release, `android/app/src/main/`):

1. `res/drawable-{m,h,xh,xxh,xxxh}dpi/ic_stat_onesignal_default.png` - the small icon (white silhouette on
   transparent, 24 / 36 / 48 / 72 / 96 px). OneSignal's default name, picked up with no code.
2. `res/drawable-xxxhdpi/ic_onesignal_large_icon_default.png` - the large logo (192 px or more, colour allowed).
3. The accent colour for OneSignal, in `AndroidManifest.xml` inside `<application>`:
   `<meta-data android:name="com.onesignal.NotificationAccentColor.DEFAULT" android:value="FF10164C" />`
4. The same icon and colour for **Firebase's own notifications** (any push Firebase shows itself: a phone still on
   the old way, the fallback when OneSignal refuses a message), in the same `<application>`:
   ```xml
   <meta-data android:name="com.google.firebase.messaging.default_notification_icon"
              android:resource="@drawable/ic_stat_onesignal_default" />
   <meta-data android:name="com.google.firebase.messaging.default_notification_color"
              android:resource="@color/kapitano_notification_accent" />
   ```
   with `res/values/colors.xml`: `<color name="kapitano_notification_accent">#10164C</color>`.
5. If the app draws a notification itself (for example a local notification while the app is open, with
   `flutter_local_notifications`), give that code the same icon (`AndroidInitializationSettings('@drawable/ic_stat_onesignal_default')`):
   neither SDK default applies to a notification the app builds.

Make the small icon from the logo's simplest shape in Android Studio (File > New > Image Asset > Notification
Icons), never by shrinking the colour logo: Android keeps only the alpha channel, so a full-colour square becomes a
white square.

**Then try it, with no help from the server** (so you prove the defaults, not the override): on one Android 12+
phone and one older or Samsung phone, send (a) a push from the OneSignal dashboard with no icon fields, (b) one
from the server (Admin > Push flows: switch on, or `php artisan push-flows:test-send --user=<id>`), (c) one that
goes through Firebase (a phone on the old path). All three must show the Kapitano icon and the navy tint. Do it
on a release build: an app installed from an old build keeps the old resources.

### 7.2 Done from the server, no app release (2026-11-01)

The app team confirmed the installed app already contains the small icon `ic_notification` and uses the brand navy
`FF10164C`. So the server sends `small_icon = ic_notification` and `android_accent_color = FF10164C` with every
OneSignal push **by default** (config/services.php, overridable with `ONESIGNAL_SMALL_ICON` /
`ONESIGNAL_ACCENT_COLOR`, empty = off): the flows, every other push through the gateway, and
`push-flows:test-send`. iOS ignores both fields. What still needs the OneSignal dashboard: a campaign or Template
composed there does not pass through the server, so fill **Small Icon** `ic_notification` and **Accent Color**
`FF10164C` in its Android options (once, in a Template). Check on a phone with the app closed: the Kapitano icon
instead of the bell, and a navy tint at the notification's head.
