Add in-app sponsor support
This commit is contained in:
@@ -113,6 +113,8 @@ dependencies {
|
|||||||
}
|
}
|
||||||
implementation 'com.google.guava:guava:32.1.2-android'
|
implementation 'com.google.guava:guava:32.1.2-android'
|
||||||
playImplementation 'com.google.android.play:app-update-ktx:2.1.0'
|
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") != "") {
|
if (getProps("APPCENTER_TOKEN") != "") {
|
||||||
|
|||||||
@@ -103,6 +103,11 @@ class SettingsFragment : Fragment() {
|
|||||||
binding.openDebugButton.setOnClickListener {
|
binding.openDebugButton.setOnClickListener {
|
||||||
startActivity(Intent(requireContext(), DebugActivity::class.java))
|
startActivity(Intent(requireContext(), DebugActivity::class.java))
|
||||||
}
|
}
|
||||||
|
binding.startSponserButton.setOnClickListener {
|
||||||
|
Vendor.startSponsor(requireActivity()) {
|
||||||
|
activity.launchCustomTab("https://sekai.icu/sponsor/")
|
||||||
|
}
|
||||||
|
}
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
reloadSettings()
|
reloadSettings()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ import android.app.Activity
|
|||||||
interface VendorInterface {
|
interface VendorInterface {
|
||||||
fun checkUpdateAvailable(): Boolean
|
fun checkUpdateAvailable(): Boolean
|
||||||
fun checkUpdate(activity: Activity, byUser: Boolean)
|
fun checkUpdate(activity: Activity, byUser: Boolean)
|
||||||
|
fun startSponsor(activity: Activity, fallback: () -> Unit)
|
||||||
}
|
}
|
||||||
@@ -348,6 +348,53 @@
|
|||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</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>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,10 @@
|
|||||||
<string name="vpn_app_type_other">Other</string>
|
<string name="vpn_app_type_other">Other</string>
|
||||||
<string name="vpn_core_type_unknown">Unknown</string>
|
<string name="vpn_core_type_unknown">Unknown</string>
|
||||||
<string name="no_updates_available">No updates available</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>
|
</resources>
|
||||||
@@ -11,5 +11,8 @@ object Vendor : VendorInterface {
|
|||||||
override fun checkUpdate(activity: Activity, byUser: Boolean) {
|
override fun checkUpdate(activity: Activity, byUser: Boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun startSponsor(activity: Activity, fallback: () -> Unit) {
|
||||||
|
fallback()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,30 @@
|
|||||||
package io.nekohasekai.sfa.vendor
|
package io.nekohasekai.sfa.vendor
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.ProgressDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
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.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
|
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
|
||||||
import com.google.android.play.core.appupdate.AppUpdateOptions
|
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.AppUpdateType
|
||||||
import com.google.android.play.core.install.model.InstallStatus
|
import com.google.android.play.core.install.model.InstallStatus
|
||||||
import com.google.android.play.core.install.model.UpdateAvailability
|
import com.google.android.play.core.install.model.UpdateAvailability
|
||||||
|
import io.nekohasekai.sfa.Application
|
||||||
import io.nekohasekai.sfa.R
|
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 {
|
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() {
|
private fun Context.showNoUpdatesDialog() {
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(io.nekohasekai.sfa.R.string.check_update)
|
.setTitle(io.nekohasekai.sfa.R.string.check_update)
|
||||||
|
|||||||
Reference in New Issue
Block a user