feat: UNO-style table, social hub, cosmetics, speed mode, store IAB
Game table & play - UNO-style restyle: suit-aware bolder cards (+xl size), pulsing playable glow, big "YOUR TURN" pill, active-seat ring, trick-win particle burst, round confetti, match coin-rain. - Per-league turn time via turnMsForStake: 15s starter/AI, 10s pro, 7s expert; mirrored server-side in GameRoom.TurnMs. - Speed (Blitz) mode for vs-AI/private: 5s turns, race to 5, ~halved pacing. - Matchmaking waits ~15s (randomized 12-18s) then fills bots; elapsed timer + hint. Rewards / gifts - Richer post-match modal (floating coins, XP bar), celebration overlay reveals the unlocked sticker pack, boosted daily rewards (client+server synced), themed 7-day daily with special day-7. Social - Public profile modal (identity, stats, achievement board) from leaderboard / friends / discover / end-of-game roster; rate-limited add-friend (10/hour). - Social hub: Friends / Discover (player search + suggestions) / Messages inbox. - Profile gender (shown in finder/profile) + social links with public/friends/ hidden visibility, enforced server-side. Cosmetics - Distinct card backs: per-design pattern families (stripes/argyle/grid/dots/ rays/scales/crosshatch/royal/filigree/gem) + luxury motifs (lib/cardBack.ts), consistent on table/shop/profile; +Peacock/Rose-Gold backs. - Purchasable titles (shop Titles section); title shown under the seat on the table and in discover/public profile. - 10 new sticker packs (banter/kol-kol, Persian trends, court cards, moods). - Persistent level+XP bar on Home and every inner screen. Payments - Buy-coins gateway opens in a new tab (no SPA dead-end) + focus refresh. - Store IAB scaffolding: Cafe Bazaar deep-link purchase + redirect-token capture, Myket native-bridge contract, server-side IabService.Verify for both stores, config-driven via Iab__* env. POST /api/coins/iab/verify (JWT). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
// Store in-app billing (Cafe Bazaar / Myket) for coin packs.
|
||||
//
|
||||
// - **Bazaar** (embedded PWA): pure deep-link flow. We navigate to
|
||||
// `bazaar://in_app?...&sku=...&redirect_url=...`; Bazaar processes payment and
|
||||
// reopens the app at redirect_url with `?purchaseToken=...`. We stash the SKU
|
||||
// first, then on return POST the token to `/api/coins/iab/verify`.
|
||||
// - **Myket**: native AIDL billing. A Capacitor plugin must inject
|
||||
// `window.MyketBilling` (see ANDROID.md). We call `.purchase(sku)` and POST the
|
||||
// returned token to verify. Without the bridge, Myket is "unavailable".
|
||||
//
|
||||
// The active store is the build flavor `NEXT_PUBLIC_STORE` (bazaar|myket|web),
|
||||
// overridden at runtime if the Myket native bridge is present.
|
||||
|
||||
import { CoinPack } from "./online/types";
|
||||
|
||||
export type StoreId = "bazaar" | "myket" | "web";
|
||||
|
||||
const ENV_STORE = ((process.env.NEXT_PUBLIC_STORE as StoreId | undefined) ?? "web");
|
||||
const PACKAGE = process.env.NEXT_PUBLIC_APP_PACKAGE ?? "com.bargevasat.hokm";
|
||||
const PENDING_SKU_KEY = "iab_pending_sku";
|
||||
|
||||
/** Native bridge contract a Myket Capacitor plugin must fulfil. */
|
||||
interface MyketBridge {
|
||||
available?: boolean;
|
||||
purchase: (sku: string) => Promise<{ purchaseToken: string; productId?: string }>;
|
||||
consume?: (token: string) => Promise<void>;
|
||||
}
|
||||
declare global {
|
||||
interface Window {
|
||||
MyketBilling?: MyketBridge;
|
||||
}
|
||||
}
|
||||
|
||||
export function getStore(): StoreId {
|
||||
if (typeof window !== "undefined" && window.MyketBilling?.available) return "myket";
|
||||
return ENV_STORE;
|
||||
}
|
||||
|
||||
/** True when coin purchases should go through a store (not the web ZarinPal gateway). */
|
||||
export function isStoreBilling(): boolean {
|
||||
return getStore() !== "web";
|
||||
}
|
||||
|
||||
function skuFor(pack: CoinPack): string {
|
||||
return pack.sku ?? pack.id;
|
||||
}
|
||||
|
||||
export type PurchaseStart =
|
||||
| { kind: "redirect" } // Bazaar — the app navigated away; result arrives on return
|
||||
| { kind: "token"; store: StoreId; productId: string; token: string } // Myket — verify now
|
||||
| { kind: "unavailable" };
|
||||
|
||||
/** Begin a store purchase for a coin pack. */
|
||||
export async function purchaseViaStore(pack: CoinPack): Promise<PurchaseStart> {
|
||||
const store = getStore();
|
||||
const sku = skuFor(pack);
|
||||
|
||||
if (store === "bazaar") {
|
||||
try {
|
||||
localStorage.setItem(PENDING_SKU_KEY, sku);
|
||||
} catch {
|
||||
/* ignore storage errors */
|
||||
}
|
||||
const redirect = encodeURIComponent(window.location.origin + window.location.pathname);
|
||||
window.location.href =
|
||||
`bazaar://in_app?package_name=${encodeURIComponent(PACKAGE)}` +
|
||||
`&sku=${encodeURIComponent(sku)}&redirect_url=${redirect}`;
|
||||
return { kind: "redirect" };
|
||||
}
|
||||
|
||||
if (store === "myket" && window.MyketBilling) {
|
||||
const res = await window.MyketBilling.purchase(sku);
|
||||
return { kind: "token", store: "myket", productId: res.productId ?? sku, token: res.purchaseToken };
|
||||
}
|
||||
|
||||
return { kind: "unavailable" };
|
||||
}
|
||||
|
||||
/**
|
||||
* On app load, capture a Bazaar redirect (`?purchaseToken=...`). Returns the
|
||||
* pending purchase to verify, or null. Also clears the stashed SKU.
|
||||
*/
|
||||
export function captureBazaarRedirect(): { store: StoreId; productId: string; token: string } | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get("purchaseToken");
|
||||
if (!token) return null;
|
||||
let productId = params.get("sku") ?? params.get("productId") ?? "";
|
||||
if (!productId) {
|
||||
try {
|
||||
productId = localStorage.getItem(PENDING_SKU_KEY) ?? "";
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem(PENDING_SKU_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return { store: "bazaar", productId, token };
|
||||
}
|
||||
Reference in New Issue
Block a user