expo-helium 3.0.18 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +7 -0
- package/android/src/main/java/expo/modules/paywallsdk/HeliumPaywallSdkModule.kt +652 -26
- package/build/HeliumPaywallSdk.types.d.ts +24 -2
- package/build/HeliumPaywallSdk.types.d.ts.map +1 -1
- package/build/HeliumPaywallSdk.types.js +2 -0
- package/build/HeliumPaywallSdk.types.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +35 -4
- package/build/index.js.map +1 -1
- package/build/revenuecat/revenuecat.d.ts +10 -2
- package/build/revenuecat/revenuecat.d.ts.map +1 -1
- package/build/revenuecat/revenuecat.js +120 -28
- package/build/revenuecat/revenuecat.js.map +1 -1
- package/ios/HeliumPaywallSdkModule.swift +0 -3
- package/package.json +1 -1
- package/src/HeliumPaywallSdk.types.ts +30 -3
- package/src/index.ts +38 -4
- package/src/revenuecat/revenuecat.ts +246 -127
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"revenuecat.js","sourceRoot":"","sources":["../../src/revenuecat/revenuecat.ts"],"names":[],"mappings":"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","sourcesContent":["import Purchases, {PURCHASES_ERROR_CODE, PurchasesStoreProduct} from 'react-native-purchases';\nimport type { PurchasesError, PurchasesPackage, CustomerInfoUpdateListener, CustomerInfo, PurchasesEntitlementInfo } from 'react-native-purchases';\nimport {HeliumPurchaseConfig, HeliumPurchaseResult} from \"../HeliumPaywallSdk.types\";\nimport {setRevenueCatAppUserId} from \"../index\";\n\n// Rename the factory function\nexport function createRevenueCatPurchaseConfig(config?: {\n apiKey?: string;\n}): HeliumPurchaseConfig {\n const rcHandler = new RevenueCatHeliumHandler(config?.apiKey);\n return {\n makePurchase: rcHandler.makePurchase.bind(rcHandler),\n restorePurchases: rcHandler.restorePurchases.bind(rcHandler),\n };\n}\n\nexport class RevenueCatHeliumHandler {\n private productIdToPackageMapping: Record<string, PurchasesPackage> = {};\n private isMappingInitialized: boolean = false;\n private initializationPromise: Promise<void> | null = null;\n\n private rcProductToPackageMapping: Record<string, PurchasesStoreProduct> = {};\n\n constructor(apiKey?: string) {\n if (apiKey) {\n Purchases.configure({ apiKey });\n }\n void this.initializePackageMapping();\n }\n\n private async initializePackageMapping(): Promise<void> {\n if (this.initializationPromise) {\n return this.initializationPromise;\n }\n this.initializationPromise = (async () => {\n try {\n // Keep this value as up-to-date as possible\n setRevenueCatAppUserId(await Purchases.getAppUserID());\n\n const offerings = await Purchases.getOfferings();\n const allOfferings = offerings.all;\n for (const offering of Object.values(allOfferings)) {\n offering.availablePackages.forEach((pkg: PurchasesPackage) => {\n if (pkg.product?.identifier) {\n this.productIdToPackageMapping[pkg.product.identifier] = pkg;\n }\n });\n }\n this.isMappingInitialized = true;\n } catch (error) {\n this.isMappingInitialized = false;\n } finally {\n this.initializationPromise = null;\n }\n })();\n return this.initializationPromise;\n }\n\n private async ensureMappingInitialized(): Promise<void> {\n if (!this.isMappingInitialized && !this.initializationPromise) {\n await this.initializePackageMapping();\n } else if (this.initializationPromise) {\n await this.initializationPromise;\n }\n }\n\n async makePurchase(productId: string): Promise<HeliumPurchaseResult> {\n await this.ensureMappingInitialized();\n // Keep this value as up-to-date as possible\n setRevenueCatAppUserId(await Purchases.getAppUserID());\n\n const pkg: PurchasesPackage | undefined = this.productIdToPackageMapping[productId];\n let rcProduct: PurchasesStoreProduct | undefined;\n if (!pkg) {\n // Use cached if available\n rcProduct = this.rcProductToPackageMapping[productId];\n if (!rcProduct) {\n // Try to retrieve now\n try {\n const rcProducts = await Purchases.getProducts([productId]);\n rcProduct = rcProducts.length > 0 ? rcProducts[0] : undefined;\n } catch {\n // 'failed' status will be returned\n }\n if (rcProduct) {\n this.rcProductToPackageMapping[productId] = rcProduct;\n }\n }\n }\n\n try {\n let customerInfo: CustomerInfo;\n if (pkg) {\n customerInfo = (await Purchases.purchasePackage(pkg)).customerInfo;\n } else if (rcProduct) {\n customerInfo = (await Purchases.purchaseStoreProduct(rcProduct)).customerInfo;\n } else {\n return { status: 'failed', error: `RevenueCat Product/Package not found for ID: ${productId}` };\n }\n const isActive = this.isProductActive(customerInfo, productId);\n if (isActive) {\n return { status: 'purchased' };\n } else {\n // This case might occur if the purchase succeeded but the entitlement wasn't immediately active\n // or if a different product became active.\n // Consider if polling/listening might be needed here too, similar to pending.\n // For now, returning failed as the specific product isn't confirmed active.\n return { status: 'failed', error: 'Purchase possibly complete but entitlement/subscription not active for this product.' };\n }\n } catch (error) {\n const purchasesError = error as PurchasesError;\n\n if (purchasesError?.code === PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {\n // Wait for a terminal state for up to 5 seconds\n return new Promise((resolve) => {\n // Define the listener function separately to remove it later\n const updateListener: CustomerInfoUpdateListener = (updatedCustomerInfo: CustomerInfo) => {\n const isActive = this.isProductActive(updatedCustomerInfo, productId);\n if (isActive) {\n clearTimeout(timeoutId);\n // Remove listener using the function reference\n Purchases.removeCustomerInfoUpdateListener(updateListener);\n resolve({ status: 'purchased' });\n }\n };\n\n const timeoutId = setTimeout(() => {\n // Remove listener using the function reference on timeout\n Purchases.removeCustomerInfoUpdateListener(updateListener);\n resolve({ status: 'pending' });\n }, 5000);\n\n // Add the listener\n Purchases.addCustomerInfoUpdateListener(updateListener);\n });\n }\n\n if (purchasesError?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {\n return { status: 'cancelled' };\n }\n\n // Handle other errors\n return { status: 'failed', error: purchasesError?.message || 'RevenueCat purchase failed.' };\n }\n }\n\n // Helper function to check if a product is active in CustomerInfo\n private isProductActive(customerInfo: CustomerInfo, productId: string): boolean {\n return Object.values(customerInfo.entitlements.active).some((entitlement: PurchasesEntitlementInfo) => entitlement.productIdentifier === productId)\n || customerInfo.activeSubscriptions.includes(productId)\n || customerInfo.allPurchasedProductIdentifiers.includes(productId);\n }\n\n async restorePurchases(): Promise<boolean> {\n try {\n const customerInfo = await Purchases.restorePurchases();\n const isActive = Object.keys(customerInfo.entitlements.active).length > 0;\n return isActive;\n } catch (error) {\n return false;\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"revenuecat.js","sourceRoot":"","sources":["../../src/revenuecat/revenuecat.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n CustomerInfo,\n PurchasesEntitlementInfo,\n PurchasesError,\n PurchasesPackage,\n SubscriptionOption\n} from 'react-native-purchases';\nimport Purchases, {PURCHASES_ERROR_CODE, PurchasesStoreProduct} from 'react-native-purchases';\nimport {Platform} from 'react-native';\nimport {HeliumPurchaseConfig, HeliumPurchaseResult} from \"../HeliumPaywallSdk.types\";\nimport {setRevenueCatAppUserId} from \"../index\";\n\n// Rename the factory function\nexport function createRevenueCatPurchaseConfig(config?: {\n apiKey?: string;\n apiKeyIOS?: string;\n apiKeyAndroid?: string;\n}): HeliumPurchaseConfig {\n const rcHandler = new RevenueCatHeliumHandler(config);\n return {\n makePurchaseIOS: rcHandler.makePurchaseIOS.bind(rcHandler),\n makePurchaseAndroid: rcHandler.makePurchaseAndroid.bind(rcHandler),\n restorePurchases: rcHandler.restorePurchases.bind(rcHandler),\n };\n}\n\nexport class RevenueCatHeliumHandler {\n private productIdToPackageMapping: Record<string, PurchasesPackage> = {};\n private isMappingInitialized: boolean = false;\n private initializationPromise: Promise<void> | null = null;\n\n private rcProductToPackageMapping: Record<string, PurchasesStoreProduct> = {};\n\n constructor(config?: { apiKey?: string; apiKeyIOS?: string; apiKeyAndroid?: string }) {\n // Determine which API key to use based on platform\n let effectiveApiKey: string | undefined;\n if (Platform.OS === 'ios' && config?.apiKeyIOS) {\n effectiveApiKey = config.apiKeyIOS;\n } else if (Platform.OS === 'android' && config?.apiKeyAndroid) {\n effectiveApiKey = config.apiKeyAndroid;\n } else {\n effectiveApiKey = config?.apiKey;\n }\n\n if (effectiveApiKey) {\n Purchases.configure({apiKey: effectiveApiKey});\n }\n void this.initializePackageMapping();\n }\n\n private async initializePackageMapping(): Promise<void> {\n if (this.initializationPromise) {\n return this.initializationPromise;\n }\n this.initializationPromise = (async () => {\n try {\n // Keep this value as up-to-date as possible\n setRevenueCatAppUserId(await Purchases.getAppUserID());\n\n const offerings = await Purchases.getOfferings();\n const allOfferings = offerings.all;\n for (const offering of Object.values(allOfferings)) {\n offering.availablePackages.forEach((pkg: PurchasesPackage) => {\n if (pkg.product?.identifier) {\n this.productIdToPackageMapping[pkg.product.identifier] = pkg;\n }\n });\n }\n this.isMappingInitialized = true;\n } catch (error) {\n this.isMappingInitialized = false;\n } finally {\n this.initializationPromise = null;\n }\n })();\n return this.initializationPromise;\n }\n\n private async ensureMappingInitialized(): Promise<void> {\n if (!this.isMappingInitialized && !this.initializationPromise) {\n await this.initializePackageMapping();\n } else if (this.initializationPromise) {\n await this.initializationPromise;\n }\n }\n\n async makePurchaseIOS(productId: string): Promise<HeliumPurchaseResult> {\n // Keep this value as up-to-date as possible\n setRevenueCatAppUserId(await Purchases.getAppUserID());\n\n await this.ensureMappingInitialized();\n const pkg: PurchasesPackage | undefined = this.productIdToPackageMapping[productId];\n let rcProduct: PurchasesStoreProduct | undefined;\n if (!pkg) {\n // Use cached if available\n rcProduct = this.rcProductToPackageMapping[productId];\n if (!rcProduct) {\n // Try to retrieve now\n try {\n const rcProducts = await Purchases.getProducts([productId]);\n rcProduct = rcProducts.length > 0 ? rcProducts[0] : undefined;\n } catch {\n // 'failed' status will be returned\n }\n if (rcProduct) {\n this.rcProductToPackageMapping[productId] = rcProduct;\n }\n }\n }\n\n try {\n let customerInfo: CustomerInfo;\n if (pkg) {\n customerInfo = (await Purchases.purchasePackage(pkg)).customerInfo;\n } else if (rcProduct) {\n customerInfo = (await Purchases.purchaseStoreProduct(rcProduct)).customerInfo;\n } else {\n return {status: 'failed', error: `RevenueCat Product/Package not found for ID: ${productId}`};\n }\n const isActive = this.isProductActive(customerInfo, productId);\n if (isActive) {\n return {status: 'purchased'};\n } else {\n // This case might occur if the purchase succeeded but the entitlement wasn't immediately active\n // or if a different product became active.\n // Consider if polling/listening might be needed here too, similar to pending.\n // For now, returning failed as the specific product isn't confirmed active.\n return {\n status: 'failed',\n error: 'Purchase possibly complete but entitlement/subscription not active for this product.'\n };\n }\n } catch (error) {\n const purchasesError = error as PurchasesError;\n\n if (purchasesError?.code === PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {\n return {status: 'pending'};\n }\n\n if (purchasesError?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {\n return {status: 'cancelled'};\n }\n\n // Handle other errors\n return {status: 'failed', error: purchasesError?.message || 'RevenueCat purchase failed.'};\n }\n }\n\n // Android-specific purchase logic (completely separated from iOS)\n async makePurchaseAndroid(productId: string, basePlanId?: string, offerId?: string): Promise<HeliumPurchaseResult> {\n // Keep this value as up-to-date as possible\n setRevenueCatAppUserId(await Purchases.getAppUserID());\n\n // Handle subscription with base plan or offer\n if (basePlanId || offerId) {\n const subscriptionOption = await this.findAndroidSubscriptionOption(\n productId,\n basePlanId,\n offerId\n );\n\n if (subscriptionOption) {\n try {\n const customerInfo = (await Purchases.purchaseSubscriptionOption(subscriptionOption)).customerInfo;\n\n const isActive = this.isProductActive(customerInfo, productId);\n if (isActive) {\n return {status: 'purchased'};\n } else {\n return {\n status: 'failed',\n error: 'Purchase possibly complete but entitlement/subscription not active for this product.'\n };\n }\n } catch (error) {\n const purchasesError = error as PurchasesError;\n\n if (purchasesError?.code === PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {\n return {status: 'pending'};\n }\n\n if (purchasesError?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {\n return {status: 'cancelled'};\n }\n\n return {status: 'failed', error: purchasesError?.message || 'RevenueCat purchase failed.'};\n }\n }\n }\n\n // Handle one-time purchase or subscription that didn't have matching base plan / offer\n let rcProduct: PurchasesStoreProduct;\n try {\n const products = await Purchases.getProducts([productId]);\n if (products.length === 0) {\n return {status: 'failed', error: `Android product not found: ${productId}`};\n }\n rcProduct = products[0];\n } catch {\n return {status: 'failed', error: `Failed to retrieve Android product: ${productId}`};\n }\n\n try {\n const customerInfo = (await Purchases.purchaseStoreProduct(rcProduct)).customerInfo;\n\n const isActive = this.isProductActive(customerInfo, productId);\n if (isActive) {\n return {status: 'purchased'};\n } else {\n return {\n status: 'failed',\n error: 'Purchase possibly complete but entitlement/subscription not active for this product.'\n };\n }\n } catch (error) {\n const purchasesError = error as PurchasesError;\n\n if (purchasesError?.code === PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {\n return {status: 'pending'};\n }\n\n if (purchasesError?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {\n return {status: 'cancelled'};\n }\n\n return {status: 'failed', error: purchasesError?.message || 'RevenueCat purchase failed.'};\n }\n }\n\n // Android helper: Find subscription option\n private async findAndroidSubscriptionOption(\n productId: string,\n basePlanId?: string,\n offerId?: string\n ): Promise<SubscriptionOption | undefined> {\n try {\n const products = await Purchases.getProducts([productId]);\n if (products.length === 0) {\n return undefined;\n }\n\n const product = products[0];\n\n if (!product.subscriptionOptions || product.subscriptionOptions.length === 0) {\n return undefined;\n }\n\n let subscriptionOption: SubscriptionOption | undefined;\n\n if (offerId && basePlanId) {\n // Look for specific offer: \"basePlanId:offerId\"\n const targetId = `${basePlanId}:${offerId}`;\n subscriptionOption = product.subscriptionOptions.find(opt => opt.id === targetId);\n } else if (basePlanId) {\n subscriptionOption = product.subscriptionOptions.find(\n opt => opt.id === basePlanId && opt.isBasePlan\n );\n }\n\n return subscriptionOption;\n } catch (error) {\n return undefined;\n }\n }\n\n // Helper function to check if a product is active in CustomerInfo\n private isProductActive(customerInfo: CustomerInfo, productId: string): boolean {\n return Object.values(customerInfo.entitlements.active).some((entitlement: PurchasesEntitlementInfo) => entitlement.productIdentifier === productId)\n || customerInfo.activeSubscriptions.includes(productId)\n || customerInfo.allPurchasedProductIdentifiers.includes(productId);\n }\n\n async restorePurchases(): Promise<boolean> {\n try {\n const customerInfo = await Purchases.restorePurchases();\n const isActive = Object.keys(customerInfo.entitlements.active).length > 0;\n return isActive;\n } catch (error) {\n return false;\n }\n }\n}\n"]}
|
|
@@ -43,13 +43,11 @@ private class NativeModuleManager {
|
|
|
43
43
|
// Store active operations
|
|
44
44
|
var activePurchaseContinuation: CheckedContinuation<HeliumPaywallTransactionStatus, Never>?
|
|
45
45
|
var activeRestoreContinuation: CheckedContinuation<Bool, Never>?
|
|
46
|
-
var currentProductId: String?
|
|
47
46
|
|
|
48
47
|
private init() {}
|
|
49
48
|
|
|
50
49
|
func clearPurchase() {
|
|
51
50
|
activePurchaseContinuation = nil
|
|
52
|
-
currentProductId = nil
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
func clearRestore() {
|
|
@@ -140,7 +138,6 @@ public class HeliumPaywallSdkModule: Module {
|
|
|
140
138
|
|
|
141
139
|
return await withCheckedContinuation { continuation in
|
|
142
140
|
NativeModuleManager.shared.activePurchaseContinuation = continuation
|
|
143
|
-
NativeModuleManager.shared.currentProductId = productId
|
|
144
141
|
|
|
145
142
|
// Send event to JavaScript
|
|
146
143
|
module.sendEvent("onDelegateActionEvent", [
|
package/package.json
CHANGED
|
@@ -58,6 +58,10 @@ export type HeliumPaywallEvent = {
|
|
|
58
58
|
export type DelegateActionEvent = {
|
|
59
59
|
type: 'purchase' | 'restore';
|
|
60
60
|
productId?: string;
|
|
61
|
+
/** Android-specific: Base plan ID for subscriptions */
|
|
62
|
+
basePlanId?: string;
|
|
63
|
+
/** Android-specific: Offer ID for promotional offers */
|
|
64
|
+
offerId?: string;
|
|
61
65
|
};
|
|
62
66
|
|
|
63
67
|
export type HeliumPaywallSdkViewProps = {
|
|
@@ -73,26 +77,43 @@ export type HeliumPurchaseResult = {
|
|
|
73
77
|
};
|
|
74
78
|
export type HeliumDownloadStatus = 'downloadSuccess' | 'downloadFailure' | 'inProgress' | 'notDownloadedYet';
|
|
75
79
|
export type HeliumLightDarkMode = 'light' | 'dark' | 'system';
|
|
80
|
+
export type HeliumEnvironment = 'sandbox' | 'production';
|
|
76
81
|
|
|
77
82
|
// --- Purchase Configuration Types ---
|
|
78
83
|
|
|
79
84
|
/** Interface for providing custom purchase handling logic. */
|
|
80
|
-
|
|
81
85
|
export interface HeliumPurchaseConfig {
|
|
82
|
-
|
|
86
|
+
/**
|
|
87
|
+
* @deprecated Use makePurchaseIOS / makePurchaseAndroid instead for platform-specific handling.
|
|
88
|
+
* This method will continue to work for backward compatibility but doesn't provide Android subscription parameters.
|
|
89
|
+
*/
|
|
90
|
+
makePurchase?: (productId: string) => Promise<HeliumPurchaseResult>;
|
|
91
|
+
|
|
92
|
+
/** iOS-specific purchase handler. Receives a simple product ID string. */
|
|
93
|
+
makePurchaseIOS?: (productId: string) => Promise<HeliumPurchaseResult>;
|
|
94
|
+
|
|
95
|
+
/** Android-specific purchase handler. Receives product ID and optional subscription parameters. */
|
|
96
|
+
makePurchaseAndroid?: (productId: string, basePlanId?: string, offerId?: string) => Promise<HeliumPurchaseResult>;
|
|
97
|
+
|
|
83
98
|
restorePurchases: () => Promise<boolean>;
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
// Helper function for creating Custom Purchase Config
|
|
87
102
|
export function createCustomPurchaseConfig(callbacks: {
|
|
88
|
-
|
|
103
|
+
/** @deprecated Use makePurchaseIOS or makePurchaseAndroid instead */
|
|
104
|
+
makePurchase?: (productId: string) => Promise<HeliumPurchaseResult>;
|
|
105
|
+
makePurchaseIOS?: (productId: string) => Promise<HeliumPurchaseResult>;
|
|
106
|
+
makePurchaseAndroid?: (productId: string, basePlanId?: string, offerId?: string) => Promise<HeliumPurchaseResult>;
|
|
89
107
|
restorePurchases: () => Promise<boolean>;
|
|
90
108
|
}): HeliumPurchaseConfig {
|
|
91
109
|
return {
|
|
92
110
|
makePurchase: callbacks.makePurchase,
|
|
111
|
+
makePurchaseIOS: callbacks.makePurchaseIOS,
|
|
112
|
+
makePurchaseAndroid: callbacks.makePurchaseAndroid,
|
|
93
113
|
restorePurchases: callbacks.restorePurchases,
|
|
94
114
|
};
|
|
95
115
|
}
|
|
116
|
+
//wtf do we even have createCustomPurchaseConfig...
|
|
96
117
|
|
|
97
118
|
export type TriggerLoadingConfig = {
|
|
98
119
|
/** Whether to show loading state for this trigger. Set to nil to use the global `useLoadingState` setting. */
|
|
@@ -139,6 +160,11 @@ export interface HeliumConfig {
|
|
|
139
160
|
fallbackBundle?: object;
|
|
140
161
|
/** Configure loading behavior for paywalls that are mid-download. */
|
|
141
162
|
paywallLoadingConfig?: HeliumPaywallLoadingConfig;
|
|
163
|
+
/** Environment to use for Android. (iOS auto-detects this.)
|
|
164
|
+
* If not specified, Android environment will be "sandbox" if app is a debug build, "production otherwise".
|
|
165
|
+
* Recommended to pass in "sandbox" for QA builds that behave like a production build but are actually just for testing.
|
|
166
|
+
*/
|
|
167
|
+
environment?: HeliumEnvironment;
|
|
142
168
|
customUserId?: string;
|
|
143
169
|
customAPIEndpoint?: string;
|
|
144
170
|
customUserTraits?: Record<string, any>;
|
|
@@ -155,6 +181,7 @@ export interface NativeHeliumConfig {
|
|
|
155
181
|
fallbackBundleString?: string;
|
|
156
182
|
paywallLoadingConfig?: HeliumPaywallLoadingConfig;
|
|
157
183
|
useDefaultDelegate?: boolean;
|
|
184
|
+
environment?: string;
|
|
158
185
|
}
|
|
159
186
|
|
|
160
187
|
export type PresentUpsellParams = {
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { ExperimentInfo } from "./HeliumExperimentInfo.types";
|
|
|
8
8
|
import HeliumPaywallSdkModule from "./HeliumPaywallSdkModule";
|
|
9
9
|
import { EventSubscription } from 'expo-modules-core';
|
|
10
10
|
import * as ExpoFileSystem from 'expo-file-system';
|
|
11
|
+
import { Platform } from 'react-native';
|
|
11
12
|
|
|
12
13
|
export { default } from './HeliumPaywallSdkModule';
|
|
13
14
|
// export { default as HeliumPaywallSdkView } from './HeliumPaywallSdkView';
|
|
@@ -49,12 +50,44 @@ export const initialize = (config: HeliumConfig) => {
|
|
|
49
50
|
addDelegateActionEventListener(async (event) => {
|
|
50
51
|
try {
|
|
51
52
|
if (event.type === 'purchase') {
|
|
52
|
-
if (event.productId) {
|
|
53
|
-
const result = await purchaseConfig.makePurchase(event.productId);
|
|
54
|
-
HeliumPaywallSdkModule.handlePurchaseResult(result.status, result.error);
|
|
55
|
-
} else {
|
|
53
|
+
if (!event.productId) {
|
|
56
54
|
HeliumPaywallSdkModule.handlePurchaseResult('failed', 'No product ID for purchase event.');
|
|
55
|
+
return;
|
|
57
56
|
}
|
|
57
|
+
|
|
58
|
+
let result;
|
|
59
|
+
|
|
60
|
+
// Platform-specific purchase handling
|
|
61
|
+
if (Platform.OS === 'ios') {
|
|
62
|
+
// iOS: Use makePurchaseIOS if available, otherwise use deprecated makePurchase
|
|
63
|
+
if (purchaseConfig.makePurchaseIOS) {
|
|
64
|
+
result = await purchaseConfig.makePurchaseIOS(event.productId);
|
|
65
|
+
} else if (purchaseConfig.makePurchase) {
|
|
66
|
+
result = await purchaseConfig.makePurchase(event.productId);
|
|
67
|
+
} else {
|
|
68
|
+
console.log('[Helium] No iOS purchase handler configured.');
|
|
69
|
+
HeliumPaywallSdkModule.handlePurchaseResult('failed', 'No iOS purchase handler configured.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
} else if (Platform.OS === 'android') {
|
|
73
|
+
// Android: Use makePurchaseAndroid if available
|
|
74
|
+
if (purchaseConfig.makePurchaseAndroid) {
|
|
75
|
+
result = await purchaseConfig.makePurchaseAndroid(
|
|
76
|
+
event.productId,
|
|
77
|
+
event.basePlanId,
|
|
78
|
+
event.offerId
|
|
79
|
+
);
|
|
80
|
+
} else {
|
|
81
|
+
console.log('[Helium] No Android purchase handler configured.');
|
|
82
|
+
HeliumPaywallSdkModule.handlePurchaseResult('failed', 'No Android purchase handler configured.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
HeliumPaywallSdkModule.handlePurchaseResult('failed', 'Unsupported platform.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
HeliumPaywallSdkModule.handlePurchaseResult(result.status, result.error);
|
|
58
91
|
} else if (event.type === 'restore') {
|
|
59
92
|
const success = await purchaseConfig.restorePurchases();
|
|
60
93
|
HeliumPaywallSdkModule.handleRestoreResult(success);
|
|
@@ -115,6 +148,7 @@ const nativeInitializeAsync = async (config: HeliumConfig) => {
|
|
|
115
148
|
fallbackBundleString: fallbackBundleString,
|
|
116
149
|
paywallLoadingConfig: convertBooleansToMarkers(config.paywallLoadingConfig),
|
|
117
150
|
useDefaultDelegate: !config.purchaseConfig,
|
|
151
|
+
environment: config.environment,
|
|
118
152
|
};
|
|
119
153
|
|
|
120
154
|
// Initialize the native module
|
|
@@ -1,163 +1,282 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CustomerInfo,
|
|
3
|
+
PurchasesEntitlementInfo,
|
|
4
|
+
PurchasesError,
|
|
5
|
+
PurchasesPackage,
|
|
6
|
+
SubscriptionOption
|
|
7
|
+
} from 'react-native-purchases';
|
|
1
8
|
import Purchases, {PURCHASES_ERROR_CODE, PurchasesStoreProduct} from 'react-native-purchases';
|
|
2
|
-
import
|
|
9
|
+
import {Platform} from 'react-native';
|
|
3
10
|
import {HeliumPurchaseConfig, HeliumPurchaseResult} from "../HeliumPaywallSdk.types";
|
|
4
11
|
import {setRevenueCatAppUserId} from "../index";
|
|
5
12
|
|
|
6
13
|
// Rename the factory function
|
|
7
14
|
export function createRevenueCatPurchaseConfig(config?: {
|
|
8
15
|
apiKey?: string;
|
|
16
|
+
apiKeyIOS?: string;
|
|
17
|
+
apiKeyAndroid?: string;
|
|
9
18
|
}): HeliumPurchaseConfig {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
const rcHandler = new RevenueCatHeliumHandler(config);
|
|
20
|
+
return {
|
|
21
|
+
makePurchaseIOS: rcHandler.makePurchaseIOS.bind(rcHandler),
|
|
22
|
+
makePurchaseAndroid: rcHandler.makePurchaseAndroid.bind(rcHandler),
|
|
23
|
+
restorePurchases: rcHandler.restorePurchases.bind(rcHandler),
|
|
24
|
+
};
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
export class RevenueCatHeliumHandler {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
private productIdToPackageMapping: Record<string, PurchasesPackage> = {};
|
|
29
|
+
private isMappingInitialized: boolean = false;
|
|
30
|
+
private initializationPromise: Promise<void> | null = null;
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
private rcProductToPackageMapping: Record<string, PurchasesStoreProduct> = {};
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
constructor(config?: { apiKey?: string; apiKeyIOS?: string; apiKeyAndroid?: string }) {
|
|
35
|
+
// Determine which API key to use based on platform
|
|
36
|
+
let effectiveApiKey: string | undefined;
|
|
37
|
+
if (Platform.OS === 'ios' && config?.apiKeyIOS) {
|
|
38
|
+
effectiveApiKey = config.apiKeyIOS;
|
|
39
|
+
} else if (Platform.OS === 'android' && config?.apiKeyAndroid) {
|
|
40
|
+
effectiveApiKey = config.apiKeyAndroid;
|
|
41
|
+
} else {
|
|
42
|
+
effectiveApiKey = config?.apiKey;
|
|
29
43
|
}
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return this.initializationPromise;
|
|
34
|
-
}
|
|
35
|
-
this.initializationPromise = (async () => {
|
|
36
|
-
try {
|
|
37
|
-
// Keep this value as up-to-date as possible
|
|
38
|
-
setRevenueCatAppUserId(await Purchases.getAppUserID());
|
|
39
|
-
|
|
40
|
-
const offerings = await Purchases.getOfferings();
|
|
41
|
-
const allOfferings = offerings.all;
|
|
42
|
-
for (const offering of Object.values(allOfferings)) {
|
|
43
|
-
offering.availablePackages.forEach((pkg: PurchasesPackage) => {
|
|
44
|
-
if (pkg.product?.identifier) {
|
|
45
|
-
this.productIdToPackageMapping[pkg.product.identifier] = pkg;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
this.isMappingInitialized = true;
|
|
50
|
-
} catch (error) {
|
|
51
|
-
this.isMappingInitialized = false;
|
|
52
|
-
} finally {
|
|
53
|
-
this.initializationPromise = null;
|
|
54
|
-
}
|
|
55
|
-
})();
|
|
56
|
-
return this.initializationPromise;
|
|
45
|
+
if (effectiveApiKey) {
|
|
46
|
+
Purchases.configure({apiKey: effectiveApiKey});
|
|
57
47
|
}
|
|
48
|
+
void this.initializePackageMapping();
|
|
49
|
+
}
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} else if (this.initializationPromise) {
|
|
63
|
-
await this.initializationPromise;
|
|
64
|
-
}
|
|
51
|
+
private async initializePackageMapping(): Promise<void> {
|
|
52
|
+
if (this.initializationPromise) {
|
|
53
|
+
return this.initializationPromise;
|
|
65
54
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await this.ensureMappingInitialized();
|
|
55
|
+
this.initializationPromise = (async () => {
|
|
56
|
+
try {
|
|
69
57
|
// Keep this value as up-to-date as possible
|
|
70
58
|
setRevenueCatAppUserId(await Purchases.getAppUserID());
|
|
71
59
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Try to retrieve now
|
|
79
|
-
try {
|
|
80
|
-
const rcProducts = await Purchases.getProducts([productId]);
|
|
81
|
-
rcProduct = rcProducts.length > 0 ? rcProducts[0] : undefined;
|
|
82
|
-
} catch {
|
|
83
|
-
// 'failed' status will be returned
|
|
84
|
-
}
|
|
85
|
-
if (rcProduct) {
|
|
86
|
-
this.rcProductToPackageMapping[productId] = rcProduct;
|
|
87
|
-
}
|
|
60
|
+
const offerings = await Purchases.getOfferings();
|
|
61
|
+
const allOfferings = offerings.all;
|
|
62
|
+
for (const offering of Object.values(allOfferings)) {
|
|
63
|
+
offering.availablePackages.forEach((pkg: PurchasesPackage) => {
|
|
64
|
+
if (pkg.product?.identifier) {
|
|
65
|
+
this.productIdToPackageMapping[pkg.product.identifier] = pkg;
|
|
88
66
|
}
|
|
67
|
+
});
|
|
89
68
|
}
|
|
69
|
+
this.isMappingInitialized = true;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.isMappingInitialized = false;
|
|
72
|
+
} finally {
|
|
73
|
+
this.initializationPromise = null;
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
return this.initializationPromise;
|
|
77
|
+
}
|
|
90
78
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return { status: 'failed', error: `RevenueCat Product/Package not found for ID: ${productId}` };
|
|
99
|
-
}
|
|
100
|
-
const isActive = this.isProductActive(customerInfo, productId);
|
|
101
|
-
if (isActive) {
|
|
102
|
-
return { status: 'purchased' };
|
|
103
|
-
} else {
|
|
104
|
-
// This case might occur if the purchase succeeded but the entitlement wasn't immediately active
|
|
105
|
-
// or if a different product became active.
|
|
106
|
-
// Consider if polling/listening might be needed here too, similar to pending.
|
|
107
|
-
// For now, returning failed as the specific product isn't confirmed active.
|
|
108
|
-
return { status: 'failed', error: 'Purchase possibly complete but entitlement/subscription not active for this product.' };
|
|
109
|
-
}
|
|
110
|
-
} catch (error) {
|
|
111
|
-
const purchasesError = error as PurchasesError;
|
|
112
|
-
|
|
113
|
-
if (purchasesError?.code === PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {
|
|
114
|
-
// Wait for a terminal state for up to 5 seconds
|
|
115
|
-
return new Promise((resolve) => {
|
|
116
|
-
// Define the listener function separately to remove it later
|
|
117
|
-
const updateListener: CustomerInfoUpdateListener = (updatedCustomerInfo: CustomerInfo) => {
|
|
118
|
-
const isActive = this.isProductActive(updatedCustomerInfo, productId);
|
|
119
|
-
if (isActive) {
|
|
120
|
-
clearTimeout(timeoutId);
|
|
121
|
-
// Remove listener using the function reference
|
|
122
|
-
Purchases.removeCustomerInfoUpdateListener(updateListener);
|
|
123
|
-
resolve({ status: 'purchased' });
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const timeoutId = setTimeout(() => {
|
|
128
|
-
// Remove listener using the function reference on timeout
|
|
129
|
-
Purchases.removeCustomerInfoUpdateListener(updateListener);
|
|
130
|
-
resolve({ status: 'pending' });
|
|
131
|
-
}, 5000);
|
|
132
|
-
|
|
133
|
-
// Add the listener
|
|
134
|
-
Purchases.addCustomerInfoUpdateListener(updateListener);
|
|
135
|
-
});
|
|
136
|
-
}
|
|
79
|
+
private async ensureMappingInitialized(): Promise<void> {
|
|
80
|
+
if (!this.isMappingInitialized && !this.initializationPromise) {
|
|
81
|
+
await this.initializePackageMapping();
|
|
82
|
+
} else if (this.initializationPromise) {
|
|
83
|
+
await this.initializationPromise;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
137
86
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
87
|
+
async makePurchaseIOS(productId: string): Promise<HeliumPurchaseResult> {
|
|
88
|
+
// Keep this value as up-to-date as possible
|
|
89
|
+
setRevenueCatAppUserId(await Purchases.getAppUserID());
|
|
141
90
|
|
|
142
|
-
|
|
143
|
-
|
|
91
|
+
await this.ensureMappingInitialized();
|
|
92
|
+
const pkg: PurchasesPackage | undefined = this.productIdToPackageMapping[productId];
|
|
93
|
+
let rcProduct: PurchasesStoreProduct | undefined;
|
|
94
|
+
if (!pkg) {
|
|
95
|
+
// Use cached if available
|
|
96
|
+
rcProduct = this.rcProductToPackageMapping[productId];
|
|
97
|
+
if (!rcProduct) {
|
|
98
|
+
// Try to retrieve now
|
|
99
|
+
try {
|
|
100
|
+
const rcProducts = await Purchases.getProducts([productId]);
|
|
101
|
+
rcProduct = rcProducts.length > 0 ? rcProducts[0] : undefined;
|
|
102
|
+
} catch {
|
|
103
|
+
// 'failed' status will be returned
|
|
104
|
+
}
|
|
105
|
+
if (rcProduct) {
|
|
106
|
+
this.rcProductToPackageMapping[productId] = rcProduct;
|
|
144
107
|
}
|
|
108
|
+
}
|
|
145
109
|
}
|
|
146
110
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
111
|
+
try {
|
|
112
|
+
let customerInfo: CustomerInfo;
|
|
113
|
+
if (pkg) {
|
|
114
|
+
customerInfo = (await Purchases.purchasePackage(pkg)).customerInfo;
|
|
115
|
+
} else if (rcProduct) {
|
|
116
|
+
customerInfo = (await Purchases.purchaseStoreProduct(rcProduct)).customerInfo;
|
|
117
|
+
} else {
|
|
118
|
+
return {status: 'failed', error: `RevenueCat Product/Package not found for ID: ${productId}`};
|
|
119
|
+
}
|
|
120
|
+
const isActive = this.isProductActive(customerInfo, productId);
|
|
121
|
+
if (isActive) {
|
|
122
|
+
return {status: 'purchased'};
|
|
123
|
+
} else {
|
|
124
|
+
// This case might occur if the purchase succeeded but the entitlement wasn't immediately active
|
|
125
|
+
// or if a different product became active.
|
|
126
|
+
// Consider if polling/listening might be needed here too, similar to pending.
|
|
127
|
+
// For now, returning failed as the specific product isn't confirmed active.
|
|
128
|
+
return {
|
|
129
|
+
status: 'failed',
|
|
130
|
+
error: 'Purchase possibly complete but entitlement/subscription not active for this product.'
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const purchasesError = error as PurchasesError;
|
|
135
|
+
|
|
136
|
+
if (purchasesError?.code === PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {
|
|
137
|
+
return {status: 'pending'};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (purchasesError?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
|
|
141
|
+
return {status: 'cancelled'};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Handle other errors
|
|
145
|
+
return {status: 'failed', error: purchasesError?.message || 'RevenueCat purchase failed.'};
|
|
152
146
|
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Android-specific purchase logic (completely separated from iOS)
|
|
150
|
+
async makePurchaseAndroid(productId: string, basePlanId?: string, offerId?: string): Promise<HeliumPurchaseResult> {
|
|
151
|
+
// Keep this value as up-to-date as possible
|
|
152
|
+
setRevenueCatAppUserId(await Purchases.getAppUserID());
|
|
153
|
+
|
|
154
|
+
// Handle subscription with base plan or offer
|
|
155
|
+
if (basePlanId || offerId) {
|
|
156
|
+
const subscriptionOption = await this.findAndroidSubscriptionOption(
|
|
157
|
+
productId,
|
|
158
|
+
basePlanId,
|
|
159
|
+
offerId
|
|
160
|
+
);
|
|
153
161
|
|
|
154
|
-
|
|
162
|
+
if (subscriptionOption) {
|
|
155
163
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
const customerInfo = (await Purchases.purchaseSubscriptionOption(subscriptionOption)).customerInfo;
|
|
165
|
+
|
|
166
|
+
const isActive = this.isProductActive(customerInfo, productId);
|
|
167
|
+
if (isActive) {
|
|
168
|
+
return {status: 'purchased'};
|
|
169
|
+
} else {
|
|
170
|
+
return {
|
|
171
|
+
status: 'failed',
|
|
172
|
+
error: 'Purchase possibly complete but entitlement/subscription not active for this product.'
|
|
173
|
+
};
|
|
174
|
+
}
|
|
159
175
|
} catch (error) {
|
|
160
|
-
|
|
176
|
+
const purchasesError = error as PurchasesError;
|
|
177
|
+
|
|
178
|
+
if (purchasesError?.code === PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {
|
|
179
|
+
return {status: 'pending'};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (purchasesError?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
|
|
183
|
+
return {status: 'cancelled'};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {status: 'failed', error: purchasesError?.message || 'RevenueCat purchase failed.'};
|
|
161
187
|
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Handle one-time purchase or subscription that didn't have matching base plan / offer
|
|
192
|
+
let rcProduct: PurchasesStoreProduct;
|
|
193
|
+
try {
|
|
194
|
+
const products = await Purchases.getProducts([productId]);
|
|
195
|
+
if (products.length === 0) {
|
|
196
|
+
return {status: 'failed', error: `Android product not found: ${productId}`};
|
|
197
|
+
}
|
|
198
|
+
rcProduct = products[0];
|
|
199
|
+
} catch {
|
|
200
|
+
return {status: 'failed', error: `Failed to retrieve Android product: ${productId}`};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const customerInfo = (await Purchases.purchaseStoreProduct(rcProduct)).customerInfo;
|
|
205
|
+
|
|
206
|
+
const isActive = this.isProductActive(customerInfo, productId);
|
|
207
|
+
if (isActive) {
|
|
208
|
+
return {status: 'purchased'};
|
|
209
|
+
} else {
|
|
210
|
+
return {
|
|
211
|
+
status: 'failed',
|
|
212
|
+
error: 'Purchase possibly complete but entitlement/subscription not active for this product.'
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const purchasesError = error as PurchasesError;
|
|
217
|
+
|
|
218
|
+
if (purchasesError?.code === PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {
|
|
219
|
+
return {status: 'pending'};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (purchasesError?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
|
|
223
|
+
return {status: 'cancelled'};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {status: 'failed', error: purchasesError?.message || 'RevenueCat purchase failed.'};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Android helper: Find subscription option
|
|
231
|
+
private async findAndroidSubscriptionOption(
|
|
232
|
+
productId: string,
|
|
233
|
+
basePlanId?: string,
|
|
234
|
+
offerId?: string
|
|
235
|
+
): Promise<SubscriptionOption | undefined> {
|
|
236
|
+
try {
|
|
237
|
+
const products = await Purchases.getProducts([productId]);
|
|
238
|
+
if (products.length === 0) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const product = products[0];
|
|
243
|
+
|
|
244
|
+
if (!product.subscriptionOptions || product.subscriptionOptions.length === 0) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let subscriptionOption: SubscriptionOption | undefined;
|
|
249
|
+
|
|
250
|
+
if (offerId && basePlanId) {
|
|
251
|
+
// Look for specific offer: "basePlanId:offerId"
|
|
252
|
+
const targetId = `${basePlanId}:${offerId}`;
|
|
253
|
+
subscriptionOption = product.subscriptionOptions.find(opt => opt.id === targetId);
|
|
254
|
+
} else if (basePlanId) {
|
|
255
|
+
subscriptionOption = product.subscriptionOptions.find(
|
|
256
|
+
opt => opt.id === basePlanId && opt.isBasePlan
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return subscriptionOption;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Helper function to check if a product is active in CustomerInfo
|
|
267
|
+
private isProductActive(customerInfo: CustomerInfo, productId: string): boolean {
|
|
268
|
+
return Object.values(customerInfo.entitlements.active).some((entitlement: PurchasesEntitlementInfo) => entitlement.productIdentifier === productId)
|
|
269
|
+
|| customerInfo.activeSubscriptions.includes(productId)
|
|
270
|
+
|| customerInfo.allPurchasedProductIdentifiers.includes(productId);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async restorePurchases(): Promise<boolean> {
|
|
274
|
+
try {
|
|
275
|
+
const customerInfo = await Purchases.restorePurchases();
|
|
276
|
+
const isActive = Object.keys(customerInfo.entitlements.active).length > 0;
|
|
277
|
+
return isActive;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return false;
|
|
162
280
|
}
|
|
281
|
+
}
|
|
163
282
|
}
|