From 5141be1c3f25da7371e5142fa8226c647a201378 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Wed, 10 Dec 2025 22:34:23 +0800 Subject: [PATCH] fix(iap): don't initialize billing on Android without google play service, closes #2630 (#2677) --- .../android/src/main/java/BillingManager.kt | 17 ++++++++++++ .../src/main/java/NativeBridgePlugin.kt | 8 ++++++ .../tauri-plugin-native-bridge/build.rs | 1 + .../ios/Sources/NativeBridgePlugin.swift | 4 +++ .../commands/iap_is_available.toml | 13 +++++++++ .../permissions/autogenerated/reference.md | 27 +++++++++++++++++++ .../permissions/default.toml | 1 + .../permissions/schemas/schema.json | 16 +++++++++-- .../src/commands.rs | 7 +++++ .../tauri-plugin-native-bridge/src/desktop.rs | 4 +++ .../tauri-plugin-native-bridge/src/lib.rs | 1 + .../tauri-plugin-native-bridge/src/mobile.rs | 8 ++++++ .../tauri-plugin-native-bridge/src/models.rs | 6 +++++ .../app/user/components/AccountActions.tsx | 4 ++- apps/readest-app/src/app/user/page.tsx | 9 +++++-- .../src/hooks/useAvailablePlans.ts | 8 +++--- .../src/libs/payment/iap/client.ts | 5 ++++ apps/readest-app/src/utils/iap.ts | 9 +++++++ 18 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/iap_is_available.toml diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/BillingManager.kt b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/BillingManager.kt index 666fd052..996d5132 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/BillingManager.kt +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/BillingManager.kt @@ -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() 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() diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt index f10354bb..83c8681a 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt @@ -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 -> diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs index 29ea7714..c61a92e9 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs @@ -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", diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift index 6f713d7c..78457c23 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift @@ -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]) diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/iap_is_available.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/iap_is_available.toml new file mode 100644 index 00000000..1a330ede --- /dev/null +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/iap_is_available.toml @@ -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"] diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md index fbed275b..2a1b59e4 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md @@ -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. +`native-bridge:allow-iap-is-available` + + + + +Enables the iap_is_available command without any pre-configured scope. + + + + + + + +`native-bridge:deny-iap-is-available` + + + + +Denies the iap_is_available command without any pre-configured scope. + + + + + + + `native-bridge:allow-iap-purchase-product` diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml index c4229af8..83b54f92 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml @@ -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", diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json index f04cf37b..a3684de1 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json @@ -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`" } ] } diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs index 09fb772d..7cc77679 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs @@ -84,6 +84,13 @@ pub(crate) async fn lock_screen_orientation( app.native_bridge().lock_screen_orientation(payload) } +#[command] +pub(crate) async fn iap_is_available( + app: AppHandle, +) -> Result { + app.native_bridge().iap_is_available() +} + #[command] pub(crate) async fn iap_initialize( app: AppHandle, diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs index e23e9f2a..2550e2e1 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs @@ -74,6 +74,10 @@ impl NativeBridge { Err(crate::Error::UnsupportedPlatformError) } + pub fn iap_is_available(&self) -> crate::Result { + Err(crate::Error::UnsupportedPlatformError) + } + pub fn iap_initialize( &self, _payload: IAPInitializeRequest, diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs index cef8dfdc..91701051 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs @@ -65,6 +65,7 @@ pub fn init() -> TauriPlugin { 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, diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs index c4af527e..6f3e7b45 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs @@ -113,6 +113,14 @@ impl NativeBridge { } } +impl NativeBridge { + pub fn iap_is_available(&self) -> crate::Result { + self.0 + .run_mobile_plugin("iap_is_available", ()) + .map_err(Into::into) + } +} + impl NativeBridge { pub fn iap_initialize( &self, diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs index b591269a..c19ba668 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs @@ -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 { diff --git a/apps/readest-app/src/app/user/components/AccountActions.tsx b/apps/readest-app/src/app/user/components/AccountActions.tsx index 8fbc5c44..fdeecedb 100644 --- a/apps/readest-app/src/app/user/components/AccountActions.tsx +++ b/apps/readest-app/src/app/user/components/AccountActions.tsx @@ -47,6 +47,7 @@ const DeleteConfirmationModal: React.FC = ({ interface AccountActionsProps { userPlan: UserPlan; + iapAvailable: boolean; onLogout: () => void; onResetPassword: () => void; onUpdateEmail: () => void; @@ -58,6 +59,7 @@ interface AccountActionsProps { const AccountActions: React.FC = ({ userPlan, + iapAvailable, onLogout, onResetPassword, onUpdateEmail, @@ -89,7 +91,7 @@ const AccountActions: React.FC = ({ }} />
- {appService?.hasIAP ? ( + {appService?.hasIAP && iapAvailable ? (
{ const [availablePlans, setAvailablePlans] = useState([]); + const [iapAvailable, setIapAvailable] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -29,9 +30,10 @@ export const useAvailablePlans = ({ hasIAP, onError }: UseAvailablePlansParams) setError(null); try { - if (hasIAP) { + if (hasIAP && (await isIAPAvailable())) { const plans = await fetchAndTransformIAPPlans(IAP_PRODUCT_IDS); setAvailablePlans(plans); + setIapAvailable(true); } else { const plans = await fetchStripePlans(); setAvailablePlans(plans); @@ -52,5 +54,5 @@ export const useAvailablePlans = ({ hasIAP, onError }: UseAvailablePlansParams) fetchPlans(); }, [hasIAP, onError]); - return { availablePlans, loading, error }; + return { availablePlans, iapAvailable, loading, error }; }; diff --git a/apps/readest-app/src/libs/payment/iap/client.ts b/apps/readest-app/src/libs/payment/iap/client.ts index 16cdfa9c..2f26c2c3 100644 --- a/apps/readest-app/src/libs/payment/iap/client.ts +++ b/apps/readest-app/src/libs/payment/iap/client.ts @@ -17,6 +17,11 @@ export const getPurchaseVerifyParams = (purchase: IAPPurchase) => { }); }; +export const isIAPAvailable = async () => { + const available = await IAPService.isAvailable(); + return available; +}; + export const purchaseIAPProduct = async (productId: string) => { const iapService = new IAPService(); const purchase = await iapService.purchaseProduct(productId); diff --git a/apps/readest-app/src/utils/iap.ts b/apps/readest-app/src/utils/iap.ts index cb715feb..8d5c81d6 100644 --- a/apps/readest-app/src/utils/iap.ts +++ b/apps/readest-app/src/utils/iap.ts @@ -35,6 +35,10 @@ interface AndroidPurchase { export type IAPPurchase = IAPPurchaseBase & (IOSPurchase | AndroidPurchase); +interface IAPIsAvailableResponse { + available: boolean; +} + interface InitializeRequest { publicKey?: string; } @@ -64,6 +68,11 @@ interface RestorePurchasesResponse { } export class IAPService { + static async isAvailable(): Promise { + const result = await invoke('plugin:native-bridge|iap_is_available'); + return result.available; + } + async initialize(): Promise { const result = await invoke('plugin:native-bridge|iap_initialize', { payload: {} as InitializeRequest,