Migrate to the (required) new billing library

This commit is contained in:
Jonas Lochmann 2021-03-15 01:00:00 +01:00
parent f535cb4767
commit b71009d1cb
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
23 changed files with 410 additions and 663 deletions

View file

@ -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'

View file

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

View file

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

View file

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

View file

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

View file

@ -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,120 +70,207 @@ 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 }
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 { runAsync {
lock.withLock { clientMutex.withLock {
isWorkingInternal.setTemporarily(true).use { _ -> if (_billingClient === initBillingClient) { _billingClient = null }
handlePurchase(result)
}
}
} }
} }
} }
}) })
activityCheckout = checkout
} }
fun forgetActivityCheckout() { return block(initBillingClient)
activityCheckout?.stop() }
activityCheckout = null }
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() { fun queryAndProcessPurchasesAsync() {
runAsync { queryAndProcessPurchases() }
}
private val purchaseUpdatedListener: PurchasesUpdatedListener = object: PurchasesUpdatedListener {
override fun onPurchasesUpdated(p0: BillingResult, p1: MutableList<Purchase>?) {
runAsync { runAsync {
lock.withLock { processMutex.withLock {
isWorkingInternal.setTemporarily(true).use { _ -> isWorkingInternal.setTemporarily(true).use {
val checkout = activityCheckout initAndUseClient { client ->
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()")
}
try { 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() 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 processPurchaseSuccessInternal.value = true
consumePurchaseAsync(purchase) } else {
} catch (ex: Exception) { Log.w(LOG_TAG, "purchase for the premium version but no server available")
hadErrorInternal.value = true }
} else {
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 {
Checkout.forApplication(billing).startAsync().use { val skuDetails = querySkus(listOf(sku)).single()
it.requests.consumeAsync(purchase.token)
} if (skuDetails.sku != sku) throw IllegalStateException()
startPurchase(skuDetails, checkAtBackend, activity)
} catch (ex: Exception) { } catch (ex: Exception) {
if (BuildConfig.DEBUG) { 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 { runAsync {
lock.withLock { initAndUseClient { client ->
isWorkingInternal.setTemporarily(true).use {
_ ->
try { try {
if (checkAtBackend) { if (checkAtBackend) {
val server = logic.serverLogic.getServerConfigCoroutine() val server = logic.serverLogic.getServerConfigCoroutine()
@ -191,7 +278,7 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
if (!server.hasAuthToken) { if (!server.hasAuthToken) {
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
} }
if (!(server.api.canDoPurchase(server.deviceAuthToken) is CanDoPurchaseStatus.Yes)) { if (!(server.api.canDoPurchase(server.deviceAuthToken) is CanDoPurchaseStatus.Yes)) {
@ -199,21 +286,12 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
} }
} }
// start the purchase client.launchBillingFlow(
val activityCheckout = activityCheckout activity,
BillingFlowParams.newBuilder()
if (activityCheckout == null) { .setSkuDetails(skuDetails)
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show() .build()
).assertSuccess()
return@runAsync
}
activityCheckout.waitUntilReady().requests.purchase(
ProductTypes.IN_APP,
sku,
null,
activityCheckout.purchaseFlow
)
} catch (ex: Exception) { } catch (ex: Exception) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "could not start purchase", ex) Log.d(LOG_TAG, "could not start purchase", ex)
@ -225,7 +303,6 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
} }
} }
} }
}
enum class ActivityPurchaseModelStatus { enum class ActivityPurchaseModelStatus {
Idle, Working, Error, Done Idle, Working, Error, Done

View file

@ -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,22 +47,30 @@ 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 (fragmentStatus) {
PurchaseFragmentPreparing -> binding.flipper.displayedChild = PAGE_WAIT
is PurchaseFragmentReady -> {
when (activityStatus) { when (activityStatus) {
null -> binding.flipper.displayedChild = PAGE_WAIT null -> binding.flipper.displayedChild = PAGE_WAIT
ActivityPurchaseModelStatus.Working -> 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 -> { ActivityPurchaseModelStatus.Error -> {
binding.flipper.displayedChild = PAGE_ERROR binding.flipper.displayedChild = PAGE_ERROR
@ -74,12 +80,6 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
} }
ActivityPurchaseModelStatus.Done -> binding.flipper.displayedChild = PAGE_DONE ActivityPurchaseModelStatus.Done -> binding.flipper.displayedChild = PAGE_DONE
}.let { } }.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 -> { 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())
} }
} }

View file

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

View file

@ -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,30 +62,21 @@ 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 {
if (!it.billingSupported) {
statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByDevice
} else {
val skus = it.requests.getSkusAsync(
ProductTypes.IN_APP,
PurchaseIds.BUY_SKUS
)
statusInternal.value = PurchaseFragmentReady( statusInternal.value = PurchaseFragmentReady(
monthPrice = skus.getSku(PurchaseIds.SKU_MONTH)?.price.toString(), monthPrice = skus.find { it.sku == PurchaseIds.SKU_MONTH }?.price.toString(),
yearPrice = skus.getSku(PurchaseIds.SKU_YEAR)?.price.toString() yearPrice = skus.find { it.sku == PurchaseIds.SKU_YEAR }?.price.toString()
) )
} }
}
}
} else if (canDoPurchase == CanDoPurchaseStatus.NotDueToOldPurchase) { } else if (canDoPurchase == CanDoPurchaseStatus.NotDueToOldPurchase) {
statusInternal.value = PurchaseFragmentExistingPaymentError statusInternal.value = PurchaseFragmentExistingPaymentError
} else { } else {
statusInternal.value = PurchaseFragmentServerRejectedError statusInternal.value = PurchaseFragmentServerRejectedError
} }
} }
} catch (ex: BillingNotSupportedException) {
statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByDevice
} catch (ex: Exception) { } catch (ex: Exception) {
statusInternal.value = PurchaseFragmentNetworkError statusInternal.value = PurchaseFragmentNetworkError
} }

View file

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

View file

@ -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,43 +36,29 @@ class StayAwesomeModel(application: Application): AndroidViewModel(application)
return@runAsync return@runAsync
} }
val checkout = Checkout.forApplication(application.billing) try {
val skus = activityPurchaseModel.querySkus(PurchaseIds.SAL_SKUS)
checkout.startAsync().use { val purchases = activityPurchaseModel.queryPurchases()
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()
statusInternal.value = ReadyStayAwesomeStatus( statusInternal.value = ReadyStayAwesomeStatus(
PurchaseIds.SLA_SKUS.map { skuId -> PurchaseIds.SAL_SKUS.map { skuId ->
val sku = skus.getSku(skuId) val sku = skus.find { it.sku == skuId }
StayAwesomeItem( StayAwesomeItem(
id = skuId, id = skuId,
title = sku?.description ?: skuId, title = sku?.description ?: skuId,
price = sku?.price ?: "???", 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 sealed class StayAwesomeStatus
object LoadingStayAwesomeStatus: StayAwesomeStatus() object LoadingStayAwesomeStatus: StayAwesomeStatus()
object NotSupportedByDeviceStayAwesomeStatus: StayAwesomeStatus() object NotSupportedByDeviceStayAwesomeStatus: StayAwesomeStatus()

View file

@ -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>

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = ""
}

View file

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

View file

@ -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

View file

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

View file

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

View file

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