mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-21 07:25:56 +00:00
feat(rewards): claimable subscription rewards UI (#4566)
## Summary Subscription perks (signup bonus + daily soft/hard currency) now land as **unclaimed rewards** the player must explicitly claim, instead of crediting balances directly (API side: openfrontio/infra#409). This PR adds the client UI: - **`RewardsPanel`** — shared Lit element listing pending rewards (currency icon, `+amount`, server `note` with translated fallbacks for known reasons), with per-reward **Claim** and **Claim All**. Both claim endpoints return post-claim balances, so the wallet updates without re-fetching `/users/@me`. - **Account modal** — renders the panel above the subscription panel (rewards survive subscription lapse, so it doesn't depend on an active sub). - **`RewardsModal`** — popup at login when rewards are pending. Reuses `RewardsPanel`, auto-closes when the last reward is claimed. Only opens on a clean homepage load (`pathname === "/"`, empty hash) so it never pops over join links, `#modal=...` deep links, or the Stripe `#purchase-completed` return — that flow reloads clean, so the signup bonus popup appears right after purchase. - **Schemas/API** — `RewardSchema` + `rewards[]` on `/users/@me` (optional for older API versions), `claimReward` / `claimAllRewards` in `Api.ts`. Reward `id`/`amount` stay opaque bigint strings; a 404 on single claim means "already claimed elsewhere" (double-click / second device) and re-syncs instead of erroring. ## Screenshots Login popup (also embedded in the account modal): - Panel: currency icon + amount + note per row, Claim / Claim All buttons - Amounts formatted via `BigInt` (can exceed `Number.MAX_SAFE_INTEGER`) ## Test plan - [x] `npm test` — 161 tests pass, including new coverage for `RewardSchema`, `/users/@me` with/without `rewards`, and both claim response shapes - [x] `tsc --noEmit`, ESLint, Prettier clean - [x] Verified in the running app (Playwright): panel renders in the account-modal style; single claim against a stubbed endpoint credits balances and removes the row; Claim All empties the list and auto-closes the popup; body scroll restored 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,7 @@ import { modalRouter } from "./ModalRouter";
|
||||
import { initNavigation } from "./Navigation";
|
||||
import "./NewsModal";
|
||||
import "./PatternInput";
|
||||
import { RewardsModal } from "./RewardsModal";
|
||||
import "./SinglePlayerModal";
|
||||
import { StoreModal } from "./Store";
|
||||
import "./TerritoryPatternsModal";
|
||||
@@ -266,6 +267,7 @@ class Client {
|
||||
private storeModal: StoreModal;
|
||||
private tokenLoginModal: TokenLoginModal;
|
||||
private matchmakingModal: MatchmakingModal;
|
||||
private rewardsModal: RewardsModal;
|
||||
private mostRecentJoinEvent: number;
|
||||
|
||||
private turnstileTokenPromise: Promise<{
|
||||
@@ -505,6 +507,11 @@ class Client {
|
||||
console.warn("Matchmaking modal element not found");
|
||||
}
|
||||
|
||||
this.rewardsModal = document.querySelector("rewards-modal") as RewardsModal;
|
||||
if (!this.rewardsModal || !(this.rewardsModal instanceof RewardsModal)) {
|
||||
console.warn("Rewards modal element not found");
|
||||
}
|
||||
|
||||
const onUserMe = async (userMeResponse: UserMeResponse | false) => {
|
||||
if (crazyGamesSDK.isOnCrazyGames()) {
|
||||
void updateCrazyGamesNavButton();
|
||||
@@ -528,6 +535,17 @@ class Client {
|
||||
`Your player ID is ${userMeResponse.player.publicId}\n` +
|
||||
"Sharing this ID will allow others to view your game history and stats.",
|
||||
);
|
||||
|
||||
// Unclaimed-rewards popup — only on a clean homepage load, never over
|
||||
// a deep link (join URL, #modal=..., #purchase-completed, ...).
|
||||
const rewards = userMeResponse.player.rewards ?? [];
|
||||
if (
|
||||
rewards.length > 0 &&
|
||||
window.location.pathname === "/" &&
|
||||
window.location.hash === ""
|
||||
) {
|
||||
this.rewardsModal?.openWithRewards(rewards);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user