Skip to content

Commit

Permalink
Merge branch 'dev' of https://github.com/tonkeeper/android into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
polstianka committed Oct 11, 2024
2 parents 2c6bc44 + 175b8b1 commit 58913d8
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 106 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.tonapps.tonkeeper.billing

import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.tonapps.extensions.ErrorForUserException
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

val ProductDetails.priceFormatted: CharSequence
get() = oneTimePurchaseOfferDetails!!.formattedPrice

val Purchase.walletId: String?
get() = accountIdentifiers?.obfuscatedAccountId

val BillingResult.isSuccess: Boolean
get() = responseCode == BillingClient.BillingResponseCode.OK

suspend fun <T> BillingClient.ready(block: suspend (client: BillingClient) -> T): T {
return if (isReady && connectionState == BillingClient.ConnectionState.CONNECTED) {
block(this)
} else {
block(getReady())
}
}

private suspend fun BillingClient.getReady(): BillingClient = suspendCancellableCoroutine { continuation ->
startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
if (!continuation.isActive) return
if (result.isSuccess) {
continuation.resume(this@getReady)
} else {
continuation.resumeWithException(ErrorForUserException.of(result.debugMessage))
}
}

override fun onBillingServiceDisconnected() { }
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,74 +2,65 @@ package com.tonapps.tonkeeper.billing

import android.app.Activity
import android.content.Context
import com.android.billingclient.api.*
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClient.ProductType
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ConsumeParams
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.QueryPurchasesParams
import com.android.billingclient.api.consumePurchase
import com.tonapps.extensions.MutableEffectFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

class BillingManager(
context: Context,
scope: CoroutineScope,
) {
) : PurchasesUpdatedListener {

private var billingClient: BillingClient

private val _purchasesFlow = MutableEffectFlow<List<Purchase>?>()
val purchasesFlow = _purchasesFlow.asSharedFlow()
private var billingClient: BillingClient = BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()

private val _productsFlow = MutableStateFlow<List<ProductDetails>?>(null)
val productsFlow = _productsFlow.asStateFlow()

private val _madePurchaseFlow = MutableEffectFlow<Unit>()
val madePurchaseFlow = _madePurchaseFlow.shareIn(scope, SharingStarted.Lazily, 1)

private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
_purchasesFlow.tryEmit(purchases)
} else {
_purchasesFlow.tryEmit(null)
}
}

private var isInitialized = false

init {
billingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()

billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
isInitialized = true
}
}

override fun onBillingServiceDisconnected() {
isInitialized = false
}
})

notifyPurchase()
}

fun notifyPurchase() {
override fun onPurchasesUpdated(result: BillingResult, purchases: MutableList<Purchase>?) {}

private fun notifyPurchase() {
_madePurchaseFlow.tryEmit(Unit)
}

suspend fun consumeProduct(purchaseToken: String) = withContext(Dispatchers.IO) {
val params = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken).build()
billingClient.consumePurchase(params)
notifyPurchase()
}

fun getProducts(
productIds: List<String>,
productType: String = ProductType.INAPP
) {
if (!isInitialized || productIds.isEmpty()) {
if (productIds.isEmpty()) {
return
}

Expand All @@ -93,20 +84,7 @@ class BillingManager(
}
}

// Method to request a purchase
fun requestPurchase(activity: Activity, productDetails: ProductDetails) {
val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build()

val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productDetailsParams))
.build()

billingClient.launchBillingFlow(activity, billingFlowParams)
}

suspend fun restorePurchases(): List<Purchase> {
private suspend fun getPendingPurchase(): List<Purchase> {
val params = QueryPurchasesParams.newBuilder()
.setProductType(ProductType.INAPP)

Expand All @@ -120,14 +98,34 @@ class BillingManager(
}
}

val pendingPurchases = purchasesList.filter { purchase ->
return purchasesList.filter { purchase ->
purchase.purchaseState != Purchase.PurchaseState.PENDING
}
}

// Method to request a purchase
suspend fun requestPurchase(
activity: Activity,
productDetails: ProductDetails
): List<Purchase> = billingClient.ready {
val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build()

val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productDetailsParams))
.build()

val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams)

if (pendingPurchases.isNotEmpty()) {
_purchasesFlow.tryEmit(pendingPurchases)
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
getPendingPurchase()
}

return pendingPurchases
emptyList()
}

suspend fun restorePurchases(): List<Purchase> = billingClient.ready {
getPendingPurchase()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.tonapps.wallet.localization.Localization
sealed class SendBlockchainException(
@StringRes stringRes: Int,
cause: Throwable? = null
): ErrorForUserException(stringRes, cause) {
): ErrorForUserException(stringRes = stringRes, cause = cause) {

companion object {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ fun Context.showToast(test: String) {
navigation?.toast(test)
}

fun Context.loading(loading: Boolean = true) {
navigation?.toastLoading(loading)
}

fun Context.copyWithToast(text: String, color: Int = backgroundContentTintColor) {
navigation?.toast(getString(Localization.copied), color)
copyToClipboard(text)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import android.app.Activity
import android.app.Application
import androidx.lifecycle.viewModelScope
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.tonapps.icu.Coins
import com.tonapps.icu.CurrencyFormatter
import com.tonapps.tonkeeper.Environment
import com.tonapps.tonkeeper.billing.BillingManager
import com.tonapps.tonkeeper.billing.priceFormatted
import com.tonapps.tonkeeper.extensions.loading
import com.tonapps.tonkeeper.extensions.showToast
import com.tonapps.tonkeeper.ui.base.BaseWalletVM
import com.tonapps.tonkeeper.ui.screen.battery.refill.entity.PromoState
Expand Down Expand Up @@ -38,9 +41,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.net.URLEncoder
Expand Down Expand Up @@ -112,11 +112,9 @@ class BatteryRefillViewModel(

uiItems.add(
Item.Refund(
wallet = wallet,
refundUrl = "${api.config.batteryRefundEndpoint}?token=${
wallet = wallet, refundUrl = "${api.config.batteryRefundEndpoint}?token=${
URLEncoder.encode(
tonProofToken,
"UTF-8"
tonProofToken, "UTF-8"
)
}&testnet=${wallet.testnet}"
)
Expand Down Expand Up @@ -169,23 +167,15 @@ class BatteryRefillViewModel(

val transactions = mapOf(
BatteryTransaction.SWAP to charges / BatteryMapper.calculateChargesAmount(
config.batteryMeanPriceSwap,
config.batteryMeanFees
),
BatteryTransaction.NFT to charges / BatteryMapper.calculateChargesAmount(
config.batteryMeanPriceNft,
config.batteryMeanFees
),
BatteryTransaction.JETTON to charges / BatteryMapper.calculateChargesAmount(
config.batteryMeanPriceJetton,
config.batteryMeanFees
config.batteryMeanPriceSwap, config.batteryMeanFees
), BatteryTransaction.NFT to charges / BatteryMapper.calculateChargesAmount(
config.batteryMeanPriceNft, config.batteryMeanFees
), BatteryTransaction.JETTON to charges / BatteryMapper.calculateChargesAmount(
config.batteryMeanPriceJetton, config.batteryMeanFees
)
)

val formattedPrice =
product?.oneTimePurchaseOfferDetails?.formattedPrice ?: context.getString(
Localization.loading
)
val formattedPrice = product?.priceFormatted ?: context.getString(Localization.loading)
uiItems.add(
Item.IAPPack(
position = position,
Expand Down Expand Up @@ -228,9 +218,7 @@ class BatteryRefillViewModel(
val position = ListCell.getPosition(supportedTokens.size + 1, index)
uiItems.add(
Item.RechargeMethod(
wallet = wallet,
position = position,
token = supportToken
wallet = wallet, position = position, token = supportToken
)
)
}
Expand All @@ -252,9 +240,7 @@ class BatteryRefillViewModel(
val tonProofToken =
accountRepository.requestTonProofToken(wallet) ?: return BatteryBalanceEntity.Empty
return batteryRepository.getBalance(
tonProofToken = tonProofToken,
publicKey = wallet.publicKey,
testnet = wallet.testnet
tonProofToken = tonProofToken, publicKey = wallet.publicKey, testnet = wallet.testnet
)
}

Expand All @@ -267,8 +253,7 @@ class BatteryRefillViewModel(
}

private suspend fun getSupportedTokens(
wallet: WalletEntity,
rechargeMethods: List<RechargeMethodEntity>
wallet: WalletEntity, rechargeMethods: List<RechargeMethodEntity>
): List<AccountTokenEntity> {
val tokens = getTokens(wallet)
val supportTokenAddress = rechargeMethods.filter { it.supportRecharge }.mapNotNull {
Expand Down Expand Up @@ -310,12 +295,13 @@ class BatteryRefillViewModel(
}
}

private fun handlePurchase() {
billingManager.purchasesFlow.take(1).onEach { purchases ->
val purchase = purchases?.first()
private suspend fun handlePurchases(purchases: List<Purchase>) {
try {
purchaseInProgress.tryEmit(true)
val tonProofToken = accountRepository.requestTonProofToken(wallet)
?: throw IllegalStateException("proof token is null")
if (purchase != null) {
context.loading()
for (purchase in purchases) {
val request = AndroidBatteryPurchaseRequest(
purchases = listOf(
AndroidBatteryPurchaseRequestPurchasesInner(
Expand All @@ -325,33 +311,35 @@ class BatteryRefillViewModel(
)
)
)
try {
api.battery(wallet.testnet).androidBatteryPurchase(tonProofToken, request)
batteryRepository.getBalance(tonProofToken, wallet.publicKey, wallet.testnet, ignoreCache = true)
billingManager.notifyPurchase()
context.showToast(Localization.battery_refilled)
} catch (e: Exception) {
purchaseInProgress.tryEmit(false)
context.showToast(Localization.error)
}
api.battery(wallet.testnet).androidBatteryPurchase(tonProofToken, request)
batteryRepository.getBalance(
tonProofToken, wallet.publicKey, wallet.testnet, ignoreCache = true
)
billingManager.consumeProduct(purchase.purchaseToken)
}
context.showToast(Localization.battery_refilled)
} catch (e: Exception) {
context.showToast(Localization.error)
} finally {
purchaseInProgress.tryEmit(false)
}.flowOn(Dispatchers.IO).launchIn(viewModelScope)
}
}

fun makePurchase(productId: String, activity: Activity) {
purchaseInProgress.tryEmit(true)
val product = billingManager.productsFlow.value!!.find { it.productId == productId }!!
billingManager.requestPurchase(activity, product)
handlePurchase()
viewModelScope.launch {
val product = billingManager.productsFlow.value!!.find { it.productId == productId }!!
val purchases = billingManager.requestPurchase(activity, product)
handlePurchases(purchases)
}
}

fun restorePurchases() {
viewModelScope.launch {
try {
context.loading()
val pendingPurchases = billingManager.restorePurchases()
if (pendingPurchases.isNotEmpty()) {
handlePurchase()
handlePurchases(pendingPurchases)
} else {
context.showToast(Localization.nothing_to_restore)
}
Expand Down
Loading

0 comments on commit 58913d8

Please sign in to comment.