Checkout: Zarinpal is the default; Bazaar tile shows the price Bazaar itself has (Poolakey getSkuDetails bridge)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 18:21:48 +03:30
parent fffdeac3ed
commit d440986b88
4 changed files with 115 additions and 11 deletions

View File

@@ -21,7 +21,7 @@
| `mediaPlaybackRequiresUserGesture = false` | صداهای رابط بعد از پاسخِ سرور پخش می‌شوند، نه در لحظه‌ی لمس | صدای جوابِ درست/غلط و پایانِ تمرین |
| `isPaymentPage` + `paysInApp()` | درگاهِ زرین‌پال داخلِ خودِ اپ باز می‌شود. اپِ فعلی آن را به مرورگرِ گوشی می‌دهد که Referer نمی‌فرستد و زرین‌پال بی‌Referer «دسترسی از این دامنه مجاز نمی‌باشد» می‌دهد؛ فعلاً فرانت از راهِ `pay.html` (با `http://`) دورش می‌زند | «پرداخت» با کارتِ بانکی: صفحه‌ی زرین‌پال و بانک داخلِ اپ، و بعد از پرداخت برگشت به اپ با پیامِ «پرداخت موفق بود» (PaymentResult). دکمه‌ی برگشت وسطِ صفحه‌ی بانک چه می‌کند را هم ببین |
| اسپلشِ یک‌مرحله‌ای: `installSplashScreen()` + `setKeepOnScreenCondition` + `TadrisinoAndroid.appReady()` (`446cf36`، `ab02bb4`) | اپِ فعلی سه تکه می‌شود: اسپلشِ اندروید ← WebViewِ سفیدِ خالی ← اسپلشِ وب (آیکون + نام) ← خانه. با APKِ تازه اسپلشِ بومی تا اولین صفحه‌ی واقعیِ وب می‌ماند و مستقیم می‌رود روی همان صفحه؛ اسپلشِ وب داخلِ اپ نامرئی است (در APKِ قدیمی فقط آیکونِ بی‌نام). فرانت `appReady` را بعد از اولین صفحه‌ی غیر‌boot صدا می‌زند و APKِ قدیمی آن را ندارد، پس بی‌اثر است | روی گوشی: باز کردنِ اپ با اینترنت باید آیکون ← خانه باشد، بی‌سفیدی و بی‌تصویرِ دوم؛ اندازه‌ی آیکون روی اندروید ۱۲+ (`drawable/splash_icon.xml`: ۲۸۸dp، حاشیه ۲۲٪) |
| خریدِ کافه‌بازار: کلیدِ RSA، `bazaarBuy` با اشتراک (`subscribeProduct`) و بسته، «تخفیف پویا»؛ `versionCode` ۲ | بی‌این‌ها از بازار هیچ خریدی ممکن نیست (APKهای قبلی کلید نداشتند) | داخلِ اپ ← فروشگاه ← اشتراکِ سه‌ماهه: کاشیِ «کافه‌بازار» فعال و پیش‌فرض باشد؛ خرید با حسابِ تست، برگشت با «اشتراکت فعاله»؛ در `/admin/payment/order/` سفارشِ بازار «پرداخت‌شده». یک بار هم «انصراف» در صفحه‌ی بازار |
| خریدِ کافه‌بازار: کلیدِ RSA، `bazaarBuy` با اشتراک (`subscribeProduct`) و بسته، «تخفیف پویا»؛ `versionCode` ۲ | بی‌این‌ها از بازار هیچ خریدی ممکن نیست (APKهای قبلی کلید نداشتند) | داخلِ اپ ← فروشگاه ← اشتراکِ سه‌ماهه: «کارتِ بانکی» پیش‌فرض، کاشیِ «کافه‌بازار» فعال با قیمتِ بازار زیرِ آن؛ خرید با حسابِ تست، برگشت با «اشتراکت فعاله»؛ در `/admin/payment/order/` سفارشِ بازار «پرداخت‌شده». یک بار هم «انصراف» در صفحه‌ی بازار |
| صفحه‌ی بی‌اتصال: `onReceivedError`/`onReceivedHttpError` ← همان تصویر + «اینترنت قطع است / کند است؛ در حال اتصال…»، تلاشِ دوباره هر ۵ ثانیه، و مجوزِ `ACCESS_NETWORK_STATE` | کاربر هرگز صفحه‌ی خطای WebView یا متنِ فنی نمی‌بیند. اتصالِ کند (بی‌خطا) عمداً هیچ متنِ اضافه نمی‌آورد و روی اسپلش می‌ماند | هواپیما روشن ← باز کردنِ اپ ← خاموش کردن: باید خودش وصل شود. سرورِ ۵۰۲ (مثلاً nginx خاموش) هم همین صفحه. اگر صفحه لود شد ولی ۱۵ ثانیه `appReady` نیامد، لاگِ ERROR و رهاشدنِ اسپلش (باگِ JS) |
**اسپلش و بی‌اینترنتی همه کارِ APK است (مالک، ۲۰۲۶-۱۰-۱۱، با اپِ نصب‌شده امتحان شد).** چیزی که روی گوشی دیده می‌شود
@@ -54,9 +54,10 @@
|---|---|---|
| اسمِ پکیج | `applicationId` در `app/build.gradle.kts` | `com.tadrisino.math.app`؛ بازار محصول‌های ۱۰۳۴۲–۱۰۳۴۴ را زیرِ همین نام شناخت (API، توکنِ ساختگی) |
| کلیدِ RSAِ عمومیِ اپ | `BAZAAR_RSA_KEY` در `app/build.gradle.kts` | گذاشته شد |
| SKUِ هر پلن | پنلِ ادمینِ بک‌اند: `/admin/subscription/plan/` ← «Bazaar sku» | سه‌ماهه `10343`، شش‌ماهه `10344` (migrationِ `subscription.0014`)؛ یک‌ماهه و بسته‌ها ندارند؛ `10342` در بازار هست ولی معلوم نیست چیست |
| SKUِ هر پلن | پنلِ ادمینِ بک‌اند: `/admin/subscription/plan/` ← «Bazaar sku» | یک‌ماهه `10342`، سه‌ماهه `10343`، شش‌ماهه `10344`؛ بسته‌ها هنوز در پیشخان تعریف نشده‌اند |
| کلیدِ «تخفیف پویا» | فقط `.env`ِ سرور (`BAZAAR_DYNAMIC_PRICE_KEY`)، **هرگز در اپ** | روی سرور |
قیمت: فرانت `TadrisinoAndroid.bazaarPrices(skusJson, productType)` را صدا می‌زند و قیمتِ پیشخان را روی کاشیِ بازار نشان می‌دهد.
خرید: فرانت `TadrisinoAndroid.bazaarBuy(sku, orderId, productType, dynamicPriceToken)` را صدا می‌زند؛ اشتراک با
`subscribeProduct` (بی‌consume، بازار خودش تمدید می‌کند)، بسته با `purchaseProduct` و بعد `bazaarConsume`. قیمتِ هر
محصول در پیشخان باید با قیمتِ همان پلن در ادمین یکی باشد؛ فقط وقتی کدِ تخفیف خورده، سرور قیمتِ کم‌شده را امضا می‌کند.

View File

@@ -42,6 +42,7 @@ import org.json.JSONObject
*
* قراردادِ پل با `src/lib/bazaar.ts`ِ فرانت:
* JS → اینجا TadrisinoAndroid.bazaarBuy(sku, orderId, productType, dynamicPriceToken)
* TadrisinoAndroid.bazaarPrices(skusJson, productType) → رویدادِ «tadrisino:bazaar-prices»
* TadrisinoAndroid.bazaarConsume(purchaseToken)
* اینجا → JS window رویدادِ «tadrisino:bazaar» با { orderId, purchaseToken } یا
* { orderId, error: "canceled" | "failed", message }
@@ -261,6 +262,36 @@ class MainActivity : AppCompatActivity() {
if (subscription) p.getSubscribedProducts(query) else p.getPurchasedProducts(query)
}
/**
* قیمتِ ثبت‌شده در پیشخانِ بازار برای هر SKU (متنِ آماده‌ی نمایش، مثلِ «۷۹۸,۰۰۰ تومان»)، تا فرانت روی کاشیِ
* بازار همان را نشان دهد. API سرور-به-سرورِ بازار قیمت نمی‌دهد؛ فقط Poolakey روی گوشی.
* نتیجه: رویدادِ «tadrisino:bazaar-prices» با { productType, prices: { sku: price } } یا { productType, error }.
*/
private fun prices(skus: List<String>, productType: String) {
val p = payment ?: return sendPrices(productType, error = "bazaar not connected")
val callback: ir.cafebazaar.poolakey.callback.GetSkuDetailsCallback.() -> Unit = {
getSkuDetailsSucceed { list ->
val map = JSONObject()
list.forEach { map.put(it.sku, it.price) }
sendPrices(productType, prices = map)
}
getSkuDetailsFailed { sendPrices(productType, error = it.message ?: "failed") }
}
if (productType == "subscription") p.getSubscriptionSkuDetails(skus, callback)
else p.getInAppSkuDetails(skus, callback)
}
private fun sendPrices(productType: String, prices: JSONObject? = null, error: String? = null) {
val detail = JSONObject().put("productType", productType)
if (prices != null) detail.put("prices", prices)
if (error != null) detail.put("error", error)
runOnUiThread {
webView.evaluateJavascript(
"window.dispatchEvent(new CustomEvent('tadrisino:bazaar-prices', { detail: $detail }));", null,
)
}
}
private fun consume(purchaseToken: String) {
payment?.consumeProduct(purchaseToken) {
consumeSucceed { Log.i(TAG, "consumed") }
@@ -324,6 +355,22 @@ class MainActivity : AppCompatActivity() {
runOnUiThread { hideOffline() }
}
/** skusJson آرایه‌ی JSON از SKUها؛ productType «inapp» یا «subscription». */
@JavascriptInterface
fun bazaarPrices(skusJson: String, productType: String) {
val skus = try {
val arr = JSONArray(skusJson)
List(arr.length()) { i -> arr.getString(i) }
} catch (e: Exception) {
Log.w(TAG, "bad skus", e)
return
}
runOnUiThread {
if (!isOurPage(Uri.parse(webView.url ?: ""))) return@runOnUiThread
prices(skus, productType)
}
}
/** فرانت با این می‌فهمد درگاه داخلِ اپ باز می‌شود (isPaymentPage)، نه در مرورگرِ بیرونی. */
@JavascriptInterface
fun paysInApp(): Boolean = true