197 lines
5.7 KiB
Markdown
197 lines
5.7 KiB
Markdown
# Flutter prompt: Kick Kings support staff app
|
|
|
|
Paste everything below this line into the Flutter agent. Do not add Firebase project ids, API keys, or service-account JSON to the app repo. The human will place `google-services.json` (Android) and `GoogleService-Info.plist` (iOS) in the Flutter project after creating those apps in the Firebase console.
|
|
|
|
---
|
|
|
|
Build a Flutter staff app for Kick Kings customer support. It is an inbox for people who already have an admin-panel account. Players do not use this app. Do not implement player login or the `/api/support/player/` routes.
|
|
|
|
## Stack
|
|
|
|
- Flutter, null-safe Dart
|
|
- `firebase_core` and `firebase_messaging` for push
|
|
- `http` or `dio` for JSON
|
|
- Persist the bearer token in secure storage (`flutter_secure_storage`)
|
|
- Android and iOS
|
|
|
|
The admin server base URL is a compile-time or settings value, for example `https://kickkings.playpoolstudios.com`. No trailing slash. Call it `baseUrl`.
|
|
|
|
## Auth
|
|
|
|
`POST {baseUrl}/api/auth/login`
|
|
|
|
Headers: `Content-Type: application/json`
|
|
|
|
```json
|
|
{ "username": "ada", "password": "secret" }
|
|
```
|
|
|
|
200:
|
|
|
|
```json
|
|
{
|
|
"ok": true,
|
|
"token": "<admin session token>",
|
|
"account": {
|
|
"id": "<account id>",
|
|
"username": "ada",
|
|
"isAdmin": false,
|
|
"permissions": {
|
|
"support": { "read": true, "write": true }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
`permissions` also includes the other admin pages. This app only cares about `support`.
|
|
|
|
401: `{ "error": "Invalid credentials" }`
|
|
|
|
429: `{ "error": "Too many failed login attempts. Try again later.", "retryAfterSeconds": 900 }`
|
|
|
|
After login, if `account.isAdmin` is false and `account.permissions.support.read` is not true, show “This account does not have support access” and do not enter the inbox. `isAdmin: true` can read and write.
|
|
|
|
Send the token on every later request:
|
|
|
|
`Authorization: Bearer <token>`
|
|
|
|
The token is also an HttpOnly cookie on web. Ignore cookies in Flutter. The token does not expire until the server secret changes; still offer logout.
|
|
|
|
Logout:
|
|
|
|
1. `DELETE {baseUrl}/api/support/devices` with `{ "token": "<fcm token>" }` if you have one
|
|
2. Delete the bearer token locally
|
|
3. Return to the login screen
|
|
|
|
Do not call `/api/auth/logout`. That only clears the browser cookie.
|
|
|
|
## Inbox API
|
|
|
|
All staff routes require the bearer token. 401 means the token is invalid: return to login. 403 means the account cannot do that action.
|
|
|
|
List:
|
|
|
|
`GET {baseUrl}/api/support/tickets?status=open&category=bug`
|
|
|
|
`status` is optional: `open` or `closed`. Omit it for both.
|
|
|
|
`category` is optional. Slugs:
|
|
|
|
| slug | label |
|
|
| --- | --- |
|
|
| bug | Report Bug |
|
|
| cheating | Report cheating |
|
|
| withdrawal | Request withdrawal |
|
|
| crypto | Pay with crypto |
|
|
| other | Other |
|
|
|
|
200: `{ "tickets": [ SupportTicket ] }`
|
|
|
|
Open tickets are already sorted before closed ones, then by latest message.
|
|
|
|
Thread:
|
|
|
|
`GET {baseUrl}/api/support/tickets/{ticketId}`
|
|
|
|
200:
|
|
|
|
```json
|
|
{
|
|
"ticket": {},
|
|
"messages": []
|
|
}
|
|
```
|
|
|
|
Reply (requires support write, or admin):
|
|
|
|
`POST {baseUrl}/api/support/tickets/{ticketId}/messages`
|
|
|
|
```json
|
|
{ "body": "We are looking into this." }
|
|
```
|
|
|
|
201 returns the same thread shape. Body is plain text, 1 to 4000 characters after trim.
|
|
|
|
Close or reopen (requires write):
|
|
|
|
`POST {baseUrl}/api/support/tickets/{ticketId}/status`
|
|
|
|
```json
|
|
{ "status": "closed" }
|
|
```
|
|
|
|
`status` is `open` or `closed`. 200 returns the thread.
|
|
|
|
Ticket:
|
|
|
|
```json
|
|
{
|
|
"id": "uuid",
|
|
"userId": 42,
|
|
"username": "playername",
|
|
"category": "bug",
|
|
"categoryLabel": "Report Bug",
|
|
"status": "open",
|
|
"createdAt": "2026-10-15T12:00:00.000Z",
|
|
"updatedAt": "2026-10-15T12:00:00.000Z",
|
|
"lastMessageAt": "2026-10-15T12:00:00.000Z",
|
|
"lastMessagePreview": "The match froze"
|
|
}
|
|
```
|
|
|
|
Message:
|
|
|
|
```json
|
|
{
|
|
"id": "uuid",
|
|
"ticketId": "uuid",
|
|
"authorKind": "player",
|
|
"authorName": "playername",
|
|
"body": "The match froze",
|
|
"createdAt": "2026-10-15T12:00:00.000Z"
|
|
}
|
|
```
|
|
|
|
`authorKind` is `player` or `staff`. Show player messages on the left and staff messages on the right. Use `categoryLabel` in the UI, not the slug, except when you need the slug for filters.
|
|
|
|
Errors are `{ "error": "..." }` with 400, 401, 403, 404, or 503.
|
|
|
|
## Screens
|
|
|
|
1. Login: username, password, error text, submit.
|
|
2. Inbox: filter by status (open by default, plus closed and all) and by category. List shows category label, player username, status, and preview. Pull to refresh.
|
|
3. Thread: messages, reply box hidden when the account cannot write, and a Close / Reopen button hidden the same way.
|
|
4. Tapping a push notification opens that ticket (`ticketId` in the data payload).
|
|
|
|
Poll the open list and the open thread every 5 seconds while the app is in the foreground, in addition to push.
|
|
|
|
## Push (FCM)
|
|
|
|
Initialize Firebase with the local `google-services.json` / `GoogleService-Info.plist`. Do not hardcode a server key.
|
|
|
|
On login, after notification permission:
|
|
|
|
1. `FirebaseMessaging.instance.getToken()`
|
|
2. `PUT {baseUrl}/api/support/devices`
|
|
|
|
```json
|
|
{ "token": "<fcm registration token>", "platform": "android" }
|
|
```
|
|
|
|
`platform` is `android` or `ios`. 200: `{ "ok": true }`.
|
|
|
|
3. Listen to `onTokenRefresh` and PUT again.
|
|
|
|
Register a background handler with `FirebaseMessaging.onBackgroundMessage`. In the foreground, `FirebaseMessaging.onMessage` should refresh the inbox and, if the payload `ticketId` is the open thread, refresh that thread. Notification title is the category label. Data always includes `ticketId` and `category`.
|
|
|
|
Request alert permission on iOS before `getToken`. On Android 13+, request `POST_NOTIFICATIONS`.
|
|
|
|
If the user denies permission, the inbox still works; skip device registration.
|
|
|
|
## Out of scope
|
|
|
|
- Player ticket creation
|
|
- Any admin page other than support
|
|
- Sending pushes from the phone (the server sends them)
|
|
- Storing the Firebase service account in the app
|