mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-04 10:19:18 +02:00
Migrate to the (required) new billing library
This commit is contained in:
parent
f535cb4767
commit
b71009d1cb
23 changed files with 410 additions and 663 deletions
|
@ -203,7 +203,8 @@ dependencies {
|
|||
implementation 'com.squareup.okhttp3:okhttp-tls:4.9.0'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.8.1'
|
||||
|
||||
googleApiImplementation 'org.solovyev.android:checkout:1.2.1'
|
||||
googleApiImplementation "com.android.billingclient:billing:3.0.3"
|
||||
googleApiImplementation "com.android.billingclient:billing-ktx:3.0.3"
|
||||
|
||||
implementation('io.socket:socket.io-client:1.0.0') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -17,7 +17,6 @@ package io.timelimit.android
|
|||
|
||||
import android.app.Application
|
||||
import com.jakewharton.threetenabp.AndroidThreeTen
|
||||
import org.solovyev.android.checkout.Billing
|
||||
|
||||
class Application : Application() {
|
||||
override fun onCreate() {
|
||||
|
@ -25,8 +24,4 @@ class Application : Application() {
|
|||
|
||||
AndroidThreeTen.init(this)
|
||||
}
|
||||
|
||||
val billing = Billing(this, object: Billing.DefaultConfiguration() {
|
||||
override fun getPublicKey() = BuildConfig.googlePlayKey
|
||||
})
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -13,23 +13,17 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.extensions
|
||||
|
||||
package org.solovyev.android.checkout
|
||||
import com.android.billingclient.api.BillingClient
|
||||
import com.android.billingclient.api.BillingResult
|
||||
import java.lang.RuntimeException
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
object ActivityCheckout: Checkout() {
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
// do nothing
|
||||
fun BillingResult.assertSuccess() {
|
||||
if (this.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||
throw BillingClientException("error during processing billing request: ${this.debugMessage}")
|
||||
}
|
||||
|
||||
fun start() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
fun createPurchaseFlow(listener: RequestListener<Purchase>) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
val purchaseFlow = PurchaseFlow
|
||||
}
|
||||
|
||||
open class BillingClientException(message: String): RuntimeException(message)
|
||||
class BillingNotSupportedException(): BillingClientException("billing not supported")
|
|
@ -1,125 +0,0 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.extensions
|
||||
|
||||
import org.solovyev.android.checkout.*
|
||||
import java.io.Closeable
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
suspend fun Checkout.startAsync(): CheckoutStartResponse {
|
||||
val checkout = this
|
||||
var resumed = false
|
||||
|
||||
return suspendCoroutine<CheckoutStartResponse> {
|
||||
continuation ->
|
||||
|
||||
checkout.start(object: Checkout.EmptyListener() {
|
||||
override fun onReady(requests: BillingRequests, product: String, billingSupported: Boolean) {
|
||||
if (!resumed) {
|
||||
resumed = true
|
||||
|
||||
continuation.resume(CheckoutStartResponse(
|
||||
requests = requests,
|
||||
product = product,
|
||||
billingSupported = billingSupported,
|
||||
checkout = checkout
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Checkout.waitUntilReady(): CheckoutStartResponse {
|
||||
val checkout = this
|
||||
var resumed = false
|
||||
|
||||
return suspendCoroutine {
|
||||
continuation ->
|
||||
|
||||
checkout.whenReady(object: Checkout.EmptyListener() {
|
||||
override fun onReady(requests: BillingRequests, product: String, billingSupported: Boolean) {
|
||||
if (!resumed) {
|
||||
resumed = true
|
||||
|
||||
continuation.resume(CheckoutStartResponse(
|
||||
requests = requests,
|
||||
product = product,
|
||||
billingSupported = billingSupported,
|
||||
checkout = checkout
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun BillingRequests.getSkusAsync(product: String, skus: List<String>): Skus {
|
||||
val requests = this
|
||||
|
||||
return suspendCoroutine {
|
||||
continuation ->
|
||||
|
||||
requests.getSkus(product, skus, object: RequestListener<Skus> {
|
||||
override fun onError(response: Int, e: Exception) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
|
||||
override fun onSuccess(result: Skus) {
|
||||
continuation.resume(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun BillingRequests.consumeAsync(token: String) {
|
||||
val requests = this
|
||||
|
||||
suspendCoroutine<Any> {
|
||||
continuation ->
|
||||
|
||||
requests.consume(token, object: RequestListener<Any> {
|
||||
override fun onError(response: Int, e: java.lang.Exception) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
|
||||
override fun onSuccess(result: Any) {
|
||||
continuation.resume(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Inventory.loadAsync(request: Inventory.Request) = suspendCoroutine<Inventory.Products> { continuation ->
|
||||
this.load(request, object: Inventory.Callback {
|
||||
override fun onLoaded(products: Inventory.Products) {
|
||||
continuation.resume(products)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
class CheckoutStartResponse (
|
||||
val requests: BillingRequests,
|
||||
val product: String,
|
||||
val billingSupported: Boolean,
|
||||
private val checkout: Checkout
|
||||
): Closeable {
|
||||
override fun close() {
|
||||
checkout.stop()
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ package io.timelimit.android.ui
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
|
@ -30,6 +31,7 @@ import androidx.navigation.NavController
|
|||
import androidx.navigation.fragment.NavHostFragment
|
||||
import io.timelimit.android.Application
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
|
@ -50,8 +52,6 @@ import io.timelimit.android.ui.payment.ActivityPurchaseModel
|
|||
import io.timelimit.android.ui.setup.SetupTermsFragment
|
||||
import io.timelimit.android.ui.setup.parent.SetupParentModeFragment
|
||||
import io.timelimit.android.ui.util.SyncStatusModel
|
||||
import org.solovyev.android.checkout.ActivityCheckout
|
||||
import org.solovyev.android.checkout.Checkout
|
||||
|
||||
class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||
companion object {
|
||||
|
@ -62,15 +62,10 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
|
||||
private val currentNavigatorFragment = MutableLiveData<Fragment?>()
|
||||
private val application: Application by lazy { getApplication() as Application }
|
||||
private val checkout: ActivityCheckout by lazy { Checkout.forActivity(this, application.billing) }
|
||||
private val syncModel: SyncStatusModel by lazy {
|
||||
ViewModelProviders.of(this).get(SyncStatusModel::class.java)
|
||||
}
|
||||
val purchaseModel: ActivityPurchaseModel by lazy {
|
||||
ViewModelProviders.of(this).get(ActivityPurchaseModel::class.java).apply {
|
||||
setActivityCheckout(checkout)
|
||||
}
|
||||
}
|
||||
val purchaseModel: ActivityPurchaseModel by viewModels()
|
||||
override var ignoreStop: Boolean = false
|
||||
override val showPasswordRecovery: Boolean = true
|
||||
|
||||
|
@ -238,12 +233,6 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
checkout.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun getActivityViewModel(): ActivityViewModel {
|
||||
return ViewModelProviders.of(this).get(ActivityViewModel::class.java)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -15,18 +15,17 @@
|
|||
*/
|
||||
package io.timelimit.android.ui.payment
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.android.billingclient.api.*
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.extensions.consumeAsync
|
||||
import io.timelimit.android.extensions.startAsync
|
||||
import io.timelimit.android.extensions.waitUntilReady
|
||||
import io.timelimit.android.livedata.castDown
|
||||
import io.timelimit.android.extensions.*
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.mergeLiveData
|
||||
import io.timelimit.android.livedata.setTemporarily
|
||||
|
@ -34,25 +33,26 @@ import io.timelimit.android.logic.DefaultAppLogic
|
|||
import io.timelimit.android.sync.network.CanDoPurchaseStatus
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.solovyev.android.checkout.*
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class ActivityPurchaseModel(application: Application): AndroidViewModel(application) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "ActivityPurchaseModel"
|
||||
}
|
||||
|
||||
private val billing = (application as io.timelimit.android.Application).billing
|
||||
private val logic = DefaultAppLogic.with(application)
|
||||
private val lock = Mutex()
|
||||
private val clientMutex = Mutex()
|
||||
private val processMutex = Mutex()
|
||||
private var _billingClient: BillingClient? = null
|
||||
|
||||
private val isWorkingInternal = MutableLiveData<Boolean>().apply { value = false }
|
||||
private val hadErrorInternal = MutableLiveData<Boolean>().apply { value = false }
|
||||
private val processPurchaseSuccessInternal = MutableLiveData<Boolean>().apply { value = false }
|
||||
|
||||
val isWorking = isWorkingInternal.castDown()
|
||||
val hadError = hadErrorInternal.castDown()
|
||||
val processPurchaseSuccess = processPurchaseSuccessInternal.castDown()
|
||||
val status = mergeLiveData(isWorking, hadError, processPurchaseSuccess).map {
|
||||
val status = mergeLiveData(isWorkingInternal, hadErrorInternal, processPurchaseSuccessInternal).map {
|
||||
(working, error, success) ->
|
||||
|
||||
if (success != null && success) {
|
||||
|
@ -70,120 +70,207 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
|
|||
processPurchaseSuccessInternal.value = false
|
||||
}
|
||||
|
||||
private var activityCheckout: ActivityCheckout? = null
|
||||
|
||||
fun setActivityCheckout(checkout: ActivityCheckout) {
|
||||
checkout.start()
|
||||
|
||||
checkout.createPurchaseFlow(object: RequestListener<Purchase> {
|
||||
override fun onError(response: Int, e: Exception) {
|
||||
// ignored
|
||||
private suspend fun <R> initAndUseClient(block: suspend (client: BillingClient) -> R): R {
|
||||
clientMutex.withLock {
|
||||
if (_billingClient == null) {
|
||||
_billingClient = BillingClient.newBuilder(getApplication())
|
||||
.enablePendingPurchases()
|
||||
.setListener(purchaseUpdatedListener)
|
||||
.build()
|
||||
}
|
||||
|
||||
val initBillingClient = _billingClient!!
|
||||
|
||||
suspendCoroutine<Unit?> { continuation ->
|
||||
initBillingClient.startConnection(object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||
try {
|
||||
billingResult.assertSuccess()
|
||||
|
||||
continuation.resume(null)
|
||||
} catch (ex: BillingClientException) {
|
||||
_billingClient = null
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "error during connecting", ex)
|
||||
}
|
||||
|
||||
continuation.resumeWithException(BillingNotSupportedException())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBillingServiceDisconnected() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "client disconnected")
|
||||
}
|
||||
|
||||
override fun onSuccess(result: Purchase) {
|
||||
if (PurchaseIds.BUY_SKUS.contains(result.sku)) {
|
||||
runAsync {
|
||||
lock.withLock {
|
||||
isWorkingInternal.setTemporarily(true).use { _ ->
|
||||
handlePurchase(result)
|
||||
}
|
||||
}
|
||||
clientMutex.withLock {
|
||||
if (_billingClient === initBillingClient) { _billingClient = null }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
activityCheckout = checkout
|
||||
}
|
||||
|
||||
fun forgetActivityCheckout() {
|
||||
activityCheckout?.stop()
|
||||
activityCheckout = null
|
||||
return block(initBillingClient)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun querySkus(skuIds: List<String>): List<SkuDetails> = initAndUseClient { client ->
|
||||
val (billingResult, data) = client.querySkuDetails(
|
||||
SkuDetailsParams.newBuilder()
|
||||
.setSkusList(skuIds)
|
||||
.setType(BillingClient.SkuType.INAPP)
|
||||
.build()
|
||||
)
|
||||
|
||||
billingResult.assertSuccess()
|
||||
|
||||
data ?: throw BillingClientException("empty response")
|
||||
}
|
||||
|
||||
suspend fun queryPurchases() = initAndUseClient { client ->
|
||||
val response = client.queryPurchases(BillingClient.SkuType.INAPP)
|
||||
|
||||
response.billingResult.assertSuccess()
|
||||
|
||||
response.purchasesList!!.filter {
|
||||
it.purchaseState == Purchase.PurchaseState.PURCHASED
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun queryAndProcessPurchases() {
|
||||
processMutex.withLock {
|
||||
isWorkingInternal.setTemporarily(true).use {
|
||||
try {
|
||||
initAndUseClient { client ->
|
||||
val result = client.queryPurchases(BillingClient.SkuType.INAPP)
|
||||
|
||||
result.billingResult.assertSuccess()
|
||||
|
||||
for (purchase in result.purchasesList!!) {
|
||||
handlePurchase(purchase, client)
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "queryAndProcessPurchases() failed", ex)
|
||||
}
|
||||
|
||||
hadErrorInternal.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun queryAndProcessPurchasesAsync() {
|
||||
runAsync { queryAndProcessPurchases() }
|
||||
}
|
||||
|
||||
private val purchaseUpdatedListener: PurchasesUpdatedListener = object: PurchasesUpdatedListener {
|
||||
override fun onPurchasesUpdated(p0: BillingResult, p1: MutableList<Purchase>?) {
|
||||
runAsync {
|
||||
lock.withLock {
|
||||
isWorkingInternal.setTemporarily(true).use { _ ->
|
||||
val checkout = activityCheckout
|
||||
|
||||
if (checkout != null) {
|
||||
val inventory = checkout.makeInventory()
|
||||
|
||||
inventory.load(
|
||||
Inventory.Request.create()
|
||||
.loadAllPurchases(),
|
||||
object: Inventory.Callback {
|
||||
override fun onLoaded(products: Inventory.Products) {
|
||||
products[ProductTypes.IN_APP].purchases.forEach { purchase ->
|
||||
if (PurchaseIds.BUY_SKUS.contains(purchase.sku)) {
|
||||
runAsync {
|
||||
handlePurchase(purchase)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handlePurchase(purchase: Purchase) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "handlePurchase()")
|
||||
}
|
||||
|
||||
processMutex.withLock {
|
||||
isWorkingInternal.setTemporarily(true).use {
|
||||
initAndUseClient { client ->
|
||||
try {
|
||||
p0.assertSuccess()
|
||||
|
||||
for (purchase in p1!!) {
|
||||
handlePurchase(purchase, client)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "onPurchasesUpdated() failed", ex)
|
||||
}
|
||||
|
||||
hadErrorInternal.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun forgetActivityCheckout() {
|
||||
runAsync {
|
||||
clientMutex.withLock {
|
||||
_billingClient?.endConnection()
|
||||
_billingClient = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handlePurchase(purchase: Purchase, billingClient: BillingClient) {
|
||||
if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED || purchase.isAcknowledged) {
|
||||
// we are not interested in it
|
||||
return
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "handlePurchase($purchase)")
|
||||
}
|
||||
|
||||
if (PurchaseIds.SAL_SKUS.contains(purchase.sku)) {
|
||||
// just acknowledge
|
||||
|
||||
billingClient.acknowledgePurchase(
|
||||
AcknowledgePurchaseParams.newBuilder()
|
||||
.setPurchaseToken(purchase.purchaseToken)
|
||||
.build()
|
||||
).assertSuccess()
|
||||
} else if (PurchaseIds.BUY_SKUS.contains(purchase.sku)) {
|
||||
// send and consume
|
||||
|
||||
val server = logic.serverLogic.getServerConfigCoroutine()
|
||||
|
||||
if (server.hasAuthToken) {
|
||||
server.api.finishPurchaseByGooglePlay(
|
||||
receipt = purchase.data,
|
||||
receipt = purchase.originalJson,
|
||||
signature = purchase.signature,
|
||||
deviceAuthToken = server.deviceAuthToken
|
||||
)
|
||||
}
|
||||
|
||||
billingClient.consumePurchase(
|
||||
ConsumeParams.newBuilder()
|
||||
.setPurchaseToken(purchase.purchaseToken)
|
||||
.build()
|
||||
)
|
||||
|
||||
processPurchaseSuccessInternal.value = true
|
||||
consumePurchaseAsync(purchase)
|
||||
} catch (ex: Exception) {
|
||||
hadErrorInternal.value = true
|
||||
|
||||
} else {
|
||||
Log.w(LOG_TAG, "purchase for the premium version but no server available")
|
||||
}
|
||||
} else {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "server rejected purchase", ex)
|
||||
Log.d(LOG_TAG, "don't know how to handle ${purchase.sku}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun consumePurchaseAsync(purchase: Purchase) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "consumePurchaseAsync()")
|
||||
}
|
||||
|
||||
fun startPurchase(sku: String, checkAtBackend: Boolean, activity: Activity) {
|
||||
runAsync {
|
||||
lock.withLock {
|
||||
try {
|
||||
Checkout.forApplication(billing).startAsync().use {
|
||||
it.requests.consumeAsync(purchase.token)
|
||||
}
|
||||
val skuDetails = querySkus(listOf(sku)).single()
|
||||
|
||||
if (skuDetails.sku != sku) throw IllegalStateException()
|
||||
|
||||
startPurchase(skuDetails, checkAtBackend, activity)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "consumePurchaseAsync() failed", ex)
|
||||
}
|
||||
Log.d(LOG_TAG, "could not start purchase", ex)
|
||||
}
|
||||
|
||||
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startPurchase(sku: String, checkAtBackend: Boolean) {
|
||||
fun startPurchase(skuDetails: SkuDetails, checkAtBackend: Boolean, activity: Activity) {
|
||||
runAsync {
|
||||
lock.withLock {
|
||||
isWorkingInternal.setTemporarily(true).use {
|
||||
_ ->
|
||||
|
||||
initAndUseClient { client ->
|
||||
try {
|
||||
if (checkAtBackend) {
|
||||
val server = logic.serverLogic.getServerConfigCoroutine()
|
||||
|
@ -191,7 +278,7 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
|
|||
if (!server.hasAuthToken) {
|
||||
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||
|
||||
return@runAsync
|
||||
return@initAndUseClient
|
||||
}
|
||||
|
||||
if (!(server.api.canDoPurchase(server.deviceAuthToken) is CanDoPurchaseStatus.Yes)) {
|
||||
|
@ -199,21 +286,12 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
|
|||
}
|
||||
}
|
||||
|
||||
// start the purchase
|
||||
val activityCheckout = activityCheckout
|
||||
|
||||
if (activityCheckout == null) {
|
||||
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||
|
||||
return@runAsync
|
||||
}
|
||||
|
||||
activityCheckout.waitUntilReady().requests.purchase(
|
||||
ProductTypes.IN_APP,
|
||||
sku,
|
||||
null,
|
||||
activityCheckout.purchaseFlow
|
||||
)
|
||||
client.launchBillingFlow(
|
||||
activity,
|
||||
BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(skuDetails)
|
||||
.build()
|
||||
).assertSuccess()
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "could not start purchase", ex)
|
||||
|
@ -224,7 +302,6 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class ActivityPurchaseModelStatus {
|
||||
|
|
|
@ -20,9 +20,9 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.databinding.FragmentPurchaseBinding
|
||||
import io.timelimit.android.livedata.liveDataFromNullableValue
|
||||
|
@ -32,9 +32,7 @@ import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
|||
|
||||
class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
||||
private val activityModel: ActivityPurchaseModel by lazy { (activity as MainActivity).purchaseModel }
|
||||
private val model: PurchaseModel by lazy {
|
||||
ViewModelProviders.of(this).get(PurchaseModel::class.java)
|
||||
}
|
||||
private val model: PurchaseModel by viewModels()
|
||||
|
||||
companion object {
|
||||
private const val PAGE_BUY = 0
|
||||
|
@ -49,22 +47,30 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
|||
if (savedInstanceState == null) {
|
||||
activityModel.resetProcessPurchaseSuccess()
|
||||
}
|
||||
|
||||
model.retry(activityModel)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = FragmentPurchaseBinding.inflate(inflater, container, false)
|
||||
var processingPurchaseError = false
|
||||
|
||||
mergeLiveData(activityModel.status, model.status).observe(this, Observer {
|
||||
mergeLiveData(activityModel.status, model.status).observe(viewLifecycleOwner, {
|
||||
status ->
|
||||
|
||||
val (activityStatus, fragmentStatus) = status!!
|
||||
|
||||
if (activityStatus != ActivityPurchaseModelStatus.Idle) {
|
||||
if (fragmentStatus != null) {
|
||||
when (fragmentStatus) {
|
||||
PurchaseFragmentPreparing -> binding.flipper.displayedChild = PAGE_WAIT
|
||||
is PurchaseFragmentReady -> {
|
||||
when (activityStatus) {
|
||||
null -> binding.flipper.displayedChild = PAGE_WAIT
|
||||
ActivityPurchaseModelStatus.Working -> binding.flipper.displayedChild = PAGE_WAIT
|
||||
ActivityPurchaseModelStatus.Idle -> throw IllegalStateException()
|
||||
ActivityPurchaseModelStatus.Idle -> {
|
||||
binding.flipper.displayedChild = PAGE_BUY
|
||||
binding.priceData = fragmentStatus
|
||||
}
|
||||
ActivityPurchaseModelStatus.Error -> {
|
||||
binding.flipper.displayedChild = PAGE_ERROR
|
||||
|
||||
|
@ -74,12 +80,6 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
|||
}
|
||||
ActivityPurchaseModelStatus.Done -> binding.flipper.displayedChild = PAGE_DONE
|
||||
}.let { }
|
||||
} else if (fragmentStatus != null) {
|
||||
when (fragmentStatus) {
|
||||
PurchaseFragmentPreparing -> binding.flipper.displayedChild = PAGE_WAIT
|
||||
is PurchaseFragmentReady -> {
|
||||
binding.flipper.displayedChild = PAGE_BUY
|
||||
binding.priceData = fragmentStatus
|
||||
}
|
||||
is PurchaseFragmentError -> {
|
||||
binding.flipper.displayedChild = PAGE_ERROR
|
||||
|
@ -110,16 +110,16 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
|||
if (processingPurchaseError) {
|
||||
activityModel.queryAndProcessPurchasesAsync()
|
||||
} else {
|
||||
model.retry()
|
||||
model.retry(activityModel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun buyForOneMonth() {
|
||||
activityModel.startPurchase(PurchaseIds.SKU_MONTH, checkAtBackend = true)
|
||||
activityModel.startPurchase(PurchaseIds.SKU_MONTH, checkAtBackend = true, activity = requireActivity())
|
||||
}
|
||||
|
||||
override fun buyForOneYear() {
|
||||
activityModel.startPurchase(PurchaseIds.SKU_YEAR, checkAtBackend = true)
|
||||
activityModel.startPurchase(PurchaseIds.SKU_YEAR, checkAtBackend = true, activity = requireActivity())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -20,5 +20,5 @@ object PurchaseIds {
|
|||
const val SKU_MONTH = "premium_month_2018"
|
||||
|
||||
val BUY_SKUS = listOf(SKU_YEAR, SKU_MONTH)
|
||||
val SLA_SKUS = listOf("sal100", "sal500", "sal1000", "sal2000")
|
||||
val SAL_SKUS = listOf("sal100", "sal500", "sal1000", "sal2000")
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -21,36 +21,28 @@ import androidx.lifecycle.AndroidViewModel
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.extensions.getSkusAsync
|
||||
import io.timelimit.android.extensions.startAsync
|
||||
import io.timelimit.android.extensions.BillingNotSupportedException
|
||||
import io.timelimit.android.livedata.castDown
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.network.CanDoPurchaseStatus
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.solovyev.android.checkout.Checkout
|
||||
import org.solovyev.android.checkout.ProductTypes
|
||||
|
||||
class PurchaseModel(application: Application): AndroidViewModel(application) {
|
||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(application) }
|
||||
private val application: io.timelimit.android.Application by lazy { application as io.timelimit.android.Application }
|
||||
private val statusInternal = MutableLiveData<PurchaseFragmentStatus>()
|
||||
private val lock = Mutex()
|
||||
|
||||
val status = statusInternal.castDown()
|
||||
|
||||
init {
|
||||
prepare()
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (this.status.value is PurchaseFragmentRecoverableError) {
|
||||
prepare()
|
||||
fun retry(activityPurchaseModel: ActivityPurchaseModel) {
|
||||
if (this.status.value is PurchaseFragmentRecoverableError || this.status.value == null) {
|
||||
prepare(activityPurchaseModel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepare() {
|
||||
private fun prepare(activityPurchaseModel: ActivityPurchaseModel) {
|
||||
runAsync {
|
||||
lock.withLock {
|
||||
try {
|
||||
|
@ -70,30 +62,21 @@ class PurchaseModel(application: Application): AndroidViewModel(application) {
|
|||
if (canDoPurchase.publicKey?.contentEquals(Base64.decode(BuildConfig.googlePlayKey, 0)) == false) {
|
||||
statusInternal.value = PurchaseFragmentServerHasDifferentPublicKey
|
||||
} else {
|
||||
val checkout = Checkout.forApplication(application.billing)
|
||||
|
||||
checkout.startAsync().use {
|
||||
if (!it.billingSupported) {
|
||||
statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByDevice
|
||||
} else {
|
||||
val skus = it.requests.getSkusAsync(
|
||||
ProductTypes.IN_APP,
|
||||
PurchaseIds.BUY_SKUS
|
||||
)
|
||||
val skus = activityPurchaseModel.querySkus(PurchaseIds.BUY_SKUS)
|
||||
|
||||
statusInternal.value = PurchaseFragmentReady(
|
||||
monthPrice = skus.getSku(PurchaseIds.SKU_MONTH)?.price.toString(),
|
||||
yearPrice = skus.getSku(PurchaseIds.SKU_YEAR)?.price.toString()
|
||||
monthPrice = skus.find { it.sku == PurchaseIds.SKU_MONTH }?.price.toString(),
|
||||
yearPrice = skus.find { it.sku == PurchaseIds.SKU_YEAR }?.price.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (canDoPurchase == CanDoPurchaseStatus.NotDueToOldPurchase) {
|
||||
statusInternal.value = PurchaseFragmentExistingPaymentError
|
||||
} else {
|
||||
statusInternal.value = PurchaseFragmentServerRejectedError
|
||||
}
|
||||
}
|
||||
} catch (ex: BillingNotSupportedException) {
|
||||
statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByDevice
|
||||
} catch (ex: Exception) {
|
||||
statusInternal.value = PurchaseFragmentNetworkError
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.databinding.StayAwesomeFragmentBinding
|
||||
import io.timelimit.android.databinding.StayAwesomeFragmentItemBinding
|
||||
|
@ -31,13 +31,11 @@ import io.timelimit.android.ui.MainActivity
|
|||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||
|
||||
class StayAwesomeFragment : Fragment(), FragmentWithCustomTitle {
|
||||
val model: StayAwesomeModel by lazy {
|
||||
ViewModelProviders.of(this).get(StayAwesomeModel::class.java)
|
||||
}
|
||||
private val model: StayAwesomeModel by viewModels()
|
||||
private val activityModel get() = (activity as MainActivity).purchaseModel
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = StayAwesomeFragmentBinding.inflate(inflater, container, false)
|
||||
val activityModel = (activity as MainActivity).purchaseModel
|
||||
|
||||
model.status.observe(viewLifecycleOwner, Observer { status ->
|
||||
when (status!!) {
|
||||
|
@ -62,7 +60,7 @@ class StayAwesomeFragment : Fragment(), FragmentWithCustomTitle {
|
|||
|
||||
if (!item.bought) {
|
||||
view.card.setOnClickListener {
|
||||
activityModel.startPurchase(item.id, checkAtBackend = false)
|
||||
activityModel.startPurchase(item.id, checkAtBackend = false, requireActivity())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +80,7 @@ class StayAwesomeFragment : Fragment(), FragmentWithCustomTitle {
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
model.load()
|
||||
model.load(activityModel)
|
||||
}
|
||||
|
||||
override fun getCustomTitle(): LiveData<String?> = liveDataFromNullableValue("${getString(R.string.about_sal)} < ${getString(R.string.main_tab_overview)}")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -20,21 +20,13 @@ import androidx.lifecycle.AndroidViewModel
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.extensions.getSkusAsync
|
||||
import io.timelimit.android.extensions.loadAsync
|
||||
import io.timelimit.android.extensions.startAsync
|
||||
import io.timelimit.android.livedata.castDown
|
||||
import org.solovyev.android.checkout.Checkout
|
||||
import org.solovyev.android.checkout.Inventory
|
||||
import org.solovyev.android.checkout.ProductTypes
|
||||
|
||||
class StayAwesomeModel(application: Application): AndroidViewModel(application) {
|
||||
private val application: io.timelimit.android.Application by lazy { application as io.timelimit.android.Application }
|
||||
private val statusInternal = MutableLiveData<StayAwesomeStatus>()
|
||||
|
||||
val status = statusInternal.castDown()
|
||||
|
||||
fun load() {
|
||||
fun load(activityPurchaseModel: ActivityPurchaseModel) {
|
||||
runAsync {
|
||||
statusInternal.value = LoadingStayAwesomeStatus
|
||||
|
||||
|
@ -44,41 +36,27 @@ class StayAwesomeModel(application: Application): AndroidViewModel(application)
|
|||
return@runAsync
|
||||
}
|
||||
|
||||
val checkout = Checkout.forApplication(application.billing)
|
||||
|
||||
checkout.startAsync().use {
|
||||
if (!it.billingSupported) {
|
||||
statusInternal.value = NotSupportedByDeviceStayAwesomeStatus
|
||||
} else {
|
||||
val skus = it.requests.getSkusAsync(
|
||||
ProductTypes.IN_APP,
|
||||
PurchaseIds.SLA_SKUS
|
||||
)
|
||||
|
||||
val inventory = checkout.makeInventory()
|
||||
val products = inventory.loadAsync(Inventory.Request.create().loadAllPurchases())
|
||||
val purchasedSkus = products[ProductTypes.IN_APP].purchases.map { it.sku }.toSet()
|
||||
try {
|
||||
val skus = activityPurchaseModel.querySkus(PurchaseIds.SAL_SKUS)
|
||||
val purchases = activityPurchaseModel.queryPurchases()
|
||||
|
||||
statusInternal.value = ReadyStayAwesomeStatus(
|
||||
PurchaseIds.SLA_SKUS.map { skuId ->
|
||||
val sku = skus.getSku(skuId)
|
||||
PurchaseIds.SAL_SKUS.map { skuId ->
|
||||
val sku = skus.find { it.sku == skuId }
|
||||
|
||||
StayAwesomeItem(
|
||||
id = skuId,
|
||||
title = sku?.description ?: skuId,
|
||||
price = sku?.price ?: "???",
|
||||
bought = purchasedSkus.contains(skuId)
|
||||
bought = purchases.find { it.sku == skuId } != null
|
||||
)
|
||||
}
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
statusInternal.value = NotSupportedByDeviceStayAwesomeStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
load()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class StayAwesomeStatus
|
||||
|
|
|
@ -92,8 +92,8 @@
|
|||
(<a href="https://github.com/KeepSafe/TapTargetView/blob/master/LICENSE">Apache License, Version 2.0</a>
|
||||
\n<a href="https://github.com/google/flexbox-layout">Flexbox Layout</a>
|
||||
(<a href="https://github.com/google/flexbox-layout/blob/master/LICENSE">Apache License, Version 2.0</a>)
|
||||
\n<a href="https://github.com/serso/android-checkout">Checkout</a>
|
||||
(<a href="https://github.com/serso/android-checkout/blob/master/LICENSE.txt">Apache License, Version 2.0</a>)
|
||||
\nGoogle Play Billing Library (only in builds in the Play Store)
|
||||
(<a href="https://developer.android.com/google/play/billing/release-notes"> Android Software Development Kit License Agreement</a>)
|
||||
\n<a href="https://github.com/square/okhttp">OkHttp</a>
|
||||
(<a href="https://github.com/square/okhttp/blob/master/LICENSE.txt">Apache License, Version 2.0</a>)
|
||||
\n<a href="https://github.com/socketio/socket.io-client-java">Socket.io-Java-Client</a>
|
||||
|
|
132
app/src/noGoogleApi/java/com/android/billingclient/api/Mocks.kt
Normal file
132
app/src/noGoogleApi/java/com/android/billingclient/api/Mocks.kt
Normal file
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.android.billingclient.api
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import io.timelimit.android.async.Threads
|
||||
|
||||
object BillingClient {
|
||||
fun newBuilder(application: Application) = Builder
|
||||
|
||||
fun startConnection(listener: BillingClientStateListener) {
|
||||
Threads.mainThreadHandler.post { listener.onBillingSetupFinished(BillingResult) }
|
||||
}
|
||||
|
||||
fun endConnection() {}
|
||||
|
||||
fun querySkuDetails(param: SkuDetailsParams) = QuerySkuDetailsResult.instance
|
||||
fun launchBillingFlow(activity: Activity, params: BillingFlowParams) = BillingResult
|
||||
fun acknowledgePurchase(params: AcknowledgePurchaseParams) = BillingResult
|
||||
fun consumePurchase(params: ConsumeParams) = BillingResult
|
||||
fun queryPurchases(type: String) = QueryPurchasesResult
|
||||
|
||||
object BillingResponseCode {
|
||||
const val OK = 0
|
||||
const val ERR = 1
|
||||
}
|
||||
|
||||
object SkuType {
|
||||
const val INAPP = ""
|
||||
}
|
||||
|
||||
object Builder {
|
||||
fun enablePendingPurchases() = this
|
||||
fun setListener(listener: PurchasesUpdatedListener) = this
|
||||
fun build() = BillingClient
|
||||
}
|
||||
}
|
||||
|
||||
object BillingResult {
|
||||
const val responseCode = BillingClient.BillingResponseCode.ERR
|
||||
const val debugMessage = "only mock linked"
|
||||
}
|
||||
|
||||
object SkuDetails {
|
||||
const val sku = ""
|
||||
const val price = ""
|
||||
const val description = ""
|
||||
}
|
||||
|
||||
object SkuDetailsParams {
|
||||
fun newBuilder() = Builder
|
||||
|
||||
object Builder {
|
||||
fun setSkusList(list: List<String>) = this
|
||||
fun setType(type: String) = this
|
||||
fun build() = SkuDetailsParams
|
||||
}
|
||||
}
|
||||
|
||||
object Purchase {
|
||||
const val purchaseState = PurchaseState.PURCHASED
|
||||
const val isAcknowledged = true
|
||||
const val sku = ""
|
||||
const val purchaseToken = ""
|
||||
const val originalJson = ""
|
||||
const val signature = ""
|
||||
|
||||
object PurchaseState {
|
||||
const val PURCHASED = 0
|
||||
}
|
||||
}
|
||||
|
||||
object AcknowledgePurchaseParams {
|
||||
fun newBuilder() = Builder
|
||||
|
||||
object Builder {
|
||||
fun setPurchaseToken(token: String) = this
|
||||
fun build() = AcknowledgePurchaseParams
|
||||
}
|
||||
}
|
||||
|
||||
object ConsumeParams {
|
||||
fun newBuilder() = Builder
|
||||
|
||||
object Builder {
|
||||
fun setPurchaseToken(token: String) = this
|
||||
fun build() = ConsumeParams
|
||||
}
|
||||
}
|
||||
|
||||
object BillingFlowParams {
|
||||
fun newBuilder() = Builder
|
||||
|
||||
object Builder {
|
||||
fun setSkuDetails(details: SkuDetails) = this
|
||||
fun build() = BillingFlowParams
|
||||
}
|
||||
}
|
||||
|
||||
object QueryPurchasesResult {
|
||||
val billingResult = BillingResult
|
||||
val purchasesList: List<Purchase>? = emptyList()
|
||||
}
|
||||
|
||||
data class QuerySkuDetailsResult(val billingResult: BillingResult, val details: List<SkuDetails>?) {
|
||||
companion object {
|
||||
val instance = QuerySkuDetailsResult(BillingResult, emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
interface BillingClientStateListener {
|
||||
fun onBillingSetupFinished(billingResult: BillingResult)
|
||||
fun onBillingServiceDisconnected()
|
||||
}
|
||||
|
||||
interface PurchasesUpdatedListener {
|
||||
fun onPurchasesUpdated(p0: BillingResult, p1: MutableList<Purchase>?)
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.solovyev.android.checkout
|
||||
|
||||
import android.app.Application
|
||||
|
||||
class Billing(application: Application, config: DefaultConfiguration) {
|
||||
abstract class DefaultConfiguration {
|
||||
abstract fun getPublicKey(): String
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.solovyev.android.checkout
|
||||
|
||||
import io.timelimit.android.async.Threads
|
||||
|
||||
object BillingRequests {
|
||||
fun consume(token: String, listener: RequestListener<Any>) {
|
||||
Threads.mainThreadHandler.post {
|
||||
listener.onSuccess(0)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSkus(product: String, skus: List<String>, listener: RequestListener<Skus>) {
|
||||
Threads.mainThreadHandler.post {
|
||||
listener.onSuccess(Skus)
|
||||
}
|
||||
}
|
||||
|
||||
fun purchase(productType: String, sku: String, something: Unit?, purchaseFlow: PurchaseFlow) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.solovyev.android.checkout
|
||||
|
||||
import android.app.Activity
|
||||
import io.timelimit.android.async.Threads
|
||||
|
||||
open class Checkout {
|
||||
companion object {
|
||||
private val instance = Checkout()
|
||||
|
||||
fun forActivity(activity: Activity, billing: Billing) = ActivityCheckout
|
||||
fun forApplication(billing: Billing) = instance
|
||||
}
|
||||
|
||||
fun start(listener: EmptyListener) {
|
||||
Threads.mainThreadHandler.post {
|
||||
listener.onReady(
|
||||
billingSupported = false,
|
||||
product = "",
|
||||
requests = BillingRequests
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun whenReady(listener: EmptyListener) = start(listener)
|
||||
|
||||
fun stop() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
fun makeInventory() = Inventory
|
||||
|
||||
abstract class EmptyListener {
|
||||
abstract fun onReady(requests: BillingRequests, product: String, billingSupported: Boolean)
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.solovyev.android.checkout
|
||||
|
||||
import io.timelimit.android.async.Threads
|
||||
|
||||
object Inventory {
|
||||
object Products {
|
||||
object Type {
|
||||
val purchases = emptyList<Purchase>()
|
||||
}
|
||||
|
||||
operator fun get(type: String) = Type
|
||||
}
|
||||
|
||||
object Request {
|
||||
fun create() = this
|
||||
fun loadAllPurchases() = this
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onLoaded(products: Inventory.Products)
|
||||
}
|
||||
|
||||
fun load(request: Inventory.Request, callback: Callback) {
|
||||
Threads.mainThreadHandler.post {
|
||||
callback.onLoaded(Products)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.solovyev.android.checkout
|
||||
|
||||
object ProductTypes {
|
||||
const val IN_APP = ""
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.solovyev.android.checkout
|
||||
|
||||
data class Purchase (val data: String, val signature: String, val token: String, val sku: String)
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.solovyev.android.checkout
|
||||
|
||||
object PurchaseFlow
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.solovyev.android.checkout
|
||||
|
||||
interface RequestListener<T> {
|
||||
fun onSuccess(result: T)
|
||||
fun onError(response: Int, e: Exception)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.solovyev.android.checkout
|
||||
|
||||
data class Sku(val description: String, val price: String)
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.solovyev.android.checkout
|
||||
|
||||
object Skus {
|
||||
fun getSku(skuId: String): Sku? = null
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue