From d00840c3bd9c1ae3f833c563eb71dce094ea7520 Mon Sep 17 00:00:00 2001 From: Johan von Forstner Date: Thu, 21 May 2020 14:53:30 +0200 Subject: [PATCH] implement donation view --- .../evmap/adapter/DataBindingAdapters.kt | 4 + .../vonforst/evmap/fragment/AboutFragment.kt | 3 - .../vonforst/evmap/fragment/DonateFragment.kt | 20 ++++ .../evmap/viewmodel/DonateViewModel.kt | 99 +++++++++++++++++-- .../net/vonforst/evmap/viewmodel/Utils.kt | 34 ++++++- app/src/main/res/layout/fragment_donate.xml | 57 ++++++++--- app/src/main/res/layout/item_donation.xml | 54 ++++++++++ app/src/main/res/values-de/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 9 files changed, 252 insertions(+), 25 deletions(-) create mode 100644 app/src/main/res/layout/item_donation.xml diff --git a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt index 4324c457..71d20be9 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt @@ -298,4 +298,8 @@ class FiltersAdapter : DataBindingAdapter>() { } return value } +} + +class DonationAdapter() : DataBindingAdapter() { + override fun getItemViewType(position: Int): Int = R.layout.item_donation } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/fragment/AboutFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/AboutFragment.kt index 080d48f5..0c67d276 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/AboutFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/AboutFragment.kt @@ -29,9 +29,6 @@ class AboutFragment : PreferenceFragmentCompat() { setPreferencesFromResource(R.xml.about, rootKey) findPreference("version")?.summary = BuildConfig.VERSION_NAME - - //TODO: disable donations until fully implemented - findPreference("donate")?.isVisible = false } override fun onPreferenceTreeClick(preference: Preference?): Boolean { diff --git a/app/src/main/java/net/vonforst/evmap/fragment/DonateFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/DonateFragment.kt index 89366824..3e226ffe 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/DonateFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/DonateFragment.kt @@ -9,10 +9,14 @@ import androidx.appcompat.widget.Toolbar import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import androidx.navigation.ui.setupWithNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar import net.vonforst.evmap.MapsActivity import net.vonforst.evmap.R +import net.vonforst.evmap.adapter.DonationAdapter import net.vonforst.evmap.databinding.FragmentDonateBinding import net.vonforst.evmap.viewmodel.DonateViewModel @@ -40,5 +44,21 @@ class DonateFragment : Fragment() { navController, (requireActivity() as MapsActivity).appBarConfiguration ) + + binding.productsList.apply { + adapter = DonationAdapter().apply { + onClickListener = { + vm.startPurchase(it, requireActivity()) + } + } + layoutManager = LinearLayoutManager(context) + } + + vm.purchaseSuccessful.observe(viewLifecycleOwner, Observer { + Snackbar.make(view, R.string.donation_successful, Snackbar.LENGTH_LONG).show() + }) + vm.purchaseFailed.observe(viewLifecycleOwner, Observer { + Snackbar.make(view, R.string.donation_failed, Snackbar.LENGTH_LONG).show() + }) } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/DonateViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/DonateViewModel.kt index fad2186a..fead18bb 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/DonateViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/DonateViewModel.kt @@ -1,9 +1,12 @@ package net.vonforst.evmap.viewmodel +import android.app.Activity import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import com.android.billingclient.api.* +import net.vonforst.evmap.BuildConfig +import net.vonforst.evmap.adapter.Equatable class DonateViewModel(application: Application) : AndroidViewModel(application), PurchasesUpdatedListener { @@ -12,18 +15,96 @@ class DonateViewModel(application: Application) : AndroidViewModel(application), .enablePendingPurchases() .build() - val products: MutableLiveData> by lazy { - MutableLiveData>().apply { - value = null + init { + billingClient.startConnection(object : BillingClientStateListener { + override fun onBillingServiceDisconnected() { + } - val params = SkuDetailsParams.newBuilder().setType(BillingClient.SkuType.INAPP).build() - billingClient.querySkuDetailsAsync(params) { result, details -> - value = details + override fun onBillingSetupFinished(p0: BillingResult?) { + loadProducts() + + // consume pending purchases + val purchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP) + purchases.purchasesList.forEach { + if (!it.isAcknowledged) { + consumePurchase(it.purchaseToken, false) + } + } + } + + }) + } + + private fun loadProducts() { + val params = SkuDetailsParams.newBuilder() + .setType(BillingClient.SkuType.INAPP) + .setSkusList( + listOf( + "donate_1_eur", "donate_2_eur", "donate_5_eur", "donate_10_eur" + ) + + if (BuildConfig.DEBUG) { + listOf( + "android.test.purchased", "android.test.canceled", + "android.test.item_unavailable" + ) + } else { + emptyList() + } + ) + .build() + billingClient.querySkuDetailsAsync(params) { result, details -> + if (result.responseCode == BillingClient.BillingResponseCode.OK) { + products.value = Resource.success(details.map { DonationItem(it) }) + } else { + products.value = Resource.error(result.debugMessage, null) } } } - override fun onPurchasesUpdated(result: BillingResult, purchases: List?) { - TODO("Not yet implemented") + val products: MutableLiveData>> by lazy { + MutableLiveData>>().apply { + value = Resource.loading(null) + } } -} \ No newline at end of file + + val purchaseSuccessful = SingleLiveEvent() + val purchaseFailed = SingleLiveEvent() + + override fun onPurchasesUpdated(result: BillingResult, purchases: List?) { + if (result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { + for (purchase in purchases) { + val purchaseToken = purchase.purchaseToken + consumePurchase(purchaseToken) + } + } else if (result.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) { + // Handle an error caused by a user cancelling the purchase flow. + } else { + purchaseFailed.call() + } + } + + private fun consumePurchase(purchaseToken: String, showSuccess: Boolean = true) { + val params = ConsumeParams.newBuilder() + .setPurchaseToken(purchaseToken) + .build() + billingClient.consumeAsync(params) { _, _ -> + if (showSuccess) purchaseSuccessful.call() + } + } + + fun startPurchase(it: DonationItem, activity: Activity) { + val flowParams = BillingFlowParams.newBuilder() + .setSkuDetails(it.sku) + .build() + val response = billingClient.launchBillingFlow(activity, flowParams) + if (response.responseCode != BillingClient.BillingResponseCode.OK) { + purchaseFailed.call() + } + } + + override fun onCleared() { + billingClient.endConnection() + } +} + +data class DonationItem(val sku: SkuDetails) : Equatable \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/Utils.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/Utils.kt index 0f86c036..cc8670af 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/Utils.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/Utils.kt @@ -1,7 +1,10 @@ package net.vonforst.evmap.viewmodel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import androidx.annotation.MainThread +import androidx.annotation.Nullable +import androidx.lifecycle.* +import java.util.concurrent.atomic.AtomicBoolean + inline fun viewModelFactory(crossinline f: () -> VM) = object : ViewModelProvider.Factory { @@ -32,4 +35,31 @@ data class Resource(val status: Status, val data: T?, val message: String return Resource(Status.LOADING, data, null) } } +} + +class SingleLiveEvent : MutableLiveData() { + private val mPending: AtomicBoolean = AtomicBoolean(false) + + @MainThread + override fun observe(owner: LifecycleOwner, observer: Observer) { + super.observe(owner, Observer { + if (mPending.compareAndSet(true, false)) { + observer.onChanged(it) + } + }) + } + + @MainThread + override fun setValue(@Nullable t: T?) { + mPending.set(true) + super.setValue(t) + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + fun call() { + value = null + } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_donate.xml b/app/src/main/res/layout/fragment_donate.xml index 28a374bd..6362cfe0 100644 --- a/app/src/main/res/layout/fragment_donate.xml +++ b/app/src/main/res/layout/fragment_donate.xml @@ -1,38 +1,73 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + - + android:layout_height="match_parent"> + android:fitsSystemWindows="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:layout_height="wrap_content" /> + + - + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginTop="8dp" + app:data="@{vm.products.data}" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/textView20" + tools:listitem="@layout/item_donation" /> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_donation.xml b/app/src/main/res/layout/item_donation.xml new file mode 100644 index 00000000..63b1e6da --- /dev/null +++ b/app/src/main/res/layout/item_donation.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 830e228f..f764a9a1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -64,4 +64,7 @@ weniger… Wenn du Ladestationen als Favorit markierst, tauchen sie hier auf. Spenden + Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab. + Vielen Dank! ❤️ + Etwas ist schiefgelaufen. 😕 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c3a62f32..a560204f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -63,4 +63,7 @@ less… If you add chargers as favorites, they will show up here. Donate + Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 30% off every donation. + Thank you! ❤️ + Something went wrong. 😕