init
This commit is contained in:
+196
@@ -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
|
||||
Reference in New Issue
Block a user