2026-06-06 18:39:24 +03:30
|
|
|
// 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");
|
2026-06-12 08:55:17 +03:30
|
|
|
const PACKAGE = process.env.NEXT_PUBLIC_APP_PACKAGE ?? "com.bargevasat.app";
|
2026-06-06 18:39:24 +03:30
|
|
|
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 };
|
|
|
|
|
}
|