# Splash screen in the mobile app: handoff for the Flutter developer

Goal: the customer app shows the splash screen that the Kapitano team sets in the admin panel (a picture, an animated GIF or a short mp4 video), instead of the one fixed splash built into the app today.

Full API contract: `docs/api/splash-screen.md`. This file explains what the app has to do.

> **The app only displays.** The app has no screen to create or edit a splash, and needs no token. Everything is managed from the admin panel on the website (Marketing → Splash screen).

---

## 1. What the splash screen is

What the customer sees for a few seconds when the app opens. **Display only**: no tap, no link, no skip button.

There are two kinds:

- **Default**: shown every day. Only one is switched on at a time.
- **Scheduled**: has a start and an end date (Eid, White Friday...). It replaces the default between those two dates. Two scheduled splashes that are switched on never overlap, so there is only one answer at any moment.

If there is none, the app shows its own built-in splash, as it does today.

---

## 2. The key idea: shown from the next launch

The splash shows **before** the app has an internet connection. So:

1. At launch, the app picks a splash **only from the files already saved on the phone**, by the phone's clock, and shows it at once. Never make a network request before showing it.
2. After launch, in the background, the app asks the server, downloads new files and deletes old ones.
3. A new splash appears from the **next launch**.

The admin team knows this and is asked to publish a scheduled splash at least a day before it starts. The server also sends scheduled splashes that start within the next 30 days, so the file is on the phone before its date.

### Important: the system launch screen

The native Android / iOS launch screen (`flutter_native_splash` or equivalent) **cannot** show a file downloaded from the internet. So the sequence is:

```text
Current system launch screen (static, unchanged, as short as possible)
        ↓
SplashScreen widget in Flutter showing the saved file (this is the new part)
        ↓
Home screen
```

Keep the system launch screen's background close to Kapitano's colour (`#10164C`), so the switch to the widget is smooth.

---

## 3. Reading the data

### Option 1: inside `app/data` (preferred)

If the app uses `POST api/v2/app/data`, add the group `splash_screen` to `groups[]` with the launch groups. The rules are the same as for every other group (`docs/api/app-data-sync.md`):

- Send `splash_screen_last_updated` with the value you received last time.
- `null` = nothing changed: do nothing.
- `{"last_updated": "...", "data": {...}}` = a new answer. Save `last_updated` and read `data` as in option 2.
- The group is missing from the answer (failed or deferred) = call the endpoint directly, or wait for the next launch.

### Option 2: the endpoint directly

`GET api/v2/splash-screen`

- No token needed. The answer is the same for every customer and every language.
- Send the usual `System-Key` header.

```json
{
  "result": true,
  "data": {
    "current": { "id": 7, "kind": "scheduled", "...": "..." },
    "default": {
      "id": 3,
      "kind": "default",
      "media_type": "image",
      "media_url": "https://kapitano.shop/public/uploads/all/abc.png",
      "media_version": "4f1c0a9e2b7d",
      "file_size": 412345,
      "fit": "cover",
      "background_color": "#10164C",
      "duration_seconds": 3,
      "starts_at": null,
      "ends_at": null
    },
    "scheduled": [
      {
        "id": 7,
        "kind": "scheduled",
        "media_type": "video",
        "media_url": "https://kapitano.shop/public/uploads/all/eid.mp4",
        "media_version": "9a03be11c4d2",
        "file_size": 3145728,
        "fit": "contain",
        "background_color": "#FFFFFF",
        "duration_seconds": 5,
        "starts_at": "2027-05-16T00:00:00+03:00",
        "ends_at": "2027-05-20T00:00:00+03:00"
      }
    ]
  }
}
```

| Field | Meaning |
| --- | --- |
| `current` | The server's pick for this moment. **Don't rely on it at launch**: pick yourself from the saved files (section 4). |
| `default` | The default splash that is switched on, or `null`. |
| `scheduled` | Every scheduled splash that is switched on, not ended, and starts within 30 days, sorted by `starts_at` (at most 10). Includes the one running now. |

Fields of one splash:

| Field | Meaning |
| --- | --- |
| `id` | The splash's id. |
| `media_type` | `image` (jpg / png / webp), `gif`, or `video` (mp4). |
| `media_url` | The file to download. |
| `media_version` | Changes **only** when the file changes. Save the file under `id` + `media_version`, and download again only when it changes. |
| `file_size` | In bytes. Pictures and GIFs are at most 5 MB, videos at most 15 MB. |
| `fit` | `cover` = fill the screen; the edges may be cut (`BoxFit.cover`). `contain` = the whole picture, with `background_color` around it (`BoxFit.contain`). |
| `background_color` | `#RRGGBB` or `null`. Paint it first, behind the file. `null` = the app's current launch background. |
| `duration_seconds` | 1 to 15. How long a picture or GIF stays. A video plays **once, muted**, and stops at its end or after this many seconds, whichever comes first. |
| `starts_at` / `ends_at` | ISO-8601 with offset; `null` on the default. A scheduled splash is shown while `starts_at <= now < ends_at`. |

> `fit`, `background_color` or `duration_seconds` may change without `media_version` changing. Save the whole latest answer and always read these values from it.

---

## 4. At launch: which splash to show

With no network request, from the last saved answer and the files on the phone:

```dart
SplashItem? pickSplash(SavedSplashAnswer? saved, DateTime nowUtc) {
  if (saved == null) return null;                      // the app's built-in splash

  // 1) a scheduled splash whose window holds the phone's time, with its file fully downloaded
  for (final s in saved.scheduled) {
    if (!s.startsAt!.toUtc().isAfter(nowUtc) &&
        s.endsAt!.toUtc().isAfter(nowUtc) &&
        fileIsReady(s)) {
      return s;
    }
  }

  // 2) otherwise the default, if its file is downloaded
  final d = saved.defaultSplash;
  if (d != null && fileIsReady(d)) return d;

  // 3) otherwise the app's built-in splash
  return null;
}
```

- Always compare in UTC (`DateTime.parse(...)` reads the offset, then `.toUtc()`).
- `fileIsReady` = the file exists under its final name (section 6). A partly downloaded file is never shown.

---

## 5. Displaying it

One full-screen widget:

```text
Container(color: background_color ?? the current launch colour)
  └─ the file, with BoxFit from fit
```

- **image**: `Image.file(file, fit: ...)`.
- **gif**: also `Image.file(file, fit: ...)`; it animates by itself. Show it for the whole `duration_seconds`, even if its loop ends earlier.
- **video**: `video_player` from the local file, `setVolume(0)`, no looping, and `fit` through `FittedBox` + a `SizedBox` of the video's size. Go to the home screen when the video ends or after `duration_seconds`, whichever comes first.
- No skip button, no tap on the screen.
- **Never make the customer wait longer than `duration_seconds`.** Load the app's usual data in parallel underneath. If the time is up and the app is still loading, carry on as today (the usual loading indicator).
- If the file cannot be opened (corrupt file, or a format the device cannot read), don't show a black screen. Move on at once as if there were no splash, and delete the file so it is downloaded again.
- Always portrait. The size suggested to the admin team is 1080 × 2340.

---

## 6. Syncing and downloading (after launch, in the background)

After the home screen appears, not before:

1. Request `splash_screen` (section 3). If it is `null` or the request failed, stop: nothing changes.
2. Save the whole answer (for example as JSON in `shared_preferences`).
3. The wanted list = `default` (if not `null`) + every item of `scheduled`.
4. For each item whose file `<id>_<media_version>.<ext>` does not exist yet:
   - download to a temporary `….part` file in a private folder (for example `getApplicationSupportDirectory()/splash/`);
   - once the download is complete and the size matches `file_size`, rename it to the final name;
   - `ext` comes from the end of `media_url` (`png`, `jpg`, `webp`, `gif`, `mp4`).
5. Delete every file in the folder that does not belong to the wanted list (splashes that were removed, or old versions).
6. If the answer is `default: null` and `scheduled: []`, delete every file in the folder: the app goes back to its built-in splash.

Notes:

- Download one at a time, and don't block anything in the app.
- Videos: preferably download on Wi-Fi when possible. It's fine if it waits for a later launch: the app falls back to the default until it is complete.
- Interrupted download: delete the `.part` file and try again on the next launch.
- Don't download anything on every launch when `media_version` hasn't changed. With `app/data` the answer is usually `null`, just a few bytes.

---

## 7. Special cases

| Case | What happens |
| --- | --- |
| First install of the app | No saved answer, so the built-in splash shows. The new splash appears from the second launch. |
| The admin switched a splash off or deleted it | It disappears from the next answer, its file is deleted, and it stops showing from the launch after the sync. |
| A scheduled splash has ended and the phone hasn't been online since | Section 4 ignores it by time (`ends_at`), even before syncing. |
| The phone's clock is wrong | The scheduled splash may appear early or late on that phone only. Acceptable. |
| A scheduled splash whose file hasn't finished downloading | The default shows instead. |
| No internet | Everything works from the saved files. |

---

## 8. Checklist before handing over

Ask the admin team to set up these cases on the test server, then check:

- [ ] A default with a PNG and `cover`: appears from the second launch and fills the screen.
- [ ] Changing only the colour and duration: applied without downloading the file again.
- [ ] Changing the file: the new one is downloaded and the old one deleted.
- [ ] A GIF with `contain` and a background colour: the whole picture shows, the colour around it, and it animates.
- [ ] An mp4 video: muted, plays once, and stops at `duration_seconds` if it is longer.
- [ ] A scheduled splash starting in a few minutes: downloaded ahead, and shown when the app opens after it starts, **even with the internet off**.
- [ ] After the scheduled splash ends: the default comes back.
- [ ] Switching every splash off: the app goes back to its built-in splash after one sync.
- [ ] Airplane mode from the start: no hang and no black screen.
- [ ] Android and iOS, on a small phone and a tall one.
- [ ] Launch time is never longer than today by more than `duration_seconds`.
