fix(iap): don't initialize billing on Android without google play service, closes #2630 (#2677)

This commit is contained in:
Huang Xin 2025-12-10 22:34:23 +08:00 committed by GitHub
parent 669d3950e2
commit 5141be1c3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 140 additions and 8 deletions

View file

@ -8,6 +8,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.ConnectionResult
import java.text.SimpleDateFormat
import java.util.*
@ -16,12 +18,27 @@ class BillingManager(private val activity: Activity) : PurchasesUpdatedListener
private val productsCache = mutableMapOf<String, ProductDetails>()
private var purchaseCallback: ((PurchaseData?) -> Unit)? = null
private val scope = CoroutineScope(Dispatchers.Main)
private val isGooglePlayAvailable: Boolean by lazy {
val availability = GoogleApiAvailability.getInstance()
val resultCode = availability.isGooglePlayServicesAvailable(activity)
resultCode == ConnectionResult.SUCCESS
}
companion object {
private const val TAG = "BillingManager"
}
fun isBillingAvailable(): Boolean {
return isGooglePlayAvailable
}
fun initialize(callback: (Boolean) -> Unit) {
if (!isGooglePlayAvailable) {
Log.d(TAG, "Google Play Services not available, skipping billing setup")
callback(false)
return
}
billingClient = BillingClient.newBuilder(activity)
.setListener(this)
.enablePendingPurchases()

View file

@ -511,6 +511,14 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
invoke.resolve(ret)
}
@Command
fun iap_is_available(invoke: Invoke) {
val isAvailable = billingManager.isBillingAvailable()
val result = JSObject()
result.put("available", isAvailable)
invoke.resolve(result)
}
@Command
fun iap_initialize(invoke: Invoke) {
billingManager.initialize { success ->

View file

@ -9,6 +9,7 @@ const COMMANDS: &[&str] = &[
"get_sys_fonts_list",
"intercept_keys",
"lock_screen_orientation",
"iap_is_available",
"iap_initialize",
"iap_fetch_products",
"iap_purchase_product",

View file

@ -706,6 +706,10 @@ class NativeBridgePlugin: Plugin {
}
}
@objc public func iap_is_available(_ invoke: Invoke) {
invoke.resolve(["available": true])
}
@objc public func iap_initialize(_ invoke: Invoke) {
StoreKitManager.shared.initialize()
invoke.resolve(["success": true])

View file

@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-iap-is-available"
description = "Enables the iap_is_available command without any pre-configured scope."
commands.allow = ["iap_is_available"]
[[permission]]
identifier = "deny-iap-is-available"
description = "Denies the iap_is_available command without any pre-configured scope."
commands.deny = ["iap_is_available"]

View file

@ -14,6 +14,7 @@ Default permissions for the plugin
- `allow-get-sys-fonts-list`
- `allow-intercept-keys`
- `allow-lock-screen-orientation`
- `allow-iap-is-available`
- `allow-iap-initialize`
- `allow-iap-fetch-products`
- `allow-iap-purchase-product`
@ -409,6 +410,32 @@ Denies the iap_initialize command without any pre-configured scope.
<tr>
<td>
`native-bridge:allow-iap-is-available`
</td>
<td>
Enables the iap_is_available command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-iap-is-available`
</td>
<td>
Denies the iap_is_available command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-iap-purchase-product`
</td>

View file

@ -11,6 +11,7 @@ permissions = [
"allow-get-sys-fonts-list",
"allow-intercept-keys",
"allow-lock-screen-orientation",
"allow-iap-is-available",
"allow-iap-initialize",
"allow-iap-fetch-products",
"allow-iap-purchase-product",

View file

@ -462,6 +462,18 @@
"const": "deny-iap-initialize",
"markdownDescription": "Denies the iap_initialize command without any pre-configured scope."
},
{
"description": "Enables the iap_is_available command without any pre-configured scope.",
"type": "string",
"const": "allow-iap-is-available",
"markdownDescription": "Enables the iap_is_available command without any pre-configured scope."
},
{
"description": "Denies the iap_is_available command without any pre-configured scope.",
"type": "string",
"const": "deny-iap-is-available",
"markdownDescription": "Denies the iap_is_available command without any pre-configured scope."
},
{
"description": "Enables the iap_purchase_product command without any pre-configured scope.",
"type": "string",
@ -655,10 +667,10 @@
"markdownDescription": "Denies the use_background_audio command without any pre-configured scope."
},
{
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-is-available`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
"type": "string",
"const": "default",
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-is-available`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
}
]
}

View file

@ -84,6 +84,13 @@ pub(crate) async fn lock_screen_orientation<R: Runtime>(
app.native_bridge().lock_screen_orientation(payload)
}
#[command]
pub(crate) async fn iap_is_available<R: Runtime>(
app: AppHandle<R>,
) -> Result<IAPIsAvailableResponse> {
app.native_bridge().iap_is_available()
}
#[command]
pub(crate) async fn iap_initialize<R: Runtime>(
app: AppHandle<R>,

View file

@ -74,6 +74,10 @@ impl<R: Runtime> NativeBridge<R> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn iap_is_available(&self) -> crate::Result<IAPIsAvailableResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn iap_initialize(
&self,
_payload: IAPInitializeRequest,

View file

@ -65,6 +65,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::get_sys_fonts_list,
commands::intercept_keys,
commands::lock_screen_orientation,
commands::iap_is_available,
commands::iap_initialize,
commands::iap_fetch_products,
commands::iap_purchase_product,

View file

@ -113,6 +113,14 @@ impl<R: Runtime> NativeBridge<R> {
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn iap_is_available(&self) -> crate::Result<IAPIsAvailableResponse> {
self.0
.run_mobile_plugin("iap_is_available", ())
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn iap_initialize(
&self,

View file

@ -113,6 +113,12 @@ pub struct Purchase {
pub purchase_state: String, // "purchased", "pending", "cancelled", "restored"
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IAPIsAvailableResponse {
pub available: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IAPInitializeRequest {