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 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 20:56:54 +03:30
parent 854149bb5d
commit 872e272ee1
3 changed files with 42 additions and 9 deletions

View File

@@ -30,9 +30,15 @@ import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import ir.cafebazaar.poolakey.Connection import ir.cafebazaar.poolakey.Connection
import ir.cafebazaar.poolakey.ConnectionState
import ir.cafebazaar.poolakey.Payment import ir.cafebazaar.poolakey.Payment
import ir.cafebazaar.poolakey.config.PaymentConfiguration import ir.cafebazaar.poolakey.config.PaymentConfiguration
import ir.cafebazaar.poolakey.config.SecurityCheck 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 ir.cafebazaar.poolakey.request.PurchaseRequest
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
@@ -44,7 +50,7 @@ import org.json.JSONObject
* JS → اینجا TadrisinoAndroid.bazaarBuy(sku, orderId, dynamicPriceToken) (اشتراک) * JS → اینجا TadrisinoAndroid.bazaarBuy(sku, orderId, dynamicPriceToken) (اشتراک)
* TadrisinoAndroid.bazaarPrices(skusJson) → رویدادِ «tadrisino:bazaar-prices» * TadrisinoAndroid.bazaarPrices(skusJson) → رویدادِ «tadrisino:bazaar-prices»
* اینجا → JS window رویدادِ «tadrisino:bazaar» با { orderId, purchaseToken } یا * اینجا → JS window رویدادِ «tadrisino:bazaar» با { orderId, purchaseToken } یا
* { orderId, error: "canceled" | "failed", message } * { orderId, error: "canceled" | "failed" | "unavailable", message }
* *
* سمتِ سرور توکن را از خودِ بازار می‌سنجد (`POST /api/payment/bazaar/verify`)، پس این‌جا * سمتِ سرور توکن را از خودِ بازار می‌سنجد (`POST /api/payment/bazaar/verify`)، پس این‌جا
* فقط واسطه است و به هیچ نتیجه‌ای اعتماد نمی‌کند. * فقط واسطه است و به هیچ نتیجه‌ای اعتماد نمی‌کند.
@@ -209,6 +215,7 @@ class MainActivity : AppCompatActivity() {
val config = PaymentConfiguration( val config = PaymentConfiguration(
localSecurityCheck = SecurityCheck.Enable(rsaPublicKey = BuildConfig.BAZAAR_RSA_KEY), localSecurityCheck = SecurityCheck.Enable(rsaPublicKey = BuildConfig.BAZAAR_RSA_KEY),
) )
connection?.disconnect()
payment = Payment(context = this, config = config).also { p -> payment = Payment(context = this, config = config).also { p ->
connection = p.connect { connection = p.connect {
connectionSucceed { Log.i(TAG, "bazaar connected") } connectionSucceed { Log.i(TAG, "bazaar connected") }
@@ -225,8 +232,10 @@ class MainActivity : AppCompatActivity() {
*/ */
private fun buy(sku: String, orderId: String, dynamicPriceToken: String) { private fun buy(sku: String, orderId: String, dynamicPriceToken: String) {
val p = payment val p = payment
if (p == null) { if (p == null || connection?.getState() !is ConnectionState.Connected) {
send(orderId, error = "failed", message = "bazaar not connected") // بازار نصب نیست، هنوز وصل نشده، یا اتصال افتاده؛ برای تلاشِ بعدی دوباره وصل می‌شویم.
connectToBazaar()
send(orderId, error = "unavailable", message = "not_connected")
return return
} }
val request = PurchaseRequest( val request = PurchaseRequest(
@@ -235,11 +244,27 @@ class MainActivity : AppCompatActivity() {
p.subscribeProduct(activityResultRegistry, request) { p.subscribeProduct(activityResultRegistry, request) {
purchaseSucceed { send(orderId, token = it.purchaseToken) } purchaseSucceed { send(orderId, token = it.purchaseToken) }
purchaseCanceled { send(orderId, error = "canceled") } purchaseCanceled { send(orderId, error = "canceled") }
purchaseFailed { recoverOwned(sku, orderId, it) } purchaseFailed { failed(sku, orderId, it) }
failedToBeginFlow { recoverOwned(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) پیدا شد، توکنش برمی‌گردد تا سرور ثبتش کند. * بعدی همان سفارش را می‌فرستد؛ اگر اشتراکِ همان سفارش (payload) پیدا شد، توکنش برمی‌گردد تا سرور ثبتش کند.

View File

@@ -15,7 +15,8 @@ import { store } from './storage';
* و detail‌ِ { prices: { sku: «۸۹۸,۰۰۰ تومان» } } یا { error } * و detail‌ِ { prices: { sku: «۸۹۸,۰۰۰ تومان» } } یا { error }
* اندروید → JS رویدادِ window به نامِ «tadrisino:bazaar» با detail: * اندروید → JS رویدادِ window به نامِ «tadrisino:bazaar» با detail:
* { orderId, purchaseToken } خرید انجام شد * { 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 در آن‌ها خاموش بود، پس از * `bazaarBuy` از APKهای پس از ۲۰۲۶-۱۰-۱۲ است. APKهای قبلی کلیدِ RSA نداشتند و Poolakey در آن‌ها خاموش بود، پس از
* بازار هیچ خریدی نکرده‌اند؛ در آن‌ها کاشیِ بازار غیرفعال است و «اپ را به‌روز کن» می‌گوید. * بازار هیچ خریدی نکرده‌اند؛ در آن‌ها کاشیِ بازار غیرفعال است و «اپ را به‌روز کن» می‌گوید.
@@ -39,10 +40,10 @@ declare global {
type BridgeResult = type BridgeResult =
| { orderId: string; purchaseToken: string } | { orderId: string; purchaseToken: string }
| { orderId: string; error: 'canceled' | 'failed'; message?: string }; | { orderId: string; error: 'canceled' | 'failed' | 'unavailable'; message?: string };
export class BazaarPurchaseError extends Error { export class BazaarPurchaseError extends Error {
constructor(readonly kind: 'canceled' | 'failed' | 'timeout', message = '') { constructor(readonly kind: 'canceled' | 'failed' | 'unavailable' | 'timeout', message = '') {
super(message || kind); super(message || kind);
} }
} }

View File

@@ -266,8 +266,15 @@ function CheckoutSheet({ plan, storePrices, onClose, onPaid }: { plan: Plan | nu
const outcome = await payWithBazaar(plan); const outcome = await payWithBazaar(plan);
if (outcome) setDone(outcome); if (outcome) setDone(outcome);
} catch (err) { } catch (err) {
if (err instanceof BazaarPurchaseError && err.kind === 'unavailable') {
// کارِ کاربر است، نه خطای خرید: پولی جابه‌جا نشده.
setError(err.message === 'old_bazaar'
? 'کافه‌بازارِ گوشیت قدیمیه؛ از خودِ بازار به‌روزش کن و دوباره بزن، یا با کارتِ بانکی پرداخت کن.'
: 'کافه‌بازار روی گوشیت پیدا نشد یا وصل نشد. مطمئن شو نصبه و وارد حسابت شدی، بعد دوباره بزن؛ یا با کارتِ بانکی پرداخت کن.');
} else {
setError(friendlyError(err, 'خرید از بازار تأیید نشد. اگه پولی کم شده، دوباره «پرداخت» رو بزن تا همون خرید ثبت بشه.')); setError(friendlyError(err, 'خرید از بازار تأیید نشد. اگه پولی کم شده، دوباره «پرداخت» رو بزن تا همون خرید ثبت بشه.'));
} }
}
setBusy(false); setBusy(false);
return; return;
} }