Add in-app sponsor support

This commit is contained in:
世界 2024-01-25 16:25:24 +08:00
parent a0efb84461
commit d5b1ff5051
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
7 changed files with 184 additions and 2 deletions

View file

@ -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") != "") {

View file

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

View file

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

View file

@ -348,6 +348,53 @@
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sponsor"
android:textAppearance="?attr/textAppearanceTitleLarge">
</TextView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/sponsor_message" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical|end"
android:orientation="horizontal">
<Button
android:id="@+id/startSponserButton"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_start" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View file

@ -140,6 +140,10 @@
<string name="vpn_app_type_other">Other</string>
<string name="vpn_core_type_unknown">Unknown</string>
<string name="no_updates_available">No updates available</string>
<string name="sponsor">Sponsor</string>
<string name="sponsor_play">Sponsor via Play Store</string>
<string name="sponsor_message">If I\'ve defended your modern life, please consider sponsoring me.</string>
<string name="other_methods">Other methods</string>
<string name="action_start">Start</string>
</resources>

View file

@ -11,5 +11,8 @@ object Vendor : VendorInterface {
override fun checkUpdate(activity: Activity, byUser: Boolean) {
}
override fun startSponsor(activity: Activity, fallback: () -> Unit) {
fallback()
}
}

View file

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