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:okhttp-tls:4.9.0'
|
||||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.8.1'
|
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') {
|
implementation('io.socket:socket.io-client:1.0.0') {
|
||||||
exclude group: 'org.json', module: 'json'
|
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
|
* 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
|
* 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 android.app.Application
|
||||||
import com.jakewharton.threetenabp.AndroidThreeTen
|
import com.jakewharton.threetenabp.AndroidThreeTen
|
||||||
import org.solovyev.android.checkout.Billing
|
|
||||||
|
|
||||||
class Application : Application() {
|
class Application : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
@ -25,8 +24,4 @@ class Application : Application() {
|
||||||
|
|
||||||
AndroidThreeTen.init(this)
|
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
|
* 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
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* 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
|
fun BillingResult.assertSuccess() {
|
||||||
|
if (this.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||||
object ActivityCheckout: Checkout() {
|
throw BillingClientException("error during processing billing request: ${this.debugMessage}")
|
||||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
// do nothing
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun start() {
|
open class BillingClientException(message: String): RuntimeException(message)
|
||||||
// do nothing
|
class BillingNotSupportedException(): BillingClientException("billing not supported")
|
||||||
}
|
|
||||||
|
|
||||||
fun createPurchaseFlow(listener: RequestListener<Purchase>) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
val purchaseFlow = PurchaseFlow
|
|
||||||
}
|
|
|
@ -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.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -30,6 +31,7 @@ import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import io.timelimit.android.Application
|
import io.timelimit.android.Application
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.data.IdGenerator
|
import io.timelimit.android.data.IdGenerator
|
||||||
import io.timelimit.android.extensions.showSafe
|
import io.timelimit.android.extensions.showSafe
|
||||||
import io.timelimit.android.livedata.ignoreUnchanged
|
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.SetupTermsFragment
|
||||||
import io.timelimit.android.ui.setup.parent.SetupParentModeFragment
|
import io.timelimit.android.ui.setup.parent.SetupParentModeFragment
|
||||||
import io.timelimit.android.ui.util.SyncStatusModel
|
import io.timelimit.android.ui.util.SyncStatusModel
|
||||||
import org.solovyev.android.checkout.ActivityCheckout
|
|
||||||
import org.solovyev.android.checkout.Checkout
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -62,15 +62,10 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
|
|
||||||
private val currentNavigatorFragment = MutableLiveData<Fragment?>()
|
private val currentNavigatorFragment = MutableLiveData<Fragment?>()
|
||||||
private val application: Application by lazy { getApplication() as Application }
|
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 {
|
private val syncModel: SyncStatusModel by lazy {
|
||||||
ViewModelProviders.of(this).get(SyncStatusModel::class.java)
|
ViewModelProviders.of(this).get(SyncStatusModel::class.java)
|
||||||
}
|
}
|
||||||
val purchaseModel: ActivityPurchaseModel by lazy {
|
val purchaseModel: ActivityPurchaseModel by viewModels()
|
||||||
ViewModelProviders.of(this).get(ActivityPurchaseModel::class.java).apply {
|
|
||||||
setActivityCheckout(checkout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override var ignoreStop: Boolean = false
|
override var ignoreStop: Boolean = false
|
||||||
override val showPasswordRecovery: Boolean = true
|
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 {
|
override fun getActivityViewModel(): ActivityViewModel {
|
||||||
return ViewModelProviders.of(this).get(ActivityViewModel::class.java)
|
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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -15,18 +15,17 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.payment
|
package io.timelimit.android.ui.payment
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.android.billingclient.api.*
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.coroutines.runAsync
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.extensions.consumeAsync
|
import io.timelimit.android.extensions.*
|
||||||
import io.timelimit.android.extensions.startAsync
|
|
||||||
import io.timelimit.android.extensions.waitUntilReady
|
|
||||||
import io.timelimit.android.livedata.castDown
|
|
||||||
import io.timelimit.android.livedata.map
|
import io.timelimit.android.livedata.map
|
||||||
import io.timelimit.android.livedata.mergeLiveData
|
import io.timelimit.android.livedata.mergeLiveData
|
||||||
import io.timelimit.android.livedata.setTemporarily
|
import io.timelimit.android.livedata.setTemporarily
|
||||||
|
@ -34,25 +33,26 @@ import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.sync.network.CanDoPurchaseStatus
|
import io.timelimit.android.sync.network.CanDoPurchaseStatus
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.solovyev.android.checkout.*
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
class ActivityPurchaseModel(application: Application): AndroidViewModel(application) {
|
class ActivityPurchaseModel(application: Application): AndroidViewModel(application) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val LOG_TAG = "ActivityPurchaseModel"
|
private const val LOG_TAG = "ActivityPurchaseModel"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val billing = (application as io.timelimit.android.Application).billing
|
|
||||||
private val logic = DefaultAppLogic.with(application)
|
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 isWorkingInternal = MutableLiveData<Boolean>().apply { value = false }
|
||||||
private val hadErrorInternal = MutableLiveData<Boolean>().apply { value = false }
|
private val hadErrorInternal = MutableLiveData<Boolean>().apply { value = false }
|
||||||
private val processPurchaseSuccessInternal = MutableLiveData<Boolean>().apply { value = false }
|
private val processPurchaseSuccessInternal = MutableLiveData<Boolean>().apply { value = false }
|
||||||
|
|
||||||
val isWorking = isWorkingInternal.castDown()
|
val status = mergeLiveData(isWorkingInternal, hadErrorInternal, processPurchaseSuccessInternal).map {
|
||||||
val hadError = hadErrorInternal.castDown()
|
|
||||||
val processPurchaseSuccess = processPurchaseSuccessInternal.castDown()
|
|
||||||
val status = mergeLiveData(isWorking, hadError, processPurchaseSuccess).map {
|
|
||||||
(working, error, success) ->
|
(working, error, success) ->
|
||||||
|
|
||||||
if (success != null && success) {
|
if (success != null && success) {
|
||||||
|
@ -70,157 +70,234 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
|
||||||
processPurchaseSuccessInternal.value = false
|
processPurchaseSuccessInternal.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private var activityCheckout: ActivityCheckout? = null
|
private suspend fun <R> initAndUseClient(block: suspend (client: BillingClient) -> R): R {
|
||||||
|
clientMutex.withLock {
|
||||||
fun setActivityCheckout(checkout: ActivityCheckout) {
|
if (_billingClient == null) {
|
||||||
checkout.start()
|
_billingClient = BillingClient.newBuilder(getApplication())
|
||||||
|
.enablePendingPurchases()
|
||||||
checkout.createPurchaseFlow(object: RequestListener<Purchase> {
|
.setListener(purchaseUpdatedListener)
|
||||||
override fun onError(response: Int, e: Exception) {
|
.build()
|
||||||
// ignored
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSuccess(result: Purchase) {
|
val initBillingClient = _billingClient!!
|
||||||
if (PurchaseIds.BUY_SKUS.contains(result.sku)) {
|
|
||||||
runAsync {
|
suspendCoroutine<Unit?> { continuation ->
|
||||||
lock.withLock {
|
initBillingClient.startConnection(object : BillingClientStateListener {
|
||||||
isWorkingInternal.setTemporarily(true).use { _ ->
|
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||||
handlePurchase(result)
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
runAsync {
|
||||||
|
clientMutex.withLock {
|
||||||
|
if (_billingClient === initBillingClient) { _billingClient = 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 {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
activityCheckout = checkout
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun forgetActivityCheckout() {
|
fun forgetActivityCheckout() {
|
||||||
activityCheckout?.stop()
|
|
||||||
activityCheckout = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun queryAndProcessPurchasesAsync() {
|
|
||||||
runAsync {
|
runAsync {
|
||||||
lock.withLock {
|
clientMutex.withLock {
|
||||||
isWorkingInternal.setTemporarily(true).use { _ ->
|
_billingClient?.endConnection()
|
||||||
val checkout = activityCheckout
|
_billingClient = null
|
||||||
|
|
||||||
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) {
|
private suspend fun handlePurchase(purchase: Purchase, billingClient: BillingClient) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED || purchase.isAcknowledged) {
|
||||||
Log.d(LOG_TAG, "handlePurchase()")
|
// we are not interested in it
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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()
|
val server = logic.serverLogic.getServerConfigCoroutine()
|
||||||
|
|
||||||
if (server.hasAuthToken) {
|
if (server.hasAuthToken) {
|
||||||
server.api.finishPurchaseByGooglePlay(
|
server.api.finishPurchaseByGooglePlay(
|
||||||
receipt = purchase.data,
|
receipt = purchase.originalJson,
|
||||||
signature = purchase.signature,
|
signature = purchase.signature,
|
||||||
deviceAuthToken = server.deviceAuthToken
|
deviceAuthToken = server.deviceAuthToken
|
||||||
)
|
)
|
||||||
|
|
||||||
|
billingClient.consumePurchase(
|
||||||
|
ConsumeParams.newBuilder()
|
||||||
|
.setPurchaseToken(purchase.purchaseToken)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
processPurchaseSuccessInternal.value = true
|
||||||
|
} else {
|
||||||
|
Log.w(LOG_TAG, "purchase for the premium version but no server available")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
processPurchaseSuccessInternal.value = true
|
|
||||||
consumePurchaseAsync(purchase)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
hadErrorInternal.value = true
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
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) {
|
fun startPurchase(sku: String, checkAtBackend: Boolean, activity: Activity) {
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "consumePurchaseAsync()")
|
|
||||||
}
|
|
||||||
|
|
||||||
runAsync {
|
runAsync {
|
||||||
lock.withLock {
|
try {
|
||||||
try {
|
val skuDetails = querySkus(listOf(sku)).single()
|
||||||
Checkout.forApplication(billing).startAsync().use {
|
|
||||||
it.requests.consumeAsync(purchase.token)
|
if (skuDetails.sku != sku) throw IllegalStateException()
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
startPurchase(skuDetails, checkAtBackend, activity)
|
||||||
if (BuildConfig.DEBUG) {
|
} catch (ex: Exception) {
|
||||||
Log.w(LOG_TAG, "consumePurchaseAsync() failed", ex)
|
if (BuildConfig.DEBUG) {
|
||||||
}
|
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 {
|
runAsync {
|
||||||
lock.withLock {
|
initAndUseClient { client ->
|
||||||
isWorkingInternal.setTemporarily(true).use {
|
try {
|
||||||
_ ->
|
if (checkAtBackend) {
|
||||||
|
val server = logic.serverLogic.getServerConfigCoroutine()
|
||||||
|
|
||||||
try {
|
if (!server.hasAuthToken) {
|
||||||
if (checkAtBackend) {
|
|
||||||
val server = logic.serverLogic.getServerConfigCoroutine()
|
|
||||||
|
|
||||||
if (!server.hasAuthToken) {
|
|
||||||
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
|
||||||
|
|
||||||
return@runAsync
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(server.api.canDoPurchase(server.deviceAuthToken) is CanDoPurchaseStatus.Yes)) {
|
|
||||||
throw IOException("can not do purchase right now")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start the purchase
|
|
||||||
val activityCheckout = activityCheckout
|
|
||||||
|
|
||||||
if (activityCheckout == null) {
|
|
||||||
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
return@runAsync
|
return@initAndUseClient
|
||||||
}
|
}
|
||||||
|
|
||||||
activityCheckout.waitUntilReady().requests.purchase(
|
if (!(server.api.canDoPurchase(server.deviceAuthToken) is CanDoPurchaseStatus.Yes)) {
|
||||||
ProductTypes.IN_APP,
|
throw IOException("can not do purchase right now")
|
||||||
sku,
|
|
||||||
null,
|
|
||||||
activityCheckout.purchaseFlow
|
|
||||||
)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "could not start purchase", ex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.launchBillingFlow(
|
||||||
|
activity,
|
||||||
|
BillingFlowParams.newBuilder()
|
||||||
|
.setSkuDetails(skuDetails)
|
||||||
|
.build()
|
||||||
|
).assertSuccess()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "could not start purchase", ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProviders
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.databinding.FragmentPurchaseBinding
|
import io.timelimit.android.databinding.FragmentPurchaseBinding
|
||||||
import io.timelimit.android.livedata.liveDataFromNullableValue
|
import io.timelimit.android.livedata.liveDataFromNullableValue
|
||||||
|
@ -32,9 +32,7 @@ import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
|
||||||
class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
private val activityModel: ActivityPurchaseModel by lazy { (activity as MainActivity).purchaseModel }
|
private val activityModel: ActivityPurchaseModel by lazy { (activity as MainActivity).purchaseModel }
|
||||||
private val model: PurchaseModel by lazy {
|
private val model: PurchaseModel by viewModels()
|
||||||
ViewModelProviders.of(this).get(PurchaseModel::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PAGE_BUY = 0
|
private const val PAGE_BUY = 0
|
||||||
|
@ -49,37 +47,39 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
activityModel.resetProcessPurchaseSuccess()
|
activityModel.resetProcessPurchaseSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model.retry(activityModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val binding = FragmentPurchaseBinding.inflate(inflater, container, false)
|
val binding = FragmentPurchaseBinding.inflate(inflater, container, false)
|
||||||
var processingPurchaseError = false
|
var processingPurchaseError = false
|
||||||
|
|
||||||
mergeLiveData(activityModel.status, model.status).observe(this, Observer {
|
mergeLiveData(activityModel.status, model.status).observe(viewLifecycleOwner, {
|
||||||
status ->
|
status ->
|
||||||
|
|
||||||
val (activityStatus, fragmentStatus) = status!!
|
val (activityStatus, fragmentStatus) = status!!
|
||||||
|
|
||||||
if (activityStatus != ActivityPurchaseModelStatus.Idle) {
|
if (fragmentStatus != null) {
|
||||||
when (activityStatus) {
|
|
||||||
null -> binding.flipper.displayedChild = PAGE_WAIT
|
|
||||||
ActivityPurchaseModelStatus.Working -> binding.flipper.displayedChild = PAGE_WAIT
|
|
||||||
ActivityPurchaseModelStatus.Idle -> throw IllegalStateException()
|
|
||||||
ActivityPurchaseModelStatus.Error -> {
|
|
||||||
binding.flipper.displayedChild = PAGE_ERROR
|
|
||||||
|
|
||||||
binding.errorReason = getString(R.string.error_general)
|
|
||||||
binding.showRetryButton = true
|
|
||||||
processingPurchaseError = true
|
|
||||||
}
|
|
||||||
ActivityPurchaseModelStatus.Done -> binding.flipper.displayedChild = PAGE_DONE
|
|
||||||
}.let { }
|
|
||||||
} else if (fragmentStatus != null) {
|
|
||||||
when (fragmentStatus) {
|
when (fragmentStatus) {
|
||||||
PurchaseFragmentPreparing -> binding.flipper.displayedChild = PAGE_WAIT
|
PurchaseFragmentPreparing -> binding.flipper.displayedChild = PAGE_WAIT
|
||||||
is PurchaseFragmentReady -> {
|
is PurchaseFragmentReady -> {
|
||||||
binding.flipper.displayedChild = PAGE_BUY
|
when (activityStatus) {
|
||||||
binding.priceData = fragmentStatus
|
null -> binding.flipper.displayedChild = PAGE_WAIT
|
||||||
|
ActivityPurchaseModelStatus.Working -> binding.flipper.displayedChild = PAGE_WAIT
|
||||||
|
ActivityPurchaseModelStatus.Idle -> {
|
||||||
|
binding.flipper.displayedChild = PAGE_BUY
|
||||||
|
binding.priceData = fragmentStatus
|
||||||
|
}
|
||||||
|
ActivityPurchaseModelStatus.Error -> {
|
||||||
|
binding.flipper.displayedChild = PAGE_ERROR
|
||||||
|
|
||||||
|
binding.errorReason = getString(R.string.error_general)
|
||||||
|
binding.showRetryButton = true
|
||||||
|
processingPurchaseError = true
|
||||||
|
}
|
||||||
|
ActivityPurchaseModelStatus.Done -> binding.flipper.displayedChild = PAGE_DONE
|
||||||
|
}.let { }
|
||||||
}
|
}
|
||||||
is PurchaseFragmentError -> {
|
is PurchaseFragmentError -> {
|
||||||
binding.flipper.displayedChild = PAGE_ERROR
|
binding.flipper.displayedChild = PAGE_ERROR
|
||||||
|
@ -110,16 +110,16 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
if (processingPurchaseError) {
|
if (processingPurchaseError) {
|
||||||
activityModel.queryAndProcessPurchasesAsync()
|
activityModel.queryAndProcessPurchasesAsync()
|
||||||
} else {
|
} else {
|
||||||
model.retry()
|
model.retry(activityModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun buyForOneMonth() {
|
override fun buyForOneMonth() {
|
||||||
activityModel.startPurchase(PurchaseIds.SKU_MONTH, checkAtBackend = true)
|
activityModel.startPurchase(PurchaseIds.SKU_MONTH, checkAtBackend = true, activity = requireActivity())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun buyForOneYear() {
|
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
|
* 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
|
* 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"
|
const val SKU_MONTH = "premium_month_2018"
|
||||||
|
|
||||||
val BUY_SKUS = listOf(SKU_YEAR, SKU_MONTH)
|
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
|
* 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
|
* 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 androidx.lifecycle.MutableLiveData
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.coroutines.runAsync
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.extensions.getSkusAsync
|
import io.timelimit.android.extensions.BillingNotSupportedException
|
||||||
import io.timelimit.android.extensions.startAsync
|
|
||||||
import io.timelimit.android.livedata.castDown
|
import io.timelimit.android.livedata.castDown
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.sync.network.CanDoPurchaseStatus
|
import io.timelimit.android.sync.network.CanDoPurchaseStatus
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.solovyev.android.checkout.Checkout
|
|
||||||
import org.solovyev.android.checkout.ProductTypes
|
|
||||||
|
|
||||||
class PurchaseModel(application: Application): AndroidViewModel(application) {
|
class PurchaseModel(application: Application): AndroidViewModel(application) {
|
||||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(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 statusInternal = MutableLiveData<PurchaseFragmentStatus>()
|
||||||
private val lock = Mutex()
|
private val lock = Mutex()
|
||||||
|
|
||||||
val status = statusInternal.castDown()
|
val status = statusInternal.castDown()
|
||||||
|
|
||||||
init {
|
fun retry(activityPurchaseModel: ActivityPurchaseModel) {
|
||||||
prepare()
|
if (this.status.value is PurchaseFragmentRecoverableError || this.status.value == null) {
|
||||||
}
|
prepare(activityPurchaseModel)
|
||||||
|
|
||||||
fun retry() {
|
|
||||||
if (this.status.value is PurchaseFragmentRecoverableError) {
|
|
||||||
prepare()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun prepare() {
|
private fun prepare(activityPurchaseModel: ActivityPurchaseModel) {
|
||||||
runAsync {
|
runAsync {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
try {
|
try {
|
||||||
|
@ -70,23 +62,12 @@ class PurchaseModel(application: Application): AndroidViewModel(application) {
|
||||||
if (canDoPurchase.publicKey?.contentEquals(Base64.decode(BuildConfig.googlePlayKey, 0)) == false) {
|
if (canDoPurchase.publicKey?.contentEquals(Base64.decode(BuildConfig.googlePlayKey, 0)) == false) {
|
||||||
statusInternal.value = PurchaseFragmentServerHasDifferentPublicKey
|
statusInternal.value = PurchaseFragmentServerHasDifferentPublicKey
|
||||||
} else {
|
} else {
|
||||||
val checkout = Checkout.forApplication(application.billing)
|
val skus = activityPurchaseModel.querySkus(PurchaseIds.BUY_SKUS)
|
||||||
|
|
||||||
checkout.startAsync().use {
|
statusInternal.value = PurchaseFragmentReady(
|
||||||
if (!it.billingSupported) {
|
monthPrice = skus.find { it.sku == PurchaseIds.SKU_MONTH }?.price.toString(),
|
||||||
statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByDevice
|
yearPrice = skus.find { it.sku == PurchaseIds.SKU_YEAR }?.price.toString()
|
||||||
} else {
|
)
|
||||||
val skus = it.requests.getSkusAsync(
|
|
||||||
ProductTypes.IN_APP,
|
|
||||||
PurchaseIds.BUY_SKUS
|
|
||||||
)
|
|
||||||
|
|
||||||
statusInternal.value = PurchaseFragmentReady(
|
|
||||||
monthPrice = skus.getSku(PurchaseIds.SKU_MONTH)?.price.toString(),
|
|
||||||
yearPrice = skus.getSku(PurchaseIds.SKU_YEAR)?.price.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (canDoPurchase == CanDoPurchaseStatus.NotDueToOldPurchase) {
|
} else if (canDoPurchase == CanDoPurchaseStatus.NotDueToOldPurchase) {
|
||||||
statusInternal.value = PurchaseFragmentExistingPaymentError
|
statusInternal.value = PurchaseFragmentExistingPaymentError
|
||||||
|
@ -94,6 +75,8 @@ class PurchaseModel(application: Application): AndroidViewModel(application) {
|
||||||
statusInternal.value = PurchaseFragmentServerRejectedError
|
statusInternal.value = PurchaseFragmentServerRejectedError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (ex: BillingNotSupportedException) {
|
||||||
|
statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByDevice
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
statusInternal.value = PurchaseFragmentNetworkError
|
statusInternal.value = PurchaseFragmentNetworkError
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProviders
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.databinding.StayAwesomeFragmentBinding
|
import io.timelimit.android.databinding.StayAwesomeFragmentBinding
|
||||||
import io.timelimit.android.databinding.StayAwesomeFragmentItemBinding
|
import io.timelimit.android.databinding.StayAwesomeFragmentItemBinding
|
||||||
|
@ -31,13 +31,11 @@ import io.timelimit.android.ui.MainActivity
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
|
||||||
class StayAwesomeFragment : Fragment(), FragmentWithCustomTitle {
|
class StayAwesomeFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
val model: StayAwesomeModel by lazy {
|
private val model: StayAwesomeModel by viewModels()
|
||||||
ViewModelProviders.of(this).get(StayAwesomeModel::class.java)
|
private val activityModel get() = (activity as MainActivity).purchaseModel
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val binding = StayAwesomeFragmentBinding.inflate(inflater, container, false)
|
val binding = StayAwesomeFragmentBinding.inflate(inflater, container, false)
|
||||||
val activityModel = (activity as MainActivity).purchaseModel
|
|
||||||
|
|
||||||
model.status.observe(viewLifecycleOwner, Observer { status ->
|
model.status.observe(viewLifecycleOwner, Observer { status ->
|
||||||
when (status!!) {
|
when (status!!) {
|
||||||
|
@ -62,7 +60,7 @@ class StayAwesomeFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
|
||||||
if (!item.bought) {
|
if (!item.bought) {
|
||||||
view.card.setOnClickListener {
|
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() {
|
override fun onResume() {
|
||||||
super.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)}")
|
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
|
* 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
|
* 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 androidx.lifecycle.MutableLiveData
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.coroutines.runAsync
|
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 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) {
|
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>()
|
private val statusInternal = MutableLiveData<StayAwesomeStatus>()
|
||||||
|
|
||||||
val status = statusInternal.castDown()
|
val status = statusInternal.castDown()
|
||||||
|
|
||||||
fun load() {
|
fun load(activityPurchaseModel: ActivityPurchaseModel) {
|
||||||
runAsync {
|
runAsync {
|
||||||
statusInternal.value = LoadingStayAwesomeStatus
|
statusInternal.value = LoadingStayAwesomeStatus
|
||||||
|
|
||||||
|
@ -44,41 +36,27 @@ class StayAwesomeModel(application: Application): AndroidViewModel(application)
|
||||||
return@runAsync
|
return@runAsync
|
||||||
}
|
}
|
||||||
|
|
||||||
val checkout = Checkout.forApplication(application.billing)
|
try {
|
||||||
|
val skus = activityPurchaseModel.querySkus(PurchaseIds.SAL_SKUS)
|
||||||
|
val purchases = activityPurchaseModel.queryPurchases()
|
||||||
|
|
||||||
checkout.startAsync().use {
|
statusInternal.value = ReadyStayAwesomeStatus(
|
||||||
if (!it.billingSupported) {
|
PurchaseIds.SAL_SKUS.map { skuId ->
|
||||||
statusInternal.value = NotSupportedByDeviceStayAwesomeStatus
|
val sku = skus.find { it.sku == skuId }
|
||||||
} else {
|
|
||||||
val skus = it.requests.getSkusAsync(
|
|
||||||
ProductTypes.IN_APP,
|
|
||||||
PurchaseIds.SLA_SKUS
|
|
||||||
)
|
|
||||||
|
|
||||||
val inventory = checkout.makeInventory()
|
StayAwesomeItem(
|
||||||
val products = inventory.loadAsync(Inventory.Request.create().loadAllPurchases())
|
id = skuId,
|
||||||
val purchasedSkus = products[ProductTypes.IN_APP].purchases.map { it.sku }.toSet()
|
title = sku?.description ?: skuId,
|
||||||
|
price = sku?.price ?: "???",
|
||||||
statusInternal.value = ReadyStayAwesomeStatus(
|
bought = purchases.find { it.sku == skuId } != null
|
||||||
PurchaseIds.SLA_SKUS.map { skuId ->
|
)
|
||||||
val sku = skus.getSku(skuId)
|
}
|
||||||
|
)
|
||||||
StayAwesomeItem(
|
} catch (ex: Exception) {
|
||||||
id = skuId,
|
statusInternal.value = NotSupportedByDeviceStayAwesomeStatus
|
||||||
title = sku?.description ?: skuId,
|
|
||||||
price = sku?.price ?: "???",
|
|
||||||
bought = purchasedSkus.contains(skuId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class StayAwesomeStatus
|
sealed class StayAwesomeStatus
|
||||||
|
|
|
@ -92,8 +92,8 @@
|
||||||
(<a href="https://github.com/KeepSafe/TapTargetView/blob/master/LICENSE">Apache License, Version 2.0</a>
|
(<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>
|
\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>)
|
(<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>
|
\nGoogle Play Billing Library (only in builds in the Play Store)
|
||||||
(<a href="https://github.com/serso/android-checkout/blob/master/LICENSE.txt">Apache License, Version 2.0</a>)
|
(<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>
|
\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>)
|
(<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>
|
\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