forked from Tadrisino/MathApp_frontend
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:
@@ -41,17 +41,16 @@ import org.json.JSONObject
|
||||
* همان سایتِ تدریسینو داخلِ WebView؛ تنها کارِ بومی پرداختِ درونبرنامهایِ بازار است.
|
||||
*
|
||||
* قراردادِ پل با `src/lib/bazaar.ts`ِ فرانت:
|
||||
* JS → اینجا TadrisinoAndroid.bazaarBuy(sku, orderId, productType, dynamicPriceToken)
|
||||
* TadrisinoAndroid.bazaarPrices(skusJson, productType) → رویدادِ «tadrisino:bazaar-prices»
|
||||
* TadrisinoAndroid.bazaarConsume(purchaseToken)
|
||||
* JS → اینجا TadrisinoAndroid.bazaarBuy(sku, orderId, dynamicPriceToken) (اشتراک)
|
||||
* TadrisinoAndroid.bazaarPrices(skusJson) → رویدادِ «tadrisino:bazaar-prices»
|
||||
* اینجا → JS window رویدادِ «tadrisino:bazaar» با { orderId, purchaseToken } یا
|
||||
* { orderId, error: "canceled" | "failed", message }
|
||||
*
|
||||
* سمتِ سرور توکن را از خودِ بازار میسنجد (`POST /api/payment/bazaar/verify`)، پس اینجا
|
||||
* فقط واسطه است و به هیچ نتیجهای اعتماد نمیکند.
|
||||
*
|
||||
* ⚠️ نوشته شده بیآنکه build شود (ماشینِ نویسنده Android SDK نداشت، ۲۰۲۶-۱۰-۰۳)؛ APIِ
|
||||
* Poolakey از README خودش است. پیش از انتشار با Android Studio بسازید و یک خریدِ واقعی بزنید.
|
||||
* ⚠️ کامپایل میشود (compileDebugKotlin، ۲۰۲۶-۱۰-۱۲) و امضای توابعِ Poolakey 2.2.0 از خودِ کتابخانه خوانده شد،
|
||||
* ولی روی گوشی اجرا نشده؛ پیش از انتشار یک خریدِ واقعی بزنید.
|
||||
*/
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@@ -220,38 +219,34 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* productType «inapp» (بسته: purchaseProduct، بعد از تأییدِ سرور consume) یا «subscription» (اشتراکِ
|
||||
* بازار: subscribeProduct، مصرف نمیشود و بازار خودش تمدید میکند). dynamicPriceToken خالی یعنی قیمتِ
|
||||
* پیشخان؛ پر یعنی قیمتی که سرور با کلیدِ «تخفیف پویا» امضا کرده (بازارِ ۱۳.۳ به بعد).
|
||||
* خریدِ اشتراکِ بازار (subscribeProduct؛ فقط اشتراک فروخته میشود و مصرف نمیشود، بازار خودش تمدید میکند).
|
||||
* dynamicPriceToken خالی یعنی قیمتِ پیشخان؛ پر یعنی قیمتی که سرور با کلیدِ «تخفیف پویا» امضا کرده
|
||||
* (بازارِ ۱۳.۳ به بعد).
|
||||
*/
|
||||
private fun buy(sku: String, orderId: String, productType: String, dynamicPriceToken: String) {
|
||||
private fun buy(sku: String, orderId: String, dynamicPriceToken: String) {
|
||||
val p = payment
|
||||
if (p == null) {
|
||||
send(orderId, error = "failed", message = "bazaar not connected")
|
||||
return
|
||||
}
|
||||
val subscription = productType == "subscription"
|
||||
val request = PurchaseRequest(
|
||||
productId = sku, payload = orderId, dynamicPriceToken = dynamicPriceToken.ifEmpty { null },
|
||||
)
|
||||
val callback: ir.cafebazaar.poolakey.callback.PurchaseCallback.() -> Unit = {
|
||||
p.subscribeProduct(activityResultRegistry, request) {
|
||||
purchaseSucceed { send(orderId, token = it.purchaseToken) }
|
||||
purchaseCanceled { send(orderId, error = "canceled") }
|
||||
purchaseFailed { recoverOwned(sku, orderId, subscription, it) }
|
||||
failedToBeginFlow { recoverOwned(sku, orderId, subscription, it) }
|
||||
purchaseFailed { recoverOwned(sku, orderId, it) }
|
||||
failedToBeginFlow { recoverOwned(sku, orderId, it) }
|
||||
}
|
||||
if (subscription) p.subscribeProduct(activityResultRegistry, request, callback)
|
||||
else p.purchaseProduct(activityResultRegistry, request, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* خریدِ قبلیِ همین محصول (بستهی مصرفنشده، یا اشتراکِ فعال) که پولش رفته ولی تأییدِ سرور نرسیده بود
|
||||
* جلوی خریدِ دوباره را میگیرد. فرانت در تلاشِ بعدی همان سفارش را میفرستد؛ اگر توکنِ ماندهٔ همان
|
||||
* سفارش (payload) پیدا شد، همان برمیگردد تا سرور ثبتش کند.
|
||||
* اشتراکِ همین محصول که پولش رفته ولی تأییدِ سرور نرسیده بود جلوی خریدِ دوباره را میگیرد. فرانت در تلاشِ
|
||||
* بعدی همان سفارش را میفرستد؛ اگر اشتراکِ همان سفارش (payload) پیدا شد، توکنش برمیگردد تا سرور ثبتش کند.
|
||||
*/
|
||||
private fun recoverOwned(sku: String, orderId: String, subscription: Boolean, cause: Throwable) {
|
||||
private fun recoverOwned(sku: String, orderId: String, cause: Throwable) {
|
||||
val p = payment ?: return send(orderId, error = "failed", message = cause.message)
|
||||
val query: ir.cafebazaar.poolakey.callback.PurchaseQueryCallback.() -> Unit = {
|
||||
p.getSubscribedProducts {
|
||||
querySucceed { owned ->
|
||||
val match = owned.firstOrNull { it.productId == sku && it.payload == orderId }
|
||||
if (match != null) send(orderId, token = match.purchaseToken)
|
||||
@@ -259,30 +254,27 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
queryFailed { send(orderId, error = "failed", message = cause.message) }
|
||||
}
|
||||
if (subscription) p.getSubscribedProducts(query) else p.getPurchasedProducts(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* قیمتِ ثبتشده در پیشخانِ بازار برای هر SKU (متنِ آمادهی نمایش، مثلِ «۷۹۸,۰۰۰ تومان»)، تا فرانت روی کاشیِ
|
||||
* قیمتِ ثبتشده در پیشخانِ بازار برای هر SKU (متنِ آمادهی نمایش، مثلِ «۸۹۸,۰۰۰ تومان»)، تا فرانت روی کاشیِ
|
||||
* بازار همان را نشان دهد. API سرور-به-سرورِ بازار قیمت نمیدهد؛ فقط Poolakey روی گوشی.
|
||||
* نتیجه: رویدادِ «tadrisino:bazaar-prices» با { productType, prices: { sku: price } } یا { productType, error }.
|
||||
* نتیجه: رویدادِ «tadrisino:bazaar-prices» با { prices: { sku: price } } یا { 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 = {
|
||||
private fun prices(skus: List<String>) {
|
||||
val p = payment ?: return sendPrices(error = "bazaar not connected")
|
||||
p.getSubscriptionSkuDetails(skus) {
|
||||
getSkuDetailsSucceed { list ->
|
||||
val map = JSONObject()
|
||||
list.forEach { map.put(it.sku, it.price) }
|
||||
sendPrices(productType, prices = map)
|
||||
sendPrices(prices = map)
|
||||
}
|
||||
getSkuDetailsFailed { sendPrices(productType, error = it.message ?: "failed") }
|
||||
getSkuDetailsFailed { sendPrices(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)
|
||||
private fun sendPrices(prices: JSONObject? = null, error: String? = null) {
|
||||
val detail = JSONObject()
|
||||
if (prices != null) detail.put("prices", prices)
|
||||
if (error != null) detail.put("error", error)
|
||||
runOnUiThread {
|
||||
@@ -292,13 +284,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun consume(purchaseToken: String) {
|
||||
payment?.consumeProduct(purchaseToken) {
|
||||
consumeSucceed { Log.i(TAG, "consumed") }
|
||||
consumeFailed { Log.e(TAG, "consume failed", it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun send(orderId: String, token: String? = null, error: String? = null, message: String? = null) {
|
||||
val detail = JSONObject().put("orderId", orderId)
|
||||
if (token != null) detail.put("purchaseToken", token)
|
||||
@@ -315,10 +300,10 @@ class MainActivity : AppCompatActivity() {
|
||||
private inner class Bridge {
|
||||
/** قراردادش در src/lib/bazaar.tsِ فرانت است. */
|
||||
@JavascriptInterface
|
||||
fun bazaarBuy(sku: String, orderId: String, productType: String, dynamicPriceToken: String) {
|
||||
fun bazaarBuy(sku: String, orderId: String, dynamicPriceToken: String) {
|
||||
runOnUiThread {
|
||||
if (!isOurPage(Uri.parse(webView.url ?: ""))) return@runOnUiThread
|
||||
buy(sku, orderId, productType, dynamicPriceToken)
|
||||
buy(sku, orderId, dynamicPriceToken)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,9 +340,9 @@ class MainActivity : AppCompatActivity() {
|
||||
runOnUiThread { hideOffline() }
|
||||
}
|
||||
|
||||
/** skusJson آرایهی JSON از SKUها؛ productType «inapp» یا «subscription». */
|
||||
/** skusJson آرایهی JSON از SKUهای اشتراک. */
|
||||
@JavascriptInterface
|
||||
fun bazaarPrices(skusJson: String, productType: String) {
|
||||
fun bazaarPrices(skusJson: String) {
|
||||
val skus = try {
|
||||
val arr = JSONArray(skusJson)
|
||||
List(arr.length()) { i -> arr.getString(i) }
|
||||
@@ -367,7 +352,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
runOnUiThread {
|
||||
if (!isOurPage(Uri.parse(webView.url ?: ""))) return@runOnUiThread
|
||||
prices(skus, productType)
|
||||
prices(skus)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,13 +360,6 @@ class MainActivity : AppCompatActivity() {
|
||||
@JavascriptInterface
|
||||
fun paysInApp(): Boolean = true
|
||||
|
||||
@JavascriptInterface
|
||||
fun bazaarConsume(purchaseToken: String) {
|
||||
runOnUiThread {
|
||||
if (!isOurPage(Uri.parse(webView.url ?: ""))) return@runOnUiThread
|
||||
consume(purchaseToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
Reference in New Issue
Block a user