Add methods to verify and purchase subscriptions
This commit is contained in:
parent
12eee2cd40
commit
2c6ae4214d
8 changed files with 306 additions and 79 deletions
|
|
@ -12,10 +12,24 @@ import com.android.billingclient.api.BillingClient.BillingResponseCode.USER_CANC
|
|||
import com.android.billingclient.api.BillingResult
|
||||
import org.amnezia.vpn.util.ErrorCode
|
||||
|
||||
internal class BillingException(billingResult: BillingResult) : Exception(billingResult.debugMessage) {
|
||||
internal class BillingException(
|
||||
billingResult: BillingResult,
|
||||
retryable: Boolean = false
|
||||
) : Exception(billingResult.toString()) {
|
||||
|
||||
constructor(msg: String) : this(BillingResult.newBuilder()
|
||||
.setResponseCode(9999)
|
||||
.setDebugMessage(msg)
|
||||
.build())
|
||||
|
||||
val errorCode: Int
|
||||
val isCanceled = billingResult.responseCode == USER_CANCELED
|
||||
val isRetryable = retryable || billingResult.responseCode in setOf(
|
||||
NETWORK_ERROR,
|
||||
SERVICE_DISCONNECTED,
|
||||
SERVICE_UNAVAILABLE,
|
||||
ERROR
|
||||
)
|
||||
|
||||
init {
|
||||
when (billingResult.responseCode) {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams
|
||||
import com.android.billingclient.api.BillingClient
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode
|
||||
import com.android.billingclient.api.BillingClient.ProductType
|
||||
import com.android.billingclient.api.BillingClientStateListener
|
||||
import com.android.billingclient.api.BillingFlowParams
|
||||
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode
|
||||
import com.android.billingclient.api.BillingResult
|
||||
import com.android.billingclient.api.GetBillingConfigParams
|
||||
import com.android.billingclient.api.PendingPurchasesParams
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||
import com.android.billingclient.api.QueryProductDetailsParams
|
||||
import com.android.billingclient.api.QueryProductDetailsParams.Product
|
||||
import com.android.billingclient.api.QueryPurchasesParams
|
||||
import com.android.billingclient.api.acknowledgePurchase
|
||||
import com.android.billingclient.api.queryProductDetails
|
||||
import com.android.billingclient.api.queryPurchasesAsync
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.vpn.util.Log
|
||||
import org.json.JSONArray
|
||||
|
|
@ -26,6 +34,7 @@ private const val TAG = "BillingProvider"
|
|||
private const val RESULT_OK = 1
|
||||
private const val RESULT_CANCELED = 0
|
||||
private const val RESULT_ERROR = -1
|
||||
private const val PRODUCT_ID = "premium"
|
||||
|
||||
class BillingProvider(context: Context) : AutoCloseable {
|
||||
|
||||
|
|
@ -33,7 +42,7 @@ class BillingProvider(context: Context) : AutoCloseable {
|
|||
private var subscriptionPurchases = MutableStateFlow<Pair<BillingResult, List<Purchase>?>?>(null)
|
||||
|
||||
private val purchasesUpdatedListeners = PurchasesUpdatedListener { billingResult, purchases ->
|
||||
Log.v(TAG, "PurchasesUpdatedListener: $billingResult")
|
||||
Log.v(TAG, "Purchases updated: $billingResult")
|
||||
subscriptionPurchases.value = billingResult to purchases
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +56,7 @@ class BillingProvider(context: Context) : AutoCloseable {
|
|||
private suspend fun connect() {
|
||||
if (billingClient.isReady) return
|
||||
|
||||
Log.v(TAG, "Connect to Google Play")
|
||||
Log.v(TAG, "Billing client connection")
|
||||
val connection = CompletableDeferred<Unit>()
|
||||
withContext(Dispatchers.IO) {
|
||||
billingClient.startConnection(object : BillingClientStateListener {
|
||||
|
|
@ -69,29 +78,67 @@ class BillingProvider(context: Context) : AutoCloseable {
|
|||
connection.await()
|
||||
}
|
||||
|
||||
private suspend fun handleBillingApiCall(block: suspend () -> JSONObject): JSONObject =
|
||||
try {
|
||||
block()
|
||||
} catch (e: BillingException) {
|
||||
if (e.isCanceled) {
|
||||
Log.w(TAG, "Billing canceled")
|
||||
JSONObject().put("result", RESULT_CANCELED)
|
||||
} else {
|
||||
Log.e(TAG, "Billing error: $e")
|
||||
JSONObject()
|
||||
.put("result", RESULT_ERROR)
|
||||
.put("errorCode", e.errorCode)
|
||||
private suspend fun handleBillingApiCall(block: suspend () -> JSONObject): JSONObject {
|
||||
val numberAttempts = 3
|
||||
var attemptCount = 0
|
||||
while (true) {
|
||||
try {
|
||||
return block()
|
||||
} catch (e: BillingException) {
|
||||
if (e.isCanceled) {
|
||||
Log.w(TAG, "Billing canceled")
|
||||
return JSONObject().put("result", RESULT_CANCELED)
|
||||
} else if (e.isRetryable && attemptCount < numberAttempts) {
|
||||
Log.d(TAG, "Retryable error: $e")
|
||||
++attemptCount
|
||||
delay(1000)
|
||||
} else {
|
||||
Log.e(TAG, "Billing error: $e")
|
||||
return JSONObject()
|
||||
.put("result", RESULT_ERROR)
|
||||
.put("errorCode", e.errorCode)
|
||||
}
|
||||
} catch (_: CancellationException) {
|
||||
Log.w(TAG, "Billing coroutine canceled")
|
||||
return JSONObject().put("result", RESULT_CANCELED)
|
||||
}
|
||||
} catch (_: CancellationException) {
|
||||
Log.w(TAG, "Billing coroutine canceled")
|
||||
JSONObject().put("result", RESULT_CANCELED)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSubscriptionPlans(): JSONObject = handleBillingApiCall {
|
||||
suspend fun getSubscriptionPlans(): JSONObject {
|
||||
Log.v(TAG, "Get subscription plans")
|
||||
|
||||
val productDetailsList = getProductDetails()
|
||||
val resultJson = JSONObject().put("result", RESULT_OK)
|
||||
val productArray = JSONArray().also { resultJson.put("products", it) }
|
||||
productDetailsList?.forEach { productDetails ->
|
||||
val product = JSONObject().also { productArray.put(it) }
|
||||
.put("productId", productDetails.productId)
|
||||
.put("name", productDetails.name)
|
||||
val offers = JSONArray().also { product.put("offers", it) }
|
||||
productDetails.subscriptionOfferDetails?.forEach { offerDetails ->
|
||||
val offer = JSONObject().also { offers.put(it) }
|
||||
.put("basePlanId", offerDetails.basePlanId)
|
||||
.put("offerId", offerDetails.offerId)
|
||||
.put("offerToken", offerDetails.offerToken)
|
||||
val pricingPhases = JSONArray().also { offer.put("pricingPhases", it) }
|
||||
offerDetails.pricingPhases.pricingPhaseList.forEach { phase ->
|
||||
JSONObject().also { pricingPhases.put(it) }
|
||||
.put("billingCycleCount", phase.billingCycleCount)
|
||||
.put("billingPeriod", phase.billingPeriod)
|
||||
.put("formatedPrice", phase.formattedPrice)
|
||||
.put("recurrenceMode", phase.recurrenceMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
return resultJson
|
||||
}
|
||||
|
||||
private suspend fun getProductDetails(): List<ProductDetails>? {
|
||||
Log.v(TAG, "Get product details")
|
||||
|
||||
val productDetailsParams = Product.newBuilder()
|
||||
.setProductId("premium")
|
||||
.setProductId(PRODUCT_ID)
|
||||
.setProductType(ProductType.SUBS)
|
||||
.build()
|
||||
|
||||
|
|
@ -103,40 +150,17 @@ class BillingProvider(context: Context) : AutoCloseable {
|
|||
billingClient.queryProductDetails(queryProductDetailsParams)
|
||||
}
|
||||
|
||||
Log.v(TAG, "Query product details result: ${result.billingResult}")
|
||||
|
||||
if (!result.billingResult.isOk) {
|
||||
Log.e(TAG, "Failed to get subscription plans: ${result.billingResult}")
|
||||
Log.e(TAG, "Failed to get product details: ${result.billingResult}")
|
||||
throw BillingException(result.billingResult)
|
||||
}
|
||||
|
||||
Log.v(TAG, "Subscription plans:\n${result.productDetailsList}")
|
||||
|
||||
val resultJson = JSONObject().put("result", RESULT_OK)
|
||||
|
||||
val productArray = JSONArray().also { resultJson.put("products", it) }
|
||||
result.productDetailsList?.forEach {
|
||||
val product = JSONObject().also { productArray.put(it) }
|
||||
product.put("productId", it.productId)
|
||||
product.put("name", it.name)
|
||||
val offers = JSONArray().also { product.put("offers", it) }
|
||||
it.subscriptionOfferDetails?.forEach {
|
||||
val offer = JSONObject().also { offers.put(it) }
|
||||
offer.put("basePlanId", it.basePlanId)
|
||||
offer.put("offerId", it.offerId)
|
||||
offer.put("offerToken", it.offerToken)
|
||||
val pricingPhases = JSONArray().also { offer.put("pricingPhases", it) }
|
||||
it.pricingPhases.pricingPhaseList.forEach {
|
||||
val pricingPhase = JSONObject().also { pricingPhases.put(it) }
|
||||
pricingPhase.put("billingCycleCount", it.billingCycleCount)
|
||||
pricingPhase.put("billingPeriod", it.billingPeriod)
|
||||
pricingPhase.put("formatedPrice", it.formattedPrice)
|
||||
pricingPhase.put("recurrenceMode", it.recurrenceMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
resultJson
|
||||
return result.productDetailsList
|
||||
}
|
||||
|
||||
suspend fun getCustomerCountryCode(): JSONObject = handleBillingApiCall {
|
||||
suspend fun getCustomerCountryCode(): JSONObject {
|
||||
Log.v(TAG, "Get customer country code")
|
||||
|
||||
val deferred = CompletableDeferred<String>()
|
||||
|
|
@ -153,22 +177,135 @@ class BillingProvider(context: Context) : AutoCloseable {
|
|||
}
|
||||
val countryCode = deferred.await()
|
||||
|
||||
JSONObject()
|
||||
return JSONObject()
|
||||
.put("result", RESULT_OK)
|
||||
.put("countryCode", countryCode)
|
||||
}
|
||||
|
||||
suspend fun purchaseSubscription(activity: Activity, obfuscatedAccountId: String): JSONObject =
|
||||
handleBillingApiCall {
|
||||
Log.v(TAG, "Purchase subscription")
|
||||
billingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder()
|
||||
.setObfuscatedAccountId(obfuscatedAccountId)
|
||||
.build())
|
||||
JSONObject()
|
||||
suspend fun purchaseSubscription(
|
||||
activity: Activity,
|
||||
offerToken: String,
|
||||
oldPurchaseToken: String? = null
|
||||
): JSONObject {
|
||||
Log.v(TAG, "Purchase subscription")
|
||||
Log.v(TAG, "Offer token: $offerToken")
|
||||
oldPurchaseToken?.let { Log.v(TAG, "Old purchase token: $it") }
|
||||
|
||||
if (offerToken.isBlank()) throw BillingException("offerToken can not be empty")
|
||||
|
||||
val productDetails = getProductDetails()?.let {
|
||||
it.filter { it.productId == PRODUCT_ID }
|
||||
}?.firstOrNull() ?: throw BillingException("Product details not found")
|
||||
|
||||
Log.v(TAG, "Filtered product details:\n$productDetails")
|
||||
|
||||
val productDetail = BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(productDetails)
|
||||
.setOfferToken(offerToken)
|
||||
.build()
|
||||
|
||||
val subscriptionUpdateParams = oldPurchaseToken?.let {
|
||||
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
|
||||
.setOldPurchaseToken(oldPurchaseToken)
|
||||
.setSubscriptionReplacementMode(ReplacementMode.WITHOUT_PRORATION)
|
||||
.build()
|
||||
}
|
||||
|
||||
val billingResult = billingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder()
|
||||
.setProductDetailsParamsList(listOf(productDetail))
|
||||
.apply { subscriptionUpdateParams?.let { setSubscriptionUpdateParams(it) } }
|
||||
.build())
|
||||
|
||||
Log.v(TAG, "Start billing flow result: $billingResult")
|
||||
|
||||
if (billingResult.responseCode == BillingResponseCode.ITEM_ALREADY_OWNED) {
|
||||
Log.w(TAG, "Attempting to purchase already owned product")
|
||||
val purchases = queryPurchases()
|
||||
if (purchases.any { PRODUCT_ID in it.products }) throw BillingException(billingResult)
|
||||
else throw BillingException(billingResult, retryable = true)
|
||||
} else if (billingResult.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
|
||||
Log.w(TAG, "Attempting to replace not owned product")
|
||||
val purchases = queryPurchases()
|
||||
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(billingResult)
|
||||
else throw BillingException(billingResult, retryable = true)
|
||||
} else if (!billingResult.isOk) throw BillingException(billingResult)
|
||||
|
||||
subscriptionPurchases.firstOrNull { it != null }?.let { (billingResult, purchases) ->
|
||||
if (!billingResult.isOk) throw BillingException(billingResult)
|
||||
return JSONObject()
|
||||
.put("result", RESULT_OK)
|
||||
.put("purchases", processPurchases(purchases))
|
||||
} ?: throw BillingException("Purchase failed")
|
||||
}
|
||||
|
||||
private fun processPurchases(purchases: List<Purchase>?): JSONArray {
|
||||
val purchaseArray = JSONArray()
|
||||
purchases?.forEach { purchase ->
|
||||
val purchaseJson = JSONObject().also { purchaseArray.put(it) }
|
||||
.put("purchaseToken", purchase.purchaseToken)
|
||||
.put("purchaseTime", purchase.purchaseTime)
|
||||
.put("purchaseState", purchase.purchaseState)
|
||||
.put("isAcknowledged", purchase.isAcknowledged)
|
||||
.put("isAutoRenewing", purchase.isAutoRenewing)
|
||||
.put("orderId", purchase.orderId)
|
||||
.put("productIds", JSONArray(purchase.products))
|
||||
|
||||
purchase.pendingPurchaseUpdate?.let { purchaseUpdate ->
|
||||
JSONObject()
|
||||
.put("purchaseToken", purchaseUpdate.purchaseToken)
|
||||
.put("productIds", JSONArray(purchaseUpdate.products))
|
||||
}.also { purchaseJson.put("pendingPurchaseUpdate", it) }
|
||||
}
|
||||
return purchaseArray
|
||||
}
|
||||
|
||||
suspend fun acknowledge(purchaseToken: String): JSONObject {
|
||||
Log.v(TAG, "Acknowledge purchase: $purchaseToken")
|
||||
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
billingClient.acknowledgePurchase(
|
||||
AcknowledgePurchaseParams.newBuilder()
|
||||
.setPurchaseToken(purchaseToken)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
Log.v(TAG, "Acknowledge purchase result: $result")
|
||||
|
||||
if (result.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
|
||||
Log.w(TAG, "Attempting to acknowledge not owned product")
|
||||
val purchases = queryPurchases()
|
||||
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(result)
|
||||
else throw BillingException(result, retryable = true)
|
||||
} else if (!result.isOk && result.responseCode != BillingResponseCode.ITEM_ALREADY_OWNED) {
|
||||
throw BillingException(result)
|
||||
}
|
||||
|
||||
return JSONObject().put("result", RESULT_OK)
|
||||
}
|
||||
|
||||
suspend fun getPurchases(): JSONObject {
|
||||
Log.v(TAG, "Get purchases")
|
||||
val purchases = queryPurchases()
|
||||
return JSONObject()
|
||||
.put("result", RESULT_OK)
|
||||
.put("purchases", processPurchases(purchases))
|
||||
}
|
||||
|
||||
private suspend fun queryPurchases(): List<Purchase> {
|
||||
Log.v(TAG, "Query purchases")
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
billingClient.queryPurchasesAsync(
|
||||
QueryPurchasesParams.newBuilder().setProductType(ProductType.SUBS).build()
|
||||
)
|
||||
}
|
||||
Log.v(TAG, "Query purchases result: ${result.billingResult}")
|
||||
if (!result.billingResult.isOk) throw BillingException(result.billingResult)
|
||||
return result.purchasesList
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
Log.d(TAG, "Close billing client connection")
|
||||
Log.v(TAG, "Close billing client connection")
|
||||
billingClient.endConnection()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,5 +6,8 @@ import android.content.Context
|
|||
class BillingPaymentRepository(@Suppress("UNUSED_PARAMETER") context: Context) : BillingRepository {
|
||||
override suspend fun getCountryCode(): String = ""
|
||||
override suspend fun getSubscriptionPlans(): String = ""
|
||||
override suspend fun purchaseSubscription(activity: Activity): String = ""
|
||||
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String = ""
|
||||
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String = ""
|
||||
override suspend fun acknowledge(purchaseToken: String): String = ""
|
||||
override suspend fun queryPurchases(): String = ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,21 @@ class BillingPaymentRepository(private val context: Context) : BillingRepository
|
|||
getSubscriptionPlans()
|
||||
}
|
||||
|
||||
override suspend fun purchaseSubscription(activity: Activity): String = withBillingProvider(context) {
|
||||
purchaseSubscription(activity, "obfuscatedAccountId")
|
||||
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String =
|
||||
withBillingProvider(context) {
|
||||
purchaseSubscription(activity, offerToken)
|
||||
}
|
||||
|
||||
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String =
|
||||
withBillingProvider(context) {
|
||||
purchaseSubscription(activity, offerToken, oldPurchaseToken)
|
||||
}
|
||||
|
||||
override suspend fun acknowledge(purchaseToken: String): String = withBillingProvider(context) {
|
||||
acknowledge(purchaseToken)
|
||||
}
|
||||
|
||||
override suspend fun queryPurchases(): String = withBillingProvider(context) {
|
||||
getPurchases()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import androidx.annotation.RequiresApi
|
|||
import androidx.core.content.ContextCompat
|
||||
import java.io.IOException
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.text.RegexOption.IGNORE_CASE
|
||||
import AppListProvider
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
|
@ -39,7 +40,6 @@ import kotlinx.coroutines.async
|
|||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.vpn.protocol.getStatistics
|
||||
import org.amnezia.vpn.protocol.getStatus
|
||||
import org.amnezia.vpn.qt.QtAndroidController
|
||||
|
|
@ -648,15 +648,9 @@ class AmneziaActivity : QtActivity() {
|
|||
@Suppress("unused")
|
||||
fun getAppList(): String {
|
||||
Log.v(TAG, "Get app list")
|
||||
var appList = ""
|
||||
runBlocking {
|
||||
mainScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
appList = AppListProvider.getAppList(packageManager, packageName)
|
||||
}
|
||||
}.join()
|
||||
return blockingCall(Dispatchers.IO) {
|
||||
AppListProvider.getAppList(packageManager, packageName)
|
||||
}
|
||||
return appList
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
|
|
@ -728,26 +722,51 @@ class AmneziaActivity : QtActivity() {
|
|||
@Suppress("unused")
|
||||
fun getCountryCode(): String {
|
||||
Log.v(TAG, "Get country code")
|
||||
return runBlocking {
|
||||
mainScope.async {
|
||||
billingRepository.getCountryCode()
|
||||
}.await()
|
||||
}
|
||||
return blockingCall { billingRepository.getCountryCode() }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getSubscriptionPlans(): String {
|
||||
Log.v(TAG, "Get subscription plans")
|
||||
return runBlocking {
|
||||
mainScope.async {
|
||||
billingRepository.getSubscriptionPlans()
|
||||
}.await()
|
||||
return blockingCall { billingRepository.getSubscriptionPlans() }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun purchaseSubscription(offerToken: String): String {
|
||||
Log.v(TAG, "Purchase subscription")
|
||||
return blockingCall { billingRepository.purchaseSubscription(this@AmneziaActivity, offerToken) }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun upgradeSubscription(offerToken: String, oldPurchaseToken: String): String {
|
||||
Log.v(TAG, "Upgrade subscription")
|
||||
return blockingCall {
|
||||
billingRepository.upgradeSubscription(this@AmneziaActivity, offerToken, oldPurchaseToken)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun acknowledgePurchase(purchaseToken: String): String {
|
||||
Log.v(TAG, "Acknowledge purchase")
|
||||
return blockingCall { billingRepository.acknowledge(purchaseToken) }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun queryPurchases(): String {
|
||||
Log.v(TAG, "Query purchases")
|
||||
return blockingCall { billingRepository.queryPurchases() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Utils methods
|
||||
*/
|
||||
private fun <T> blockingCall(
|
||||
context: CoroutineContext = Dispatchers.Default,
|
||||
block: suspend () -> T
|
||||
) = runBlocking {
|
||||
mainScope.async(context) { block() }.await()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun actionCodeToString(actionCode: Int): String =
|
||||
when (actionCode) {
|
||||
|
|
|
|||
|
|
@ -5,5 +5,8 @@ import android.app.Activity
|
|||
interface BillingRepository {
|
||||
suspend fun getCountryCode(): String
|
||||
suspend fun getSubscriptionPlans(): String
|
||||
suspend fun purchaseSubscription(activity: Activity): String
|
||||
suspend fun purchaseSubscription(activity: Activity, offerToken: String): String
|
||||
suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String
|
||||
suspend fun acknowledge(purchaseToken: String): String
|
||||
suspend fun queryPurchases(): String
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,6 +299,39 @@ QJsonObject AndroidController::getSubscriptionPlans()
|
|||
return json;
|
||||
}
|
||||
|
||||
QJsonObject AndroidController::purchaseSubscription(const QString &offerToken)
|
||||
{
|
||||
QJniObject result = callActivityMethod<jstring, jstring>("purchaseSubscription", "(Ljava/lang/String;)Ljava/lang/String;",
|
||||
QJniObject::fromString(offerToken).object<jstring>());
|
||||
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||
return json;
|
||||
}
|
||||
|
||||
QJsonObject AndroidController::upgradeSubscription(const QString &offerToken, const QString &oldPurchaseToken)
|
||||
{
|
||||
QJniObject result = callActivityMethod<jstring, jstring, jstring>("upgradeSubscription",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
|
||||
QJniObject::fromString(offerToken).object<jstring>(),
|
||||
QJniObject::fromString(oldPurchaseToken).object<jstring>());
|
||||
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||
return json;
|
||||
}
|
||||
|
||||
QJsonObject AndroidController::acknowledgePurchase(const QString &purchaseToken)
|
||||
{
|
||||
QJniObject result = callActivityMethod<jstring, jstring>("acknowledgePurchase", "(Ljava/lang/String;)Ljava/lang/String;",
|
||||
QJniObject::fromString(purchaseToken).object<jstring>());
|
||||
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||
return json;
|
||||
}
|
||||
|
||||
QJsonObject AndroidController::queryPurchases()
|
||||
{
|
||||
QJniObject result = callActivityMethod<jstring>("queryPurchases", "()Ljava/lang/String;");
|
||||
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||
return json;
|
||||
}
|
||||
|
||||
// Moving log processing to the Android side
|
||||
jclass AndroidController::log;
|
||||
jmethodID AndroidController::logDebug;
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ public:
|
|||
bool requestAuthentication();
|
||||
bool isPlay();
|
||||
QJsonObject getSubscriptionPlans();
|
||||
QJsonObject purchaseSubscription(const QString &offerToken);
|
||||
QJsonObject upgradeSubscription(const QString &offerToken, const QString &oldPurchaseToken);
|
||||
QJsonObject acknowledgePurchase(const QString &purchaseToken);
|
||||
QJsonObject queryPurchases();
|
||||
|
||||
static bool initLogging();
|
||||
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue