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:
@@ -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) پیدا شد، توکنش برمیگردد تا سرور ثبتش کند.
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,14 @@ 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) {
|
||||||
setError(friendlyError(err, 'خرید از بازار تأیید نشد. اگه پولی کم شده، دوباره «پرداخت» رو بزن تا همون خرید ثبت بشه.'));
|
if (err instanceof BazaarPurchaseError && err.kind === 'unavailable') {
|
||||||
|
// کارِ کاربر است، نه خطای خرید: پولی جابهجا نشده.
|
||||||
|
setError(err.message === 'old_bazaar'
|
||||||
|
? 'کافهبازارِ گوشیت قدیمیه؛ از خودِ بازار بهروزش کن و دوباره بزن، یا با کارتِ بانکی پرداخت کن.'
|
||||||
|
: 'کافهبازار روی گوشیت پیدا نشد یا وصل نشد. مطمئن شو نصبه و وارد حسابت شدی، بعد دوباره بزن؛ یا با کارتِ بانکی پرداخت کن.');
|
||||||
|
} else {
|
||||||
|
setError(friendlyError(err, 'خرید از بازار تأیید نشد. اگه پولی کم شده، دوباره «پرداخت» رو بزن تا همون خرید ثبت بشه.'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user