Subscriptions only: no question credits anywhere; Bazaar bridge is subscribeProduct only (bazaarBuy(sku, orderId, token), bazaarPrices(skusJson)), no consume

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 20:33:47 +03:30
parent e0a9605b8e
commit 854149bb5d
7 changed files with 98 additions and 180 deletions

View File

@@ -8,12 +8,11 @@ import { store } from './storage';
* جایی است که با آن حرف می‌زند. در مرورگرِ معمولی آن شیء نیست و خرید با زرین‌پال است.
*
* قرارداد (سمتِ اندروید: `android/` در همین ریپو):
* JS → اندروید TadrisinoAndroid.bazaarBuy(sku, orderId, productType, dynamicPriceToken)
* productType «inapp» (بسته، purchaseProduct) یا «subscription» (subscribeProduct)؛
* orderId همان payloadِ خرید است؛ dynamicPriceToken رشته‌ی خالی یعنی قیمتِ پیشخان.
* JS → اندروید TadrisinoAndroid.bazaarConsume(purchaseToken) فقط بسته، بعد از تأییدِ سرور.
* JS → اندروید TadrisinoAndroid.bazaarPrices(skusJson, productType) قیمتِ پیشخان؛ جواب با رویدادِ
* «tadrisino:bazaar-prices» و detail‌ِ { productType, prices: { sku: «۷۹۸,۰۰۰ تومان» } } یا { productType, error }
* JS → اندروید TadrisinoAndroid.bazaarBuy(sku, orderId, dynamicPriceToken)
* خریدِ اشتراک با subscribeProduct (فقط اشتراک فروخته می‌شود)؛ orderId همان payloadِ خرید است؛
* dynamicPriceToken رشته‌ی خالی یعنی قیمتِ پیشخان.
* JS → اندروید TadrisinoAndroid.bazaarPrices(skusJson) قیمتِ پیشخان؛ جواب با رویدادِ «tadrisino:bazaar-prices»
* و detail‌ِ { prices: { sku: «۸۹۸,۰۰۰ تومان» } } یا { error }
* اندروید → JS رویدادِ window به نامِ «tadrisino:bazaar» با detail:
* { orderId, purchaseToken } خرید انجام شد
* { orderId, error: 'canceled' | 'failed', message? }
@@ -23,14 +22,11 @@ import { store } from './storage';
* هیچ‌کدام از این‌ها هنوز روی گوشیِ واقعی اجرا نشده (۲۰۲۶-۱۰-۱۲).
*/
export type BazaarProductType = 'inapp' | 'subscription';
interface AndroidBridge {
bazaarBuy?: (sku: string, orderId: string, productType: BazaarProductType, dynamicPriceToken: string) => void;
bazaarBuy?: (sku: string, orderId: string, dynamicPriceToken: string) => void;
/** APKِ قدیمی؛ فقط برای این‌که بدانیم داخلِ اپیم ولی خرید از بازار در آن ممکن نیست. */
bazaarPurchase?: (sku: string, orderId: string) => void;
bazaarConsume?: (purchaseToken: string) => void;
bazaarPrices?: (skusJson: string, productType: BazaarProductType) => void;
bazaarPrices?: (skusJson: string) => void;
/** اولین صفحه‌ی واقعی کشیده شد؛ اسپلشِ سیستم می‌تواند برود (APKهای قدیمی‌تر این را ندارند). */
appReady?: () => void;
}
@@ -56,7 +52,7 @@ export function inAndroidApp(): boolean {
return typeof window.TadrisinoAndroid === 'object' && window.TadrisinoAndroid !== null;
}
/** اپی که می‌تواند از بازار بخرد (بسته و اشتراک). */
/** اپی که می‌تواند از بازار اشتراک بخرد. */
export function canBuyFromBazaar(): boolean {
return typeof window.TadrisinoAndroid?.bazaarBuy === 'function';
}
@@ -65,7 +61,7 @@ export function canBuyFromBazaar(): boolean {
const PURCHASE_TIMEOUT_MS = 10 * 60 * 1000;
/** خرید را به اپِ اندروید می‌سپارد و purchaseToken را برمی‌گرداند. */
export function bazaarBuy(sku: string, orderId: string, productType: BazaarProductType, dynamicPriceToken: string | null): Promise<string> {
export function bazaarBuy(sku: string, orderId: string, dynamicPriceToken: string | null): Promise<string> {
const bridge = window.TadrisinoAndroid;
if (!bridge?.bazaarBuy) return Promise.reject(new BazaarPurchaseError('failed', 'not in the Android app'));
return new Promise((resolve, reject) => {
@@ -83,16 +79,16 @@ export function bazaarBuy(sku: string, orderId: string, productType: BazaarProdu
}
window.addEventListener('tadrisino:bazaar', onResult);
try {
bridge.bazaarBuy!(sku, orderId, productType, dynamicPriceToken ?? '');
bridge.bazaarBuy!(sku, orderId, dynamicPriceToken ?? '');
} catch (err) {
finish(() => reject(new BazaarPurchaseError('failed', String(err))));
}
});
}
type PricesResult = { productType: BazaarProductType; prices?: Record<string, string>; error?: string };
type PricesResult = { prices?: Record<string, string>; error?: string };
function askPrices(skus: string[], productType: BazaarProductType): Promise<Record<string, string>> {
function askPrices(skus: string[]): Promise<Record<string, string>> {
const bridge = window.TadrisinoAndroid;
if (!bridge?.bazaarPrices || !skus.length) return Promise.resolve({});
return new Promise((resolve, reject) => {
@@ -104,12 +100,12 @@ function askPrices(skus: string[], productType: BazaarProductType): Promise<Reco
}
function onResult(event: Event) {
const detail = (event as CustomEvent<PricesResult>).detail;
if (!detail || detail.productType !== productType) return;
if (!detail) return;
if (detail.prices) done(() => resolve(detail.prices!));
else done(() => reject(new Error(detail.error || 'failed')));
}
window.addEventListener('tadrisino:bazaar-prices', onResult);
bridge.bazaarPrices!(JSON.stringify(skus), productType);
bridge.bazaarPrices!(JSON.stringify(skus));
});
}
@@ -117,13 +113,13 @@ function askPrices(skus: string[], productType: BazaarProductType): Promise<Reco
* قیمتِ ثبت‌شده در پیشخانِ بازار برای هر SKU، از خودِ بازار روی گوشی (API سرور قیمت نمی‌دهد). اتصالِ Poolakey
* چند لحظه بعد از باز شدنِ اپ برقرار می‌شود، پس یک بار دوباره امتحان می‌شود. خطا یعنی قیمتی نشان داده نمی‌شود.
*/
export async function bazaarPrices(skus: string[], productType: BazaarProductType): Promise<Record<string, string>> {
export async function bazaarPrices(skus: string[]): Promise<Record<string, string>> {
try {
return await askPrices(skus, productType);
return await askPrices(skus);
} catch {
await new Promise((r) => setTimeout(r, 1500));
try {
return await askPrices(skus, productType);
return await askPrices(skus);
} catch (err) {
console.error('bazaarPrices failed', err);
return {};
@@ -131,15 +127,6 @@ export async function bazaarPrices(skus: string[], productType: BazaarProductTyp
}
}
export function bazaarConsume(purchaseToken: string) {
try {
window.TadrisinoAndroid?.bazaarConsume?.(purchaseToken);
} catch (err) {
// مصرف نشدن یعنی خریدِ دوباره‌ی همین بسته تا مصرفِ بعدی گیر می‌کند؛ اعتبارِ کاربر سالم است.
console.error('bazaarConsume failed', err);
}
}
/**
* Idempotency-Keyِ خریدِ نیمه‌تمامِ هر SKU. اگر بازار پول را گرفت ولی تأییدِ سرور نرسید (قطعِ
* اینترنت)، تلاشِ بعدی باید همان سفارش را بگیرد؛ سفارشِ تازه با توکنِ قبلی تأیید نمی‌شد چون هر