forked from Tadrisino/MathApp_frontend
- muted text #9e9ec0 -> #727299 (contrast 2.6 -> 4.5 on white); tab labels 11px - map: chapter titles wrap to two lines instead of being cut off - reduced-motion respected (MotionConfig + CSS), focus-visible ring, no pull-to-refresh, short vibration on correct/wrong answers - src/lib/bazaar.ts: bridge to the Android WebView wrapper; checkout uses Bazaar IAP only inside that app, Zarinpal everywhere else (unchanged) - android/: WebView wrapper with Poolakey; written but never built Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
4.8 KiB
TypeScript
97 lines
4.8 KiB
TypeScript
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<string> {
|
||
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<BridgeResult>).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)),
|
||
};
|