expo-helium 0.8.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/.eslintrc.js +5 -0
- package/README.md +139 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/paywallsdk/HeliumPaywallSdkModule.kt +50 -0
- package/android/src/main/java/expo/modules/paywallsdk/HeliumPaywallSdkView.kt +30 -0
- package/build/HeliumPaywallSdk.types.d.ts +77 -0
- package/build/HeliumPaywallSdk.types.d.ts.map +1 -0
- package/build/HeliumPaywallSdk.types.js +12 -0
- package/build/HeliumPaywallSdk.types.js.map +1 -0
- package/build/HeliumPaywallSdkModule.d.ts +15 -0
- package/build/HeliumPaywallSdkModule.d.ts.map +1 -0
- package/build/HeliumPaywallSdkModule.js +4 -0
- package/build/HeliumPaywallSdkModule.js.map +1 -0
- package/build/HeliumPaywallSdkView.d.ts +4 -0
- package/build/HeliumPaywallSdkView.d.ts.map +1 -0
- package/build/HeliumPaywallSdkView.js +7 -0
- package/build/HeliumPaywallSdkView.js.map +1 -0
- package/build/index.d.ts +14 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +84 -0
- package/build/index.js.map +1 -0
- package/build/revenuecat/index.d.ts +2 -0
- package/build/revenuecat/index.d.ts.map +1 -0
- package/build/revenuecat/index.js +2 -0
- package/build/revenuecat/index.js.map +1 -0
- package/build/revenuecat/revenuecat.d.ts +16 -0
- package/build/revenuecat/revenuecat.d.ts.map +1 -0
- package/build/revenuecat/revenuecat.js +126 -0
- package/build/revenuecat/revenuecat.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/HeliumPaywallSdk.podspec +30 -0
- package/ios/HeliumPaywallSdkModule.swift +249 -0
- package/ios/HeliumPaywallSdkView.swift +38 -0
- package/package.json +55 -0
- package/src/HeliumPaywallSdk.types.ts +95 -0
- package/src/HeliumPaywallSdkModule.ts +36 -0
- package/src/HeliumPaywallSdkView.tsx +11 -0
- package/src/index.ts +113 -0
- package/src/revenuecat/index.ts +1 -0
- package/src/revenuecat/revenuecat.ts +136 -0
- package/tsconfig.json +9 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DelegateActionEvent,
|
|
3
|
+
HeliumConfig,
|
|
4
|
+
HeliumPaywallEvent,
|
|
5
|
+
NativeHeliumConfig,
|
|
6
|
+
} from "./HeliumPaywallSdk.types";
|
|
7
|
+
import HeliumPaywallSdkModule from "./HeliumPaywallSdkModule";
|
|
8
|
+
import { EventSubscription } from 'expo-modules-core';
|
|
9
|
+
|
|
10
|
+
export { default } from './HeliumPaywallSdkModule';
|
|
11
|
+
// export { default as HeliumPaywallSdkView } from './HeliumPaywallSdkView';
|
|
12
|
+
export * from './HeliumPaywallSdk.types';
|
|
13
|
+
|
|
14
|
+
function addHeliumPaywallEventListener(listener: (event: HeliumPaywallEvent) => void): EventSubscription {
|
|
15
|
+
return HeliumPaywallSdkModule.addListener('onHeliumPaywallEvent', listener);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function addDelegateActionEventListener(listener: (event: DelegateActionEvent) => void): EventSubscription {
|
|
19
|
+
return HeliumPaywallSdkModule.addListener('onDelegateActionEvent', listener);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let isInitialized = false;
|
|
23
|
+
export const initialize = (config: HeliumConfig) => {
|
|
24
|
+
if (isInitialized) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
isInitialized = true;
|
|
28
|
+
|
|
29
|
+
HeliumPaywallSdkModule.removeAllListeners('onHeliumPaywallEvent');
|
|
30
|
+
HeliumPaywallSdkModule.removeAllListeners('onDelegateActionEvent');
|
|
31
|
+
|
|
32
|
+
// Set up listener for paywall events
|
|
33
|
+
addHeliumPaywallEventListener((event) => {
|
|
34
|
+
config.onHeliumPaywallEvent(event);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Set up delegate action listener for purchase and restore operations
|
|
38
|
+
addDelegateActionEventListener(async (event) => {
|
|
39
|
+
try {
|
|
40
|
+
if (event.type === 'purchase') {
|
|
41
|
+
if (event.productId) {
|
|
42
|
+
const result = await config.purchaseConfig.makePurchase(event.productId);
|
|
43
|
+
HeliumPaywallSdkModule.handlePurchaseResult(result.status, result.error);
|
|
44
|
+
} else {
|
|
45
|
+
HeliumPaywallSdkModule.handlePurchaseResult('failed', 'No product ID for purchase event.');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (event.type === 'restore') {
|
|
49
|
+
const success = await config.purchaseConfig.restorePurchases();
|
|
50
|
+
HeliumPaywallSdkModule.handleRestoreResult(success);
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Send failure result based on action type
|
|
54
|
+
if (event.type === 'purchase') {
|
|
55
|
+
console.log('[Helium] Unexpected error: ', error);
|
|
56
|
+
HeliumPaywallSdkModule.handlePurchaseResult('failed');
|
|
57
|
+
} else if (event.type === 'restore') {
|
|
58
|
+
HeliumPaywallSdkModule.handleRestoreResult(false);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Create native config object
|
|
64
|
+
const nativeConfig: NativeHeliumConfig = {
|
|
65
|
+
apiKey: config.apiKey,
|
|
66
|
+
customUserId: config.customUserId,
|
|
67
|
+
customAPIEndpoint: config.customAPIEndpoint,
|
|
68
|
+
customUserTraits: config.customUserTraits,
|
|
69
|
+
revenueCatAppUserId: config.revenueCatAppUserId
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Initialize the native module
|
|
73
|
+
HeliumPaywallSdkModule.initialize(nativeConfig);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const presentUpsell = ({
|
|
77
|
+
triggerName,
|
|
78
|
+
onFallback
|
|
79
|
+
}: {
|
|
80
|
+
triggerName: string;
|
|
81
|
+
onFallback?: () => void;
|
|
82
|
+
}) => {
|
|
83
|
+
// todo check HeliumBridge.getFetchedTriggerNames((triggerNames: string[]) ??
|
|
84
|
+
const downloadStatus = getDownloadStatus();
|
|
85
|
+
if (downloadStatus !== 'downloadSuccess') {
|
|
86
|
+
console.log(
|
|
87
|
+
`Helium trigger "${triggerName}" not found or download status not successful. Status:`,
|
|
88
|
+
downloadStatus
|
|
89
|
+
);
|
|
90
|
+
onFallback?.();
|
|
91
|
+
HeliumPaywallSdkModule.fallbackOpenOrCloseEvent(triggerName, true, 'presented');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
HeliumPaywallSdkModule.presentUpsell(triggerName);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.log('Helium present error', error);
|
|
99
|
+
onFallback?.();
|
|
100
|
+
HeliumPaywallSdkModule.fallbackOpenOrCloseEvent(triggerName, true, 'presented');
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const hideUpsell = HeliumPaywallSdkModule.hideUpsell;
|
|
105
|
+
export const hideAllUpsells = HeliumPaywallSdkModule.hideAllUpsells;
|
|
106
|
+
export const getDownloadStatus = HeliumPaywallSdkModule.getDownloadStatus;
|
|
107
|
+
|
|
108
|
+
export {createCustomPurchaseConfig, HELIUM_CTA_NAMES} from './HeliumPaywallSdk.types';
|
|
109
|
+
|
|
110
|
+
export type {
|
|
111
|
+
HeliumTransactionStatus,
|
|
112
|
+
HeliumConfig,
|
|
113
|
+
} from './HeliumPaywallSdk.types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createRevenueCatPurchaseConfig } from "./revenuecat";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import Purchases, { PURCHASES_ERROR_CODE } from 'react-native-purchases';
|
|
2
|
+
import type { PurchasesError, PurchasesPackage, CustomerInfoUpdateListener, CustomerInfo, PurchasesEntitlementInfo } from 'react-native-purchases';
|
|
3
|
+
import {HeliumPurchaseConfig, HeliumPurchaseResult} from "../HeliumPaywallSdk.types";
|
|
4
|
+
|
|
5
|
+
// Rename the factory function
|
|
6
|
+
export function createRevenueCatPurchaseConfig(config?: {
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
}): HeliumPurchaseConfig {
|
|
9
|
+
const rcHandler = new RevenueCatHeliumHandler(config?.apiKey);
|
|
10
|
+
return {
|
|
11
|
+
apiKey: config?.apiKey,
|
|
12
|
+
makePurchase: rcHandler.makePurchase.bind(rcHandler),
|
|
13
|
+
restorePurchases: rcHandler.restorePurchases.bind(rcHandler),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class RevenueCatHeliumHandler {
|
|
18
|
+
private productIdToPackageMapping: Record<string, PurchasesPackage> = {};
|
|
19
|
+
private isMappingInitialized: boolean = false;
|
|
20
|
+
private initializationPromise: Promise<void> | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(apiKey?: string) {
|
|
23
|
+
if (apiKey) {
|
|
24
|
+
Purchases.configure({ apiKey });
|
|
25
|
+
} else {
|
|
26
|
+
}
|
|
27
|
+
this.initializePackageMapping();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private async initializePackageMapping(): Promise<void> {
|
|
31
|
+
if (this.initializationPromise) {
|
|
32
|
+
return this.initializationPromise;
|
|
33
|
+
}
|
|
34
|
+
this.initializationPromise = (async () => {
|
|
35
|
+
try {
|
|
36
|
+
const offerings = await Purchases.getOfferings();
|
|
37
|
+
if (offerings.current?.availablePackages) {
|
|
38
|
+
offerings.current.availablePackages.forEach((pkg: PurchasesPackage) => {
|
|
39
|
+
if (pkg.product?.identifier) {
|
|
40
|
+
this.productIdToPackageMapping[pkg.product.identifier] = pkg;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
}
|
|
45
|
+
this.isMappingInitialized = true;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.isMappingInitialized = false;
|
|
48
|
+
} finally {
|
|
49
|
+
this.initializationPromise = null;
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
return this.initializationPromise;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async ensureMappingInitialized(): Promise<void> {
|
|
56
|
+
if (!this.isMappingInitialized && !this.initializationPromise) {
|
|
57
|
+
await this.initializePackageMapping();
|
|
58
|
+
} else if (this.initializationPromise) {
|
|
59
|
+
await this.initializationPromise;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async makePurchase(productId: string): Promise<HeliumPurchaseResult> {
|
|
64
|
+
await this.ensureMappingInitialized();
|
|
65
|
+
|
|
66
|
+
const pkg: PurchasesPackage | undefined = this.productIdToPackageMapping[productId];
|
|
67
|
+
if (!pkg) {
|
|
68
|
+
return { status: 'failed', error: `RevenueCat Package not found for ID: ${productId}` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const { customerInfo } = await Purchases.purchasePackage(pkg);
|
|
73
|
+
const isActive = this.isProductActive(customerInfo, productId);
|
|
74
|
+
if (isActive) {
|
|
75
|
+
return { status: 'purchased' };
|
|
76
|
+
} else {
|
|
77
|
+
// This case might occur if the purchase succeeded but the entitlement wasn't immediately active
|
|
78
|
+
// or if a different product became active.
|
|
79
|
+
// Consider if polling/listening might be needed here too, similar to pending.
|
|
80
|
+
// For now, returning failed as the specific product isn't confirmed active.
|
|
81
|
+
return { status: 'failed', error: 'Purchase possibly complete but entitlement/subscription not active for this product.' };
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const purchasesError = error as PurchasesError;
|
|
85
|
+
|
|
86
|
+
if (purchasesError?.code === PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {
|
|
87
|
+
// Wait for a terminal state for up to 5 seconds
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
// Define the listener function separately to remove it later
|
|
90
|
+
const updateListener: CustomerInfoUpdateListener = (updatedCustomerInfo: CustomerInfo) => {
|
|
91
|
+
const isActive = this.isProductActive(updatedCustomerInfo, productId);
|
|
92
|
+
if (isActive) {
|
|
93
|
+
clearTimeout(timeoutId);
|
|
94
|
+
// Remove listener using the function reference
|
|
95
|
+
Purchases.removeCustomerInfoUpdateListener(updateListener);
|
|
96
|
+
resolve({ status: 'purchased' });
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const timeoutId = setTimeout(() => {
|
|
101
|
+
// Remove listener using the function reference on timeout
|
|
102
|
+
Purchases.removeCustomerInfoUpdateListener(updateListener);
|
|
103
|
+
resolve({ status: 'pending' });
|
|
104
|
+
}, 5000);
|
|
105
|
+
|
|
106
|
+
// Add the listener
|
|
107
|
+
Purchases.addCustomerInfoUpdateListener(updateListener);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (purchasesError?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
|
|
112
|
+
return { status: 'cancelled' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle other errors
|
|
116
|
+
return { status: 'failed', error: purchasesError?.message || 'RevenueCat purchase failed.' };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Helper function to check if a product is active in CustomerInfo
|
|
121
|
+
private isProductActive(customerInfo: CustomerInfo, productId: string): boolean {
|
|
122
|
+
return Object.values(customerInfo.entitlements.active).some((entitlement: PurchasesEntitlementInfo) => entitlement.productIdentifier === productId)
|
|
123
|
+
|| customerInfo.activeSubscriptions.includes(productId)
|
|
124
|
+
|| customerInfo.allPurchasedProductIdentifiers.includes(productId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async restorePurchases(): Promise<boolean> {
|
|
128
|
+
try {
|
|
129
|
+
const customerInfo = await Purchases.restorePurchases();
|
|
130
|
+
const isActive = Object.keys(customerInfo.entitlements.active).length > 0;
|
|
131
|
+
return isActive;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/tsconfig.json
ADDED