import { store } from './storage'; /** * پلِ پرداختِ کافه‌بازار، وقتی همین سایت داخلِ اپِ اندرویدِ تدریسینو (WebView) باز است. * * خریدِ درون‌برنامه‌ایِ بازار فقط از کدِ بومیِ اندروید (کتابخانه‌ی Poolakey) شدنی است، پس * اپِ اندروید یک شیء به نامِ `window.TadrisinoAndroid` در صفحه می‌گذارد و این فایل تنها * جایی است که با آن حرف می‌زند. در مرورگرِ معمولی آن شیء نیست، `inBazaarApp()` false است و * خرید مثلِ قبل به زرین‌پال می‌رود. * * قرارداد (سمتِ اندروید: `android/` در همین ریپو): * JS → اندروید TadrisinoAndroid.bazaarPurchase(sku, orderId) خرید را شروع کن؛ orderId همان * developerPayloadِ خرید است. * JS → اندروید TadrisinoAndroid.bazaarConsume(purchaseToken) بعد از تأییدِ سرور؛ بدونِ آن * همان بسته دوباره خریدنی نیست. * اندروید → JS رویدادِ window به نامِ «tadrisino:bazaar» با detail: * { orderId, purchaseToken } خرید انجام شد * { orderId, error: 'canceled' | 'failed', message? } * * هیچ‌کدام از این‌ها هنوز روی گوشیِ واقعی اجرا نشده (۲۰۲۶-۱۰-۰۳): اپِ اندروید ساخته نشده. */ interface AndroidBridge { bazaarPurchase?: (sku: string, orderId: string) => void; bazaarConsume?: (purchaseToken: string) => void; } declare global { interface Window { TadrisinoAndroid?: AndroidBridge; } } type BridgeResult = | { orderId: string; purchaseToken: string } | { orderId: string; error: 'canceled' | 'failed'; message?: string }; export class BazaarPurchaseError extends Error { constructor(readonly kind: 'canceled' | 'failed' | 'timeout', message = '') { super(message || kind); } } export function inBazaarApp(): boolean { return typeof window.TadrisinoAndroid?.bazaarPurchase === 'function'; } /** صفحه‌ی خریدِ بازار ممکن است طول بکشد (ورود به بازار، کارتِ بانکی)؛ این سقف فقط جلوی گیر کردنِ همیشگی را می‌گیرد. */ const PURCHASE_TIMEOUT_MS = 10 * 60 * 1000; /** خرید را به اپِ اندروید می‌سپارد و purchaseToken را برمی‌گرداند. */ export function bazaarPurchase(sku: string, orderId: string): Promise { const bridge = window.TadrisinoAndroid; if (!bridge?.bazaarPurchase) return Promise.reject(new BazaarPurchaseError('failed', 'not in the Android app')); return new Promise((resolve, reject) => { const timer = window.setTimeout(() => finish(() => reject(new BazaarPurchaseError('timeout'))), PURCHASE_TIMEOUT_MS); function finish(settle: () => void) { window.clearTimeout(timer); window.removeEventListener('tadrisino:bazaar', onResult); settle(); } function onResult(event: Event) { const detail = (event as CustomEvent).detail; if (!detail || detail.orderId !== orderId) return; if ('purchaseToken' in detail && detail.purchaseToken) finish(() => resolve(detail.purchaseToken)); else if ('error' in detail) finish(() => reject(new BazaarPurchaseError(detail.error, detail.message))); } window.addEventListener('tadrisino:bazaar', onResult); try { bridge.bazaarPurchase!(sku, orderId); } catch (err) { finish(() => reject(new BazaarPurchaseError('failed', String(err)))); } }); } export function bazaarConsume(purchaseToken: string) { try { window.TadrisinoAndroid?.bazaarConsume?.(purchaseToken); } catch (err) { // مصرف نشدن یعنی خریدِ دوباره‌ی همین بسته تا مصرفِ بعدی گیر می‌کند؛ اعتبارِ کاربر سالم است. console.error('bazaarConsume failed', err); } } /** * Idempotency-Keyِ خریدِ نیمه‌تمامِ هر SKU. اگر بازار پول را گرفت ولی تأییدِ سرور نرسید (قطعِ * اینترنت)، تلاشِ بعدی باید همان سفارش را بگیرد؛ سفارشِ تازه با توکنِ قبلی تأیید نمی‌شد چون هر * توکن فقط به یک سفارش می‌خورد. */ const pendingKey = (sku: string) => `tadrisino.bazaarPending.${sku}`; export const bazaarPending = { get: (sku: string) => store.get(pendingKey(sku)), set: (sku: string, key: string) => store.set(pendingKey(sku), key), clear: (sku: string) => store.remove(pendingKey(sku)), };