Files
kickkings_admin_app/instructions.md
T
2026-10-16 01:25:50 +05:30

5.7 KiB

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

{ "username": "ada", "password": "secret" }

200:

{
  "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:

{
  "ticket": {},
  "messages": []
}

Reply (requires support write, or admin):

POST {baseUrl}/api/support/tickets/{ticketId}/messages

{ "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

{ "status": "closed" }

status is open or closed. 200 returns the thread.

Ticket:

{
  "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:

{
  "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
{ "token": "<fcm registration token>", "platform": "android" }

platform is android or ios. 200: { "ok": true }.

  1. 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