This commit is contained in:
2026-10-16 01:25:50 +05:30
commit c60ea2d370
89 changed files with 4661 additions and 0 deletions
+196
View File
@@ -0,0 +1,196 @@
# 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