From 872e272ee1922350906a46cb792eaeee0c7da658 Mon Sep 17 00:00:00 2001 From: AliReza Date: Mon, 12 Oct 2026 20:56:54 +0330 Subject: [PATCH] Bazaar: clear message when Bazaar is missing, old or not connected; reconnect before the next try Android: buy() checks the Poolakey connection state and reconnects; BazaarNotFound / not supported / dynamic price not supported become error "unavailable" (not_installed | old_bazaar | not_connected) instead of a generic failure. assembleDebug green: com.tadrisino.math.app, versionCode 2, queries com.farsitel.bazaar. Not run on a phone. Co-Authored-By: Claude Opus 5.5 --- .../java/com/tadrisino/app/MainActivity.kt | 35 ++++++++++++++++--- src/lib/bazaar.ts | 7 ++-- src/screens/SubscriptionScreen.tsx | 9 ++++- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/java/com/tadrisino/app/MainActivity.kt b/android/app/src/main/java/com/tadrisino/app/MainActivity.kt index a4ecdd4..327d9bd 100644 --- a/android/app/src/main/java/com/tadrisino/app/MainActivity.kt +++ b/android/app/src/main/java/com/tadrisino/app/MainActivity.kt @@ -30,9 +30,15 @@ import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import ir.cafebazaar.poolakey.Connection +import ir.cafebazaar.poolakey.ConnectionState import ir.cafebazaar.poolakey.Payment import ir.cafebazaar.poolakey.config.PaymentConfiguration import ir.cafebazaar.poolakey.config.SecurityCheck +import ir.cafebazaar.poolakey.exception.BazaarNotFoundException +import ir.cafebazaar.poolakey.exception.BazaarNotSupportedException +import ir.cafebazaar.poolakey.exception.DynamicPriceNotSupportedException +import ir.cafebazaar.poolakey.exception.IAPNotSupportedException +import ir.cafebazaar.poolakey.exception.SubsNotSupportedException import ir.cafebazaar.poolakey.request.PurchaseRequest import org.json.JSONArray import org.json.JSONObject @@ -44,7 +50,7 @@ import org.json.JSONObject * JS → اینجا TadrisinoAndroid.bazaarBuy(sku, orderId, dynamicPriceToken) (اشتراک) * TadrisinoAndroid.bazaarPrices(skusJson) → رویدادِ «tadrisino:bazaar-prices» * اینجا → JS window رویدادِ «tadrisino:bazaar» با { orderId, purchaseToken } یا - * { orderId, error: "canceled" | "failed", message } + * { orderId, error: "canceled" | "failed" | "unavailable", message } * * سمتِ سرور توکن را از خودِ بازار می‌سنجد (`POST /api/payment/bazaar/verify`)، پس این‌جا * فقط واسطه است و به هیچ نتیجه‌ای اعتماد نمی‌کند. @@ -209,6 +215,7 @@ class MainActivity : AppCompatActivity() { val config = PaymentConfiguration( localSecurityCheck = SecurityCheck.Enable(rsaPublicKey = BuildConfig.BAZAAR_RSA_KEY), ) + connection?.disconnect() payment = Payment(context = this, config = config).also { p -> connection = p.connect { connectionSucceed { Log.i(TAG, "bazaar connected") } @@ -225,8 +232,10 @@ class MainActivity : AppCompatActivity() { */ private fun buy(sku: String, orderId: String, dynamicPriceToken: String) { val p = payment - if (p == null) { - send(orderId, error = "failed", message = "bazaar not connected") + if (p == null || connection?.getState() !is ConnectionState.Connected) { + // بازار نصب نیست، هنوز وصل نشده، یا اتصال افتاده؛ برای تلاشِ بعدی دوباره وصل می‌شویم. + connectToBazaar() + send(orderId, error = "unavailable", message = "not_connected") return } val request = PurchaseRequest( @@ -235,11 +244,27 @@ class MainActivity : AppCompatActivity() { p.subscribeProduct(activityResultRegistry, request) { purchaseSucceed { send(orderId, token = it.purchaseToken) } purchaseCanceled { send(orderId, error = "canceled") } - purchaseFailed { recoverOwned(sku, orderId, it) } - failedToBeginFlow { recoverOwned(sku, orderId, it) } + purchaseFailed { failed(sku, orderId, it) } + failedToBeginFlow { failed(sku, orderId, it) } } } + /** + * بازارِ نصب‌نشده یا قدیمی (بی پشتیبانیِ اشتراک یا «تخفیف پویا») کاری است که کاربر باید بکند، نه خطای خرید؛ + * فرانت برایش پیامِ جدا دارد. بقیه‌ی خطاها ممکن است خریدی باشند که پولش رفته، پس از بازار پرسیده می‌شوند. + */ + private fun failed(sku: String, orderId: String, cause: Throwable) { + val reason = when (cause) { + is BazaarNotFoundException -> "not_installed" + is BazaarNotSupportedException, is SubsNotSupportedException, is IAPNotSupportedException, + is DynamicPriceNotSupportedException -> "old_bazaar" + else -> null + } + Log.e(TAG, "bazaar purchase failed: ${reason ?: cause.javaClass.simpleName}", cause) + if (reason != null) send(orderId, error = "unavailable", message = reason) + else recoverOwned(sku, orderId, cause) + } + /** * اشتراکِ همین محصول که پولش رفته ولی تأییدِ سرور نرسیده بود جلوی خریدِ دوباره را می‌گیرد. فرانت در تلاشِ * بعدی همان سفارش را می‌فرستد؛ اگر اشتراکِ همان سفارش (payload) پیدا شد، توکنش برمی‌گردد تا سرور ثبتش کند. diff --git a/src/lib/bazaar.ts b/src/lib/bazaar.ts index 8530d62..7758e22 100644 --- a/src/lib/bazaar.ts +++ b/src/lib/bazaar.ts @@ -15,7 +15,8 @@ import { store } from './storage'; * و detail‌ِ { prices: { sku: «۸۹۸,۰۰۰ تومان» } } یا { error } * اندروید → JS رویدادِ window به نامِ «tadrisino:bazaar» با detail: * { orderId, purchaseToken } خرید انجام شد - * { orderId, error: 'canceled' | 'failed', message? } + * { orderId, error: 'canceled' | 'failed' | 'unavailable', message? } + * («unavailable»: بازار نصب نیست/وصل نشد یا قدیمی است؛ message: not_installed | old_bazaar | not_connected) * * `bazaarBuy` از APKهای پس از ۲۰۲۶-۱۰-۱۲ است. APKهای قبلی کلیدِ RSA نداشتند و Poolakey در آن‌ها خاموش بود، پس از * بازار هیچ خریدی نکرده‌اند؛ در آن‌ها کاشیِ بازار غیرفعال است و «اپ را به‌روز کن» می‌گوید. @@ -39,10 +40,10 @@ declare global { type BridgeResult = | { orderId: string; purchaseToken: string } - | { orderId: string; error: 'canceled' | 'failed'; message?: string }; + | { orderId: string; error: 'canceled' | 'failed' | 'unavailable'; message?: string }; export class BazaarPurchaseError extends Error { - constructor(readonly kind: 'canceled' | 'failed' | 'timeout', message = '') { + constructor(readonly kind: 'canceled' | 'failed' | 'unavailable' | 'timeout', message = '') { super(message || kind); } } diff --git a/src/screens/SubscriptionScreen.tsx b/src/screens/SubscriptionScreen.tsx index 4ecd1be..7bfc474 100644 --- a/src/screens/SubscriptionScreen.tsx +++ b/src/screens/SubscriptionScreen.tsx @@ -266,7 +266,14 @@ function CheckoutSheet({ plan, storePrices, onClose, onPaid }: { plan: Plan | nu const outcome = await payWithBazaar(plan); if (outcome) setDone(outcome); } catch (err) { - setError(friendlyError(err, 'خرید از بازار تأیید نشد. اگه پولی کم شده، دوباره «پرداخت» رو بزن تا همون خرید ثبت بشه.')); + if (err instanceof BazaarPurchaseError && err.kind === 'unavailable') { + // کارِ کاربر است، نه خطای خرید: پولی جابه‌جا نشده. + setError(err.message === 'old_bazaar' + ? 'کافه‌بازارِ گوشیت قدیمیه؛ از خودِ بازار به‌روزش کن و دوباره بزن، یا با کارتِ بانکی پرداخت کن.' + : 'کافه‌بازار روی گوشیت پیدا نشد یا وصل نشد. مطمئن شو نصبه و وارد حسابت شدی، بعد دوباره بزن؛ یا با کارتِ بانکی پرداخت کن.'); + } else { + setError(friendlyError(err, 'خرید از بازار تأیید نشد. اگه پولی کم شده، دوباره «پرداخت» رو بزن تا همون خرید ثبت بشه.')); + } } setBusy(false); return;