forked from akai/readest
This commit is contained in:
+17
@@ -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()
|
||||
|
||||
+8
@@ -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 ->
|
||||
|
||||
@@ -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",
|
||||
|
||||
+4
@@ -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])
|
||||
|
||||
+13
@@ -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"]
|
||||
+27
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
+14
-2
@@ -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`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -47,6 +47,7 @@ const DeleteConfirmationModal: React.FC<DeleteConfirmationModalProps> = ({
|
||||
|
||||
interface AccountActionsProps {
|
||||
userPlan: UserPlan;
|
||||
iapAvailable: boolean;
|
||||
onLogout: () => void;
|
||||
onResetPassword: () => void;
|
||||
onUpdateEmail: () => void;
|
||||
@@ -58,6 +59,7 @@ interface AccountActionsProps {
|
||||
|
||||
const AccountActions: React.FC<AccountActionsProps> = ({
|
||||
userPlan,
|
||||
iapAvailable,
|
||||
onLogout,
|
||||
onResetPassword,
|
||||
onUpdateEmail,
|
||||
@@ -89,7 +91,7 @@ const AccountActions: React.FC<AccountActionsProps> = ({
|
||||
}}
|
||||
/>
|
||||
<div className='flex flex-col gap-4 md:grid md:grid-cols-2 lg:grid-cols-3'>
|
||||
{appService?.hasIAP ? (
|
||||
{appService?.hasIAP && iapAvailable ? (
|
||||
<button
|
||||
onClick={onRestorePurchase}
|
||||
className='w-full rounded-lg bg-blue-100 px-6 py-3 font-medium text-blue-600 transition-colors hover:bg-blue-200 md:w-auto'
|
||||
|
||||
@@ -73,7 +73,7 @@ const ProfilePage = () => {
|
||||
const { handleLogout, handleResetPassword, handleUpdateEmail, handleConfirmDelete } =
|
||||
useUserActions();
|
||||
|
||||
const { availablePlans } = useAvailablePlans({
|
||||
const { availablePlans, iapAvailable } = useAvailablePlans({
|
||||
hasIAP: appService?.hasIAP || false,
|
||||
onError: useCallback(
|
||||
(message: string) => {
|
||||
@@ -284,12 +284,17 @@ const ProfilePage = () => {
|
||||
<PlansComparison
|
||||
availablePlans={availablePlans}
|
||||
userPlan={userProfilePlan}
|
||||
onSubscribe={appService.hasIAP ? handleIAPSubscribe : handleStripeSubscribe}
|
||||
onSubscribe={
|
||||
appService.hasIAP && iapAvailable
|
||||
? handleIAPSubscribe
|
||||
: handleStripeSubscribe
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-y-8 px-6'>
|
||||
<AccountActions
|
||||
userPlan={userProfilePlan}
|
||||
iapAvailable={iapAvailable}
|
||||
onLogout={handleLogout}
|
||||
onResetPassword={handleResetPassword}
|
||||
onUpdateEmail={handleUpdateEmail}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { fetchAndTransformIAPPlans } from '@/libs/payment/iap/client';
|
||||
import { fetchAndTransformIAPPlans, isIAPAvailable } from '@/libs/payment/iap/client';
|
||||
import { fetchStripePlans } from '@/libs/payment/stripe/client';
|
||||
import { AvailablePlan } from '@/types/quota';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
@@ -20,6 +20,7 @@ interface UseAvailablePlansParams {
|
||||
|
||||
export const useAvailablePlans = ({ hasIAP, onError }: UseAvailablePlansParams) => {
|
||||
const [availablePlans, setAvailablePlans] = useState<AvailablePlan[]>([]);
|
||||
const [iapAvailable, setIapAvailable] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<boolean> {
|
||||
const result = await invoke<IAPIsAvailableResponse>('plugin:native-bridge|iap_is_available');
|
||||
return result.available;
|
||||
}
|
||||
|
||||
async initialize(): Promise<boolean> {
|
||||
const result = await invoke<InitializeResponse>('plugin:native-bridge|iap_initialize', {
|
||||
payload: {} as InitializeRequest,
|
||||
|
||||
Reference in New Issue
Block a user