"use client"; import { Coins } from "lucide-react"; import { useEffect, useState } from "react"; import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader"; import { useSessionStore } from "@/lib/session-store"; import { useI18n } from "@/lib/i18n"; import { getService } from "@/lib/online/service"; import { consumeStorePurchase, isPurchaseDisabled, isStoreBilling, purchaseViaStore } from "@/lib/storeBilling"; import { sound } from "@/lib/sound"; import { CoinPack } from "@/lib/online/types"; import { cn } from "@/lib/cn"; export function BuyCoinsScreen() { const { t, locale } = useI18n(); const profile = useSessionStore((s) => s.profile); const setProfile = useSessionStore((s) => s.setProfile); const refreshProfile = useSessionStore((s) => s.refreshProfile); const [packs, setPacks] = useState([]); const [busy, setBusy] = useState(null); const [gained, setGained] = useState(null); const [msg, setMsg] = useState(""); useEffect(() => { getService().getCoinPacks().then(setPacks); }, []); // When the user returns from the payment tab, pull the (possibly credited) balance. useEffect(() => { const onFocus = () => refreshProfile(); window.addEventListener("focus", onFocus); return () => window.removeEventListener("focus", onFocus); }, [refreshProfile]); const fmt = (n: number) => new Intl.NumberFormat(locale === "fa" ? "fa-IR" : "en-US").format(n); const buy = async (p: CoinPack) => { setBusy(p.id); setMsg(""); // Google Play build: coin purchases are intentionally not wired (no Iranian // IAB on Play). Show a notice instead of starting any payment flow. if (isPurchaseDisabled()) { setMsg(t("buy.notImplemented")); setBusy(null); return; } // Inside a store build (Cafe Bazaar / Myket), route through store billing. if (isStoreBilling()) { try { const r = await purchaseViaStore(p); if (r.kind === "redirect") return; // Bazaar navigated away; credited on return if (r.kind === "token") { const v = await getService().verifyIab(r.store, r.productId, r.token); if (v.ok && v.profile) { // Consumable: let the store mark it consumed so it can be re-bought. await consumeStorePurchase(r.store, r.token); setProfile(v.profile); sound.play("purchase"); setGained(v.coins); setTimeout(() => setGained(null), 2500); } else { setMsg(t("buy.failed")); } setBusy(null); return; } // unavailable → fall through to the web gateway below } catch { setBusy(null); setMsg(t("buy.failed")); return; } } let res; try { res = await getService().buyCoins(p.id); } catch { setBusy(null); setMsg(t("buy.failed")); return; } // Live: hand off to the ZarinPal gateway. Open it in a NEW tab so the app // itself never navigates away (and so a slow/blocked gateway can't dead-end // the whole app). Credit lands via the server callback; we refresh on focus. if (res.redirectUrl) { const url = res.redirectUrl; if (!/^https?:\/\//i.test(url)) { setBusy(null); setMsg(t("buy.failed")); return; } const win = window.open(url, "_blank", "noopener,noreferrer"); if (!win) window.location.href = url; // popup blocked → same-tab fallback setBusy(null); setMsg(t("buy.redirecting")); return; } // Mock/offline: instant credit. if (res.ok && res.profile) { setProfile(res.profile); sound.play("purchase"); setGained(res.coins); setTimeout(() => setGained(null), 2500); } else { setMsg(t("buy.failed")); } setBusy(null); }; return ( {fmt(profile.coins)} ) } /> {gained != null && (
+{fmt(gained)}
)} {msg && (
{msg}
)}
{packs.map((p) => ( ))}
); }