Skip to content

Commit

Permalink
Merge pull request #78 from tonkeeper/iap
Browse files Browse the repository at this point in the history
battery iap
  • Loading branch information
polstianka authored Oct 1, 2024
2 parents 01e9334 + 2b03915 commit fa6b4cd
Show file tree
Hide file tree
Showing 23 changed files with 608 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.tonapps.wallet.api.entity

import android.net.Uri
import android.os.Parcelable
import android.util.Log
import kotlinx.parcelize.Parcelize
import org.json.JSONObject

Expand Down Expand Up @@ -37,13 +36,15 @@ data class ConfigEntity(
val batteryMeanPriceSwap: String,
val batteryMeanPriceJetton: String,
val disableBatteryCryptoRecharge: Boolean,
val disableBatteryIapModule: Boolean,
val batteryReservedAmount: String,
val batteryMaxInputAmount: String,
val batteryRefundEndpoint: String,
val batteryPromoDisabled: Boolean,
val stakingInfoUrl: String,
val tonapiSSEEndpoint: String,
val tonapiSSETestnetEndpoint: String,
val iapPackages: List<IAPPackageEntity>,
): Parcelable {

val swapUri: Uri
Expand Down Expand Up @@ -83,6 +84,7 @@ data class ConfigEntity(
batterySendDisabled = json.optBoolean("disable_battery_send", false),
batteryMeanFees = json.optString("batteryMeanFees", "0.0055"),
disableBatteryCryptoRecharge = json.optBoolean("disable_battery_crypto_recharge_module", false),
disableBatteryIapModule = json.optBoolean("disable_battery_iap_module", false),
batteryMeanPriceNft = json.optString("batteryMeanPrice_nft", "0.03"),
batteryMeanPriceSwap = json.optString("batteryMeanPrice_swap", "0.22"),
batteryMeanPriceJetton = json.optString("batteryMeanPrice_jetton", "0.06"),
Expand All @@ -93,6 +95,9 @@ data class ConfigEntity(
stakingInfoUrl = json.getString("stakingInfoUrl"),
tonapiSSEEndpoint = json.optString("tonapi_sse_endpoint", "https://rt.tonapi.io"),
tonapiSSETestnetEndpoint = json.optString("tonapi_sse_testnet_endpoint", "https://rt-testnet.tonapi.io"),
iapPackages = json.optJSONArray("iap_packages")?.let { array ->
(0 until array.length()).map { IAPPackageEntity(array.getJSONObject(it)) }
} ?: emptyList()
)

constructor() : this(
Expand Down Expand Up @@ -122,6 +127,7 @@ data class ConfigEntity(
batterySendDisabled = false,
batteryMeanFees = "0.0055",
disableBatteryCryptoRecharge = false,
disableBatteryIapModule = false,
batteryMeanPriceNft = "0.03",
batteryMeanPriceSwap = "0.22",
batteryMeanPriceJetton = "0.06",
Expand All @@ -132,6 +138,7 @@ data class ConfigEntity(
stakingInfoUrl = "https://ton.org/stake",
tonapiSSEEndpoint = "https://rt.tonapi.io",
tonapiSSETestnetEndpoint = "https://rt-testnet.tonapi.io",
iapPackages = emptyList()
)

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.tonapps.wallet.api.entity

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.json.JSONObject

@Parcelize
data class IAPPackageEntity(
val id: IAPPackageId,
val userProceed: Double,
val productId: String,
) : Parcelable {

constructor(json: JSONObject) : this(
id = IAPPackageId.fromId(json.getString("id")),
userProceed = json.getDouble("user_proceed"),
productId = json.getString("package_id_android"),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.tonapps.wallet.api.entity

enum class IAPPackageId(val id: String) {
LARGE("large"),
MEDIUM("medium"),
SMALL("small");

companion object {
fun fromId(id: String): IAPPackageId {
return values().find { it.id.equals(id, ignoreCase = true) }
?: throw IllegalArgumentException("Invalid IAPPackageId id: $id")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.tonapps.wallet.api.internal
import android.content.Context
import android.net.Uri
import android.util.ArrayMap
import com.tonapps.extensions.appVersionName
import com.tonapps.extensions.isDebug
import com.tonapps.extensions.locale
import com.tonapps.network.get
Expand Down Expand Up @@ -39,7 +40,7 @@ internal class InternalApi(
path: String,
testnet: Boolean,
platform: String = "android", // "android_x"
build: String = "4.9.0", // context.packageInfo.versionName.removeSuffix("-debug"), //
build: String = context.appVersionName,
locale: Locale,
): JSONObject {
val url = endpoint(path, testnet, platform, build)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ object BatteryMapper {
fun calculateCryptoCharges(method: RechargeMethodEntity, meanFees: String, amount: Coins): Int {
val meanFeesBigDecimal = BigDecimal(meanFees)
val rateBigDecimal = BigDecimal(method.rate)
return rateBigDecimal.divide(meanFeesBigDecimal, 20, RoundingMode.HALF_UP).multiply(amount.value).setScale(0, RoundingMode.FLOOR).toInt()
return rateBigDecimal.divide(meanFeesBigDecimal, 20, RoundingMode.HALF_UP)
.multiply(amount.value).setScale(0, RoundingMode.FLOOR).toInt()
}

fun calculateIapCharges(
userProceed: Double,
tonPriceInUsd: Coins,
reservedAmount: BigDecimal,
meanFees: BigDecimal
): Int {
return userProceed.toBigDecimal()
.divide(tonPriceInUsd.value, 20, RoundingMode.HALF_UP)
.minus(reservedAmount)
.divide(meanFees, 20, RoundingMode.HALF_UP)
.setScale(0, RoundingMode.FLOOR)
.toInt()

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ data class WalletCurrency(
val DEFAULT = WalletCurrency(FIAT.first())
val TON = WalletCurrency("TON")

val USD = WalletCurrency("USD", true)

val ALL = FIAT + CRYPTO

fun of(code: String?): WalletCurrency {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,109 @@
package com.tonapps.tonkeeper.billing

import com.android.billingclient.api.Purchase
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import android.app.Activity
import android.content.Context
import com.android.billingclient.api.*
import com.android.billingclient.api.BillingClient.ProductType
import com.tonapps.extensions.MutableEffectFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull

class BillingManager {
class BillingManager(
context: Context,
) {

private var billingClient: BillingClient

// Example
private val _purchasesFlow = MutableSharedFlow<List<Purchase>>()
private val _purchasesFlow = MutableEffectFlow<List<Purchase>?>()
val purchasesFlow = _purchasesFlow.asSharedFlow()

private val purchasesUpdatedListener = { purchases: List<Purchase> ->
// _purchasesFlow.value = purchases
private val _productsFlow = MutableStateFlow<List<ProductDetails>?>(null)
val productsFlow = _productsFlow.asStateFlow()

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
}
})
}

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

val productList = productIds.map { productId ->
QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(productType)
.build()
}

val params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()

billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
_productsFlow.value = productDetailsList
} else {
_productsFlow.value = emptyList() // In case of an error
}
}
}

// 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() {
val params = QueryPurchasesParams.newBuilder()
.setProductType(ProductType.INAPP)

billingClient.queryPurchasesAsync(params.build()) { billingResult, purchasesList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
val pendingPurchases = purchasesList.filter { purchase ->
purchase.purchaseState != Purchase.PurchaseState.PENDING
}
if (pendingPurchases.isNotEmpty()) {
_purchasesFlow.tryEmit(pendingPurchases)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ class BatteryRefillScreen(wallet: WalletEntity) : BaseHolderWalletScreen.ChildLi

private val adapter = Adapter(
openSettings = { primaryViewModel.routeToSettings() },
onSubmitPromo = { viewModel.applyPromo(it) }
onSubmitPromo = { viewModel.applyPromo(it) },
onPackSelect = { viewModel.makePurchase(it, requireActivity()) },
onRestorePurchases = { viewModel.restorePurchases() }
)

override fun onCreate(savedInstanceState: Bundle?) {
Expand Down
Loading

0 comments on commit fa6b4cd

Please sign in to comment.