# PRD — skrmiish

Evidence date: 2026-10-03. Source: `git@bitbucket.org:overdrive/skrmiish.git`, `master` at `5077fba` (2020-12-09).

There is no prior PRD in Silicon Ark. `README.md` is the Create React App template and is not a product specification. Requirements below come from the screens, routes, and API calls in this front end. The API itself is not in this repository, so server-side rules are **Unknown** unless the client shows them.

Provenance is marked on each item.

## Product purpose

The page title is "Skrmiish Esports | Shoot for Loot".

**Inferred** from the UI and the API the client calls: Skrmiish is a web console where a streamer (Partner) publishes a presence, runs leaderboards, and sees supporter balance and revenue share. A Supporter can register. Visitors can read the public home page.

A documented product statement was not found. Marketing sections titled "What is Skrmiish" and "Ready to take it to the next level?" use lorem ipsum. Provenance of the purpose: **Inferred**. Provenance of the placeholder copy: **Implemented**.

## Users and roles

| Role | How the client assigns it | What they can open | Evidence | Provenance |
|---|---|---|---|---|
| Anonymous visitor | `auth.loggedIn` is false and `auth.role` is `""` | `/` only. Protected routes redirect to `/` | `ProtectedRoute.js` | Implemented |
| Supporter | `setUser` sets `role` to `"Supporter"` when `is_streamer` is not `true` | `/`. The same protected routes still redirect to `/` because the gate also requires `role === "Partner"` | `authSlice.js`, `ProtectedRoute.js` | Implemented |
| Partner | `setUser` sets `role` to `"Partner"` and `isStreamer` to true when `is_streamer === true` | `/`, `/accounts`, `/profile`, `/manageLeaderboard`, `/streamer` | `authSlice.js`, `App.js` | Implemented |

Twitch `broadcaster_type` of `affiliate` or `partner` is used only after an HTTP 401, to choose which signup modal opens. It does not by itself set the Skrmiish role. Provenance: **Implemented** (`App.js`).

There is no admin role in this client. Provenance: **Implemented** (absence). Whether the API has other roles is **Unknown**.

## Main workflows

### W1 — Open the public home page

Trigger: a person opens `/`.

The page renders navigation, a three-column grid (left drawer, main, right drawer), then marketing blocks: What is Skrmiish, Play with or against heroes, a heroes grid, current leaderboards, "Leaderboards as a service", "Ready to take it to the next level?", download images, and a footer.

On load, sagas can request streamers (`GET /api/v1/Streamer`) and leaderboards (`GET /api/v1/Leaderboard`). The right rail sorts streamers by `clicker_game.clicks` and shows the first three. Leaderboard titles from the list are rendered in that rail.

"Get Started", "APPLY NOW", and several hero-grid actions are present. Two hero-grid handlers call `alert("Todo")`. Download images are static Azure Blob URLs with no store links in the component.

Provenance: **Implemented**.

### W2 — Register with email and password

Trigger: the visitor opens the sign-up modal and submits the normal registration form.

The client posts `POST /api/v2/User` with `is_streamer: false`, name, surname, email, username, mobile number, date of birth, an empty address, an empty avatar and salt, the password as `pwd`, and `signup.signup_type` `"0"`. A static token field is also sent. The value is hardcoded in the saga and is not repeated here.

Success stores the response with `setUser`, closes the register modal, and shows "Registration Succesful!". Failure shows "Registration Failed!".

Field-level validation in the client, beyond the inputs the form collects, was not found. A comment in one partner form says "we can run a validation script here" and does not run one.

Provenance: **Implemented** (`NormalRegister.js`, `SignUpModal`).

### W3 — Register or continue as a Supporter after Twitch

Trigger: Twitch implicit OAuth returns a hash on `/`, or the supporter modal is opened (including after HTTP 401 when `broadcaster_type` is not `affiliate` or `partner`).

`useGetTokenAndLogin` reads `location.hash`, takes the first value after `=`, and dispatches `INIT_TWITCH_LOGIN`.

That saga:

1. Stores the token.
2. Calls `GET https://api.twitch.tv/helix/users` with `Authorization: Bearer` and a hardcoded `Client-ID`.
3. Posts `POST /api/v1/User/login` with `loginType: 1`, the Twitch user's email, and a `token` field.

The login body sets `token` twice. In JavaScript the second assignment wins, so the Twitch access token is not the value that remains on `token`. Provenance: **Implemented** (`LogInWithTwitch.js`). Whether the API still accepts this body is **Unknown**.

Supporter registration (`REGISTER_AS_SUPPORTER`) posts `POST /api/v2/User` with camelCase fields (`isStreamer: false`, `signupType: "twitch"`, `accessToken`). That shape differs from normal registration, which uses snake_case. Provenance: **Implemented**.

An older Twitch signup helper in `authSlice.js` posts a body with the literal name and email `donniethedev` / `donniethedev@gmail.com`. `verifyTokenAndGetUserID` is exported and was not found to be called from the live login hook. Provenance: **Implemented** as unused code.

### W4 — Register as a Partner (streamer)

Trigger: the streamer modal or the partner tab of the sign-up modal submits.

`REGISTER_AS_STREAMER` posts `POST /api/v2/User` with `is_streamer: true` and `signup.signup_type` `"twitch"`. `access_token` is set to the result of calling `select(...)` inside a plain async function, which does not read the Redux store. Provenance: **Implemented** (`RegisterAsStreamer.js`). The effect is that the access token sent here is not the Twitch token from state. **Inferred** from the call site.

Success sets the user, closes the streamer modal, and toasts success. The failure branch calls `showToast` without `yield put`, so the failure toast is not dispatched. Provenance: **Implemented**.

Copy on the partner congrats tab says the person is a "Skrmiish Partner". The supporter congrats tab says "Supporter. It’s time to fight for your heroes!" Provenance: **Implemented** as UI copy.

### W5 — Log in with email and password

Trigger: the login modal submits email and password.

`POST /api/v1/User/login` with `logintype: 0` (different casing from the Twitch body's `loginType`), email, `pwd`, `mobile_platform: "web"`, and the same static token field.

Success stores the user, closes the login modal, and toasts "Login Succesful!". Any thrown error toasts "Login Failed!".

Provenance: **Implemented** (`NormalLogin.js`).

### W6 — Stay signed in and open a partner page

Trigger: a later visit, or a click toward `/accounts`, `/profile`, `/manageLeaderboard`, or `/streamer`.

The whole Redux tree is persisted to `localStorage` under the key `root` (`redux-persist`). `loggedIn` and `role` survive a refresh.

`ProtectedRoute` renders the page only when `loggedIn` is true and `role === "Partner"`. Otherwise it redirects to `/`.

When a Partner is logged in and streamer data is still an empty object, `useGetStreamerData` dispatches `GET_STREAMER_DATA_REQUEST`, which downloads every streamer and keeps the row whose `uuid` matches the user.

Provenance: **Implemented**.

### W7 — See account balances and request a payout

Trigger: a Partner opens `/accounts`.

Tabs: OVERVIEW, QR CODE, PAYOUT DETAILS, LEGAL.

Overview reads `state.streamerData` and returns nothing until that object has keys. It shows `account.supporter_balance` with a Buy button, and `$ {account.revenue_share.amount}` with Request Payout. Below that is a "Subscriber Leaderboard" / "Active leaderboard tracker" table (Supporter, Activity, and further columns in the same file).

Request Payout opens a modal. The confirm path dispatches `INIT_REQUEST_PAYOUT` with `uuid` and an amount. The saga posts `POST /api/v1/Streamer/{uuid}/request_payout` with `{ uuid, amount }`. It does not toast success or failure.

The payout-details tab shows the method label "Paypal", the user's email, and the sentence "You only qualify for Payouts if your Revenue Share is a minumum of $100." The save control dispatches `INIT_UPDATE_PAYMENT_DETAILS` with `email` on the action, not under `payload`. The saga reads `action.payload` and also destructures `uuid` from the string `state.auth.user.uuid`. Provenance: **Implemented**. Whether a payout is actually saved is **Unknown** because those two client mistakes prevent a well-formed request. The $100 line is UI copy. Client-side enforcement of $100 was not found. API enforcement is **Unknown**.

Buy opens a modal titled "Buy Subscription Credits" whose description says funds will be removed from revenue share to buy subscription credits. The amount field is not wired to an API call in the portion that was read. No credit-purchase saga exists. Provenance: **Implemented** as UI. Purchase behaviour is **Unknown / not implemented in this client**.

### W8 — Edit profile, links, bio, and avatar

Trigger: a Partner opens `/profile`.

Tabs: PROFILE, DOWNLOADS (hidden below 768px), SETTINGS.

Settings shows username, email (disabled), first name, last name, cell number (country prefix `+27` disabled, and the number input disabled), date of birth, a disabled password field, and a Delete Account button with no handler found. Locked-detail copy says to email `changemyinfo@skrmiish.com`. Save dispatches `INIT_UPDATE_USER`, which posts the stored user object plus name, username, surname, mobile number, and `dob` as `{year}-{month}-{day}T00:00:00+02:00` to `POST /api/v1/User/{uuid}/update`.

Profile can update a bio (`POST /api/v1/Streamer/{uuid}/update_bio` with `text` and `timestamp`) and links (`POST .../update_links` with twitch, instagram, facebook, website, twitter, and `merch: ""`). Discord is accepted by the saga argument list and is not sent in the body. Avatar crop uses `react-image-crop`, then `POST /api/v1/Image` and a user update.

Provenance: **Implemented**.

### W9 — Create a leaderboard

Trigger: a Partner opens `/manageLeaderboard` and uses CREATE.

The form collects a title (max length 30), a game (`Fortnite` or `PubG`, default Fortnite), a format field fixed to `KOTK` and disabled, banner colour, prize, description, start and end date parts, and up to six carousel cards. Format, prize, and description are not included in the request. Comments say prize and description should be uncommented "when api is ready".

Submit dispatches `INIT_CREATE_LEADERBOARD`. The saga base64-encodes card images, posts each one to `POST /api/v1/Image`, then posts the leaderboard to `POST /api/v1/Leaderboard` with `title`, `game_title`, empty `ssid` / `usid`, `thumbnail`, `carousel_items`, `players: []`, and `schedule.startTimestamp` plus `schedule.durationHours`.

`durationHours` is computed as `endDate.getTime() - startDate.getTime() / 3600000`. Division binds tighter than subtraction, so the value is not the hour difference. Provenance: **Implemented**. That the unit is hours is **Inferred** from the variable name, and the arithmetic does not produce that unit.

Start and end are parsed as `GMT+0200`. Provenance: **Implemented**.

TRACK and ARCHIVE tabs exist. Whether they load live or archived rows from the API was not established beyond the tab components being present. **Requires Confirmation** if a tester needs the exact columns.

On viewports under 768px the CREATE tab is not the initial tab; the effect sets the tab index to 1 (TRACK). Provenance: **Implemented**.

### W10 — Alerts, feature requests, newsletter

- Alerts: `GET /api/v1/User/{uuid}/alerts`. The streamer main panel lists `alert_msg`. The home page comment and a commit say the alert centre is not shown on mobile.
- Missing something: `POST /api/v1/Streamer/{uuid}/request_feature` with `{ missingSomething }`.
- Newsletter subscribe: `GET /api/v1/User/{uuid}/newsletter_subscribe`.
- Newsletter unsubscribe and subscription check both `GET /api/v1/Leaderboard`. Provenance: **Implemented**. That these two were meant to be newsletter endpoints is **Inferred** from the saga names. As written, they do not call a newsletter URL.

### W11 — Session loss

An axios response interceptor, registered on every `App` render, treats HTTP 401 as follows: if a Twitch token is in state and `broadcaster_type` is `affiliate` or `partner`, open the streamer modal; otherwise if a Twitch token is present, open the supporter modal. It does not clear `auth` or redirect. If `error.response` is missing, reading `status` throws. Provenance: **Implemented**.

Logout exists as the `logOut` reducer. Where every menu calls it was not fully traced. Provenance: reducer **Implemented**. Coverage of the control is **Unknown / Requires Confirmation**.

## Functional requirements

### FR-001 — Public home

The system shows a public home page at `/` without login.

Evidence: `App.js` route `/`, `SPA.js`.

Implementation: Implemented.

Confidence: High.

### FR-002 — Partner route guard

The system shows `/accounts`, `/profile`, `/manageLeaderboard`, and `/streamer` only when the persisted session is logged in and `role` is `Partner`. Everyone else is sent to `/`.

Evidence: `ProtectedRoute.js`.

Implementation: Implemented.

Confidence: High.

### FR-003 — Email registration

A visitor can submit name, surname, email, username, mobile number, date of birth, and password. The client creates a non-streamer user on `POST /api/v2/User`.

Evidence: `NormalRegister.js`.

Implementation: Implemented in the client. Server acceptance is Unknown.

Confidence: High for the client contract. Low for the live API.

### FR-004 — Email login

A visitor can submit email and password. The client calls `POST /api/v1/User/login` with `logintype` 0 and `mobile_platform` `web`.

Evidence: `NormalLogin.js`.

Implementation: Implemented in the client.

Confidence: High for the client.

### FR-005 — Twitch sign-in

A visitor can follow a Twitch implicit-grant link (`response_type=token`, `scope=user_read`) and, on return, the client reads the hash and attempts Helix user lookup plus Skrmiish login.

Evidence: `TwitchButton.js`, `useGetTokenAndLogin.js`, `LogInWithTwitch.js`.

Implementation: Implemented. The login body overwrites the Twitch token. The redirect host is an Amplify URL, not the Azure deploy URL.

Confidence: High that this is what the code does. Low that it succeeds against current Twitch and API hosts.

### FR-006 — Role from `is_streamer`

After a successful auth response, `is_streamer === true` becomes Partner. Any other value becomes Supporter.

Evidence: `authSlice.js` `setUser`.

Implementation: Implemented.

Confidence: High.

### FR-007 — Partner account overview

A Partner with streamer data can see supporter balance and revenue-share amount, and can open payout and credit modals.

Evidence: `Accounts/MainGrid/Main/Overview/Overview.js`.

Implementation: Implemented for display. Credit purchase has no saga.

Confidence: High.

### FR-008 — Payout request

A Partner can submit a payout amount. The client posts `uuid` and `amount` to the streamer payout endpoint.

Evidence: `RequestPayout.js`, payout modal.

Implementation: Implemented as a request. The $100 minimum is displayed and not enforced in the client.

Confidence: Medium. The modal wiring was confirmed; minimum enforcement was not.

### FR-009 — Profile update

A Partner can save username, first name, surname, mobile number, and date of birth to `POST /api/v1/User/{uuid}/update`. Email and password inputs are disabled. Cell number input is disabled. Delete Account has no request.

Evidence: `SettingsTab.js`, `UpdateUser.js`.

Implementation: Partial. Save is implemented. Delete, email change, and password change are not implemented in the client.

Confidence: High.

### FR-010 — Leaderboard create

A Partner can submit a leaderboard with title, game (Fortnite or PubG), carousel items, and a schedule. Images are uploaded through `POST /api/v1/Image` first.

Evidence: `CreateTab.js`, `CreateLeaderBoard.js`.

Implementation: Implemented, with prize, description, and format omitted, and with the duration formula above.

Confidence: High for the payload. Low for API acceptance.

### FR-011 — Public streamer and leaderboard lists

The home page loads streamers and leaderboards from the API without an auth header in those sagas.

Evidence: `Streamers.js`, `Leaderboards.js`.

Implementation: Implemented.

Confidence: High for the client calls.

### FR-012 — Deployed site

A pipeline on `master` installs dependencies and deploys the `build` directory to the Azure static website named in `bitbucket-pipelines.yml`.

Evidence: `bitbucket-pipelines.yml`.

Implementation: The pipeline file exists. The script calls `npm build`, and `package.json` defines `build` only as `npm run build`. Whether the pipeline succeeds is **Unknown / Requires Confirmation**.

Confidence: High for the file contents. Low for a working deploy.

## Integrations

| System | Purpose | Trigger | Direction | Auth as seen in the client | Failure behaviour |
|---|---|---|---|---|---|
| Skrmiish API `skmsapi-dev.azurewebsites.net` | Users, streamers, leaderboards, images, payouts, alerts | User actions and page load | Client → API | A static token on some login and register bodies. No `Authorization` header was found on the other calls | Toasts on some failures. Many sagas only `console.log` |
| Twitch OAuth and Helix | Identify the Twitch user | Sign-in button and hash on return | Browser → Twitch, then client → Helix | Implicit token in the URL hash. Helix uses Bearer plus Client-ID | Toast "Login Failed!" |
| Azure Blob `skmwebsitemain.blob.core.windows.net` | Marketing images | Page render | Browser → blob | None in the client | Image fails to load. No fallback was found |
| Azure static website `skmwebsitemain.z6.web.core.windows.net` | Host the production build | Pipeline on `master` | Pipeline → storage | `DESTINATION_SAS_TOKEN: $SASTOKEN` | Not described |
| AWS Amplify URL `master.d373v11reppwb7.amplifyapp.com` | Twitch `redirect_uri` | Twitch button | Browser redirect | OAuth implicit | Unknown |
| Zendesk | Chat snippet in `index.html` | Commented out | — | A snippet key is in a comment and is not repeated here | Not active |

## Important constraints

- The client is the only system in this repository. Business rules that the API enforces cannot be confirmed from here.
- Partner pages depend on `is_streamer` from the login or register response. A Supporter session cannot open them.
- Auth state, including the user object, is written to `localStorage`.
- Login and registration send a static token that is committed in source. That is a credential-handling defect in the client. The value is intentionally omitted from this PRD.
- The API base URL is a host whose name contains `-dev`. A production API base URL was not found.
- Dates sent by profile update use offset `+02:00`. Leaderboard schedule strings are parsed as `GMT+0200`.

## Error conditions visible in the client

| Condition | Behaviour | Provenance |
|---|---|---|
| Login or normal registration throws | Toast "Login Failed!" or "Registration Failed!" | Implemented |
| Partner registration throws | `showToast` is called without dispatch | Implemented |
| HTTP 401 with a Twitch token in state | Open streamer or supporter modal. Session is not cleared | Implemented |
| HTTP 401 without a Twitch token | Interceptor rejects and does not open a modal | Implemented |
| Network error with no `error.response` | Interceptor throws while reading `status` | Implemented |
| Leaderboard or streamer list fails | `FETCH_FAILED` or a console log. UI behaviour depends on the previous store value | Implemented |
| Create leaderboard image fetch fails | That card is kept as-is and the create call still proceeds | Implemented |

## Unknown / Requires Confirmation

- Whether `skmsapi-dev` is still the API, and what it validates.
- Whether the Azure static site or the Amplify redirect is the site users open.
- Whether the Isle of Man address, phone, and the two support email spellings are current.
- Whether a mobile app exists outside this repository.
- Whether supporters were supposed to use `/streamer` or only the home page. The route guard denies them.
- Payment provider beyond the Paypal label. No card or bank fields were found.
- Legal page contents were not treated as a signed policy. The footer lists About, Blog, Terms of Use, Privacy Policy, and Contact Us as text items without routes in the footer component that was read.
