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.
Files changed (50) hide show
  1. package/.eslintrc.js +5 -0
  2. package/README.md +139 -0
  3. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  4. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  5. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  6. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  7. package/android/.gradle/8.9/gc.properties +0 -0
  8. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  9. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  10. package/android/.gradle/vcs-1/gc.properties +0 -0
  11. package/android/build.gradle +43 -0
  12. package/android/src/main/AndroidManifest.xml +2 -0
  13. package/android/src/main/java/expo/modules/paywallsdk/HeliumPaywallSdkModule.kt +50 -0
  14. package/android/src/main/java/expo/modules/paywallsdk/HeliumPaywallSdkView.kt +30 -0
  15. package/build/HeliumPaywallSdk.types.d.ts +77 -0
  16. package/build/HeliumPaywallSdk.types.d.ts.map +1 -0
  17. package/build/HeliumPaywallSdk.types.js +12 -0
  18. package/build/HeliumPaywallSdk.types.js.map +1 -0
  19. package/build/HeliumPaywallSdkModule.d.ts +15 -0
  20. package/build/HeliumPaywallSdkModule.d.ts.map +1 -0
  21. package/build/HeliumPaywallSdkModule.js +4 -0
  22. package/build/HeliumPaywallSdkModule.js.map +1 -0
  23. package/build/HeliumPaywallSdkView.d.ts +4 -0
  24. package/build/HeliumPaywallSdkView.d.ts.map +1 -0
  25. package/build/HeliumPaywallSdkView.js +7 -0
  26. package/build/HeliumPaywallSdkView.js.map +1 -0
  27. package/build/index.d.ts +14 -0
  28. package/build/index.d.ts.map +1 -0
  29. package/build/index.js +84 -0
  30. package/build/index.js.map +1 -0
  31. package/build/revenuecat/index.d.ts +2 -0
  32. package/build/revenuecat/index.d.ts.map +1 -0
  33. package/build/revenuecat/index.js +2 -0
  34. package/build/revenuecat/index.js.map +1 -0
  35. package/build/revenuecat/revenuecat.d.ts +16 -0
  36. package/build/revenuecat/revenuecat.d.ts.map +1 -0
  37. package/build/revenuecat/revenuecat.js +126 -0
  38. package/build/revenuecat/revenuecat.js.map +1 -0
  39. package/expo-module.config.json +9 -0
  40. package/ios/HeliumPaywallSdk.podspec +30 -0
  41. package/ios/HeliumPaywallSdkModule.swift +249 -0
  42. package/ios/HeliumPaywallSdkView.swift +38 -0
  43. package/package.json +55 -0
  44. package/src/HeliumPaywallSdk.types.ts +95 -0
  45. package/src/HeliumPaywallSdkModule.ts +36 -0
  46. package/src/HeliumPaywallSdkView.tsx +11 -0
  47. package/src/index.ts +113 -0
  48. package/src/revenuecat/index.ts +1 -0
  49. package/src/revenuecat/revenuecat.ts +136 -0
  50. 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
@@ -0,0 +1,9 @@
1
+ // @generated by expo-module-scripts
2
+ {
3
+ "extends": "expo-module-scripts/tsconfig.base",
4
+ "compilerOptions": {
5
+ "outDir": "./build"
6
+ },
7
+ "include": ["./src"],
8
+ "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
9
+ }