diff --git a/app/build.gradle b/app/build.gradle
index 7587001..204dae1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -113,6 +113,8 @@ dependencies {
}
implementation 'com.google.guava:guava:32.1.2-android'
playImplementation 'com.google.android.play:app-update-ktx:2.1.0'
+ playImplementation "com.android.billingclient:billing:6.1.0"
+ playImplementation "com.android.billingclient:billing-ktx:6.1.0"
}
if (getProps("APPCENTER_TOKEN") != "") {
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt
index 32456c6..925daf9 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt
@@ -103,6 +103,11 @@ class SettingsFragment : Fragment() {
binding.openDebugButton.setOnClickListener {
startActivity(Intent(requireContext(), DebugActivity::class.java))
}
+ binding.startSponserButton.setOnClickListener {
+ Vendor.startSponsor(requireActivity()) {
+ activity.launchCustomTab("https://sekai.icu/sponsor/")
+ }
+ }
lifecycleScope.launch(Dispatchers.IO) {
reloadSettings()
}
diff --git a/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt b/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt
index e8a6f07..b7c5189 100644
--- a/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt
@@ -5,5 +5,5 @@ import android.app.Activity
interface VendorInterface {
fun checkUpdateAvailable(): Boolean
fun checkUpdate(activity: Activity, byUser: Boolean)
-
+ fun startSponsor(activity: Activity, fallback: () -> Unit)
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
index 29a8652..4c00786 100644
--- a/app/src/main/res/layout/fragment_settings.xml
+++ b/app/src/main/res/layout/fragment_settings.xml
@@ -348,6 +348,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 05523dc..a29d84e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -140,6 +140,10 @@
Other
Unknown
No updates available
-
+ Sponsor
+ Sponsor via Play Store
+ If I\'ve defended your modern life, please consider sponsoring me.
+ Other methods
+ Start
\ No newline at end of file
diff --git a/app/src/other/java/io/nekohasekai/sfa/vendor/Vendor.kt b/app/src/other/java/io/nekohasekai/sfa/vendor/Vendor.kt
index 1e80b4e..effa5c4 100644
--- a/app/src/other/java/io/nekohasekai/sfa/vendor/Vendor.kt
+++ b/app/src/other/java/io/nekohasekai/sfa/vendor/Vendor.kt
@@ -11,5 +11,8 @@ object Vendor : VendorInterface {
override fun checkUpdate(activity: Activity, byUser: Boolean) {
}
+ override fun startSponsor(activity: Activity, fallback: () -> Unit) {
+ fallback()
+ }
}
\ No newline at end of file
diff --git a/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt b/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt
index cf3b2ba..e462a3c 100644
--- a/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt
+++ b/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt
@@ -1,15 +1,30 @@
package io.nekohasekai.sfa.vendor
import android.app.Activity
+import android.app.ProgressDialog
import android.content.Context
import android.util.Log
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingClientStateListener
+import com.android.billingclient.api.BillingFlowParams
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.ProductDetails
+import com.android.billingclient.api.QueryProductDetailsParams
+import com.android.billingclient.api.queryProductDetails
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.InstallStatus
import com.google.android.play.core.install.model.UpdateAvailability
+import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.R
+import io.nekohasekai.sfa.ktx.errorDialogBuilder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.util.concurrent.atomic.AtomicInteger
object Vendor : VendorInterface {
@@ -69,6 +84,112 @@ object Vendor : VendorInterface {
}
}
+ private lateinit var billingClient: BillingClient
+ override fun startSponsor(activity: Activity, fallback: () -> Unit) {
+ if (!::billingClient.isInitialized) {
+ billingClient = BillingClient.newBuilder(Application.application)
+ .setListener { _, _ ->
+ }
+ .enablePendingPurchases()
+ .build()
+ }
+ val dialog = ProgressDialog(activity)
+ dialog.setMessage(activity.getString(R.string.loading))
+ dialog.show()
+ billingClient.startConnection(object : BillingClientStateListener {
+ override fun onBillingSetupFinished(result: BillingResult) {
+ dialog.dismiss()
+ if (result.responseCode == BillingClient.BillingResponseCode.OK) {
+ GlobalScope.launch(Dispatchers.IO) {
+ runCatching {
+ startSponsor0(activity, fallback)
+ }.onFailure { exception ->
+ withContext(Dispatchers.Main) {
+ activity.errorDialogBuilder(exception).show()
+ }
+ }
+ }
+ } else {
+ GlobalScope.launch(Dispatchers.Main) {
+ activity.errorDialogBuilder(result.toString()).show()
+ }
+ }
+ }
+
+ override fun onBillingServiceDisconnected() {
+ }
+ })
+ }
+
+ private suspend fun startSponsor0(activity: Activity, fallback: () -> Unit) {
+ val params = QueryProductDetailsParams.newBuilder()
+ .setProductList(
+ listOf(
+ QueryProductDetailsParams.Product.newBuilder()
+ .setProductId("sponsor_circle_1")
+ .setProductType(BillingClient.ProductType.SUBS)
+ .build(),
+ QueryProductDetailsParams.Product.newBuilder()
+ .setProductId("sponsor_circle_10")
+ .setProductType(BillingClient.ProductType.SUBS)
+ .build(),
+ QueryProductDetailsParams.Product.newBuilder()
+ .setProductId("sponsor_circle_100")
+ .setProductType(BillingClient.ProductType.SUBS)
+ .build(),
+ )
+ ).build()
+ val (result, products) = billingClient.queryProductDetails(params)
+ if (result.responseCode != BillingClient.BillingResponseCode.OK) {
+ error(result.toString())
+ }
+ if (products.isNullOrEmpty()) {
+ withContext(Dispatchers.Main) {
+ fallback()
+ }
+ return
+ }
+ val selecting = products.sortedBy { it.productId.substringAfterLast("_").toInt() }
+ val selected = AtomicInteger(0)
+ withContext(Dispatchers.Main) {
+ MaterialAlertDialogBuilder(activity)
+ .setTitle(R.string.sponsor_play)
+ .setSingleChoiceItems(
+ selecting.map { it.title.removeSuffix(" (sing-box)") }.toMutableList().also {
+ it.add(activity.getString(R.string.other_methods))
+ }.toTypedArray(),
+ 0
+ ) { _, which ->
+ selected.set(which)
+ }
+ .setNeutralButton(android.R.string.cancel, null)
+ .setPositiveButton(R.string.action_start) { _, _ ->
+ if (selected.get() == selecting.size) {
+ fallback()
+ return@setPositiveButton
+ }
+ startSponsor1(activity, selecting[selected.get()])
+ }
+ .show()
+ }
+ }
+
+ private fun startSponsor1(activity: Activity, product: ProductDetails) {
+ val paramsList = listOf(
+ BillingFlowParams.ProductDetailsParams.newBuilder()
+ .setProductDetails(product)
+ .setOfferToken(product.subscriptionOfferDetails!![0].offerToken)
+ .build()
+ )
+ val flowParams = BillingFlowParams.newBuilder()
+ .setProductDetailsParamsList(paramsList)
+ .build()
+ val result = billingClient.launchBillingFlow(activity, flowParams)
+ if (result.responseCode != BillingClient.BillingResponseCode.OK) {
+ activity.errorDialogBuilder(result.toString()).show()
+ }
+ }
+
private fun Context.showNoUpdatesDialog() {
MaterialAlertDialogBuilder(this)
.setTitle(io.nekohasekai.sfa.R.string.check_update)