expo-helium 3.0.18 → 3.1.1
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/HeliumExperimentInfo.types.d.ts +10 -2
- package/build/HeliumExperimentInfo.types.d.ts.map +1 -1
- package/build/HeliumExperimentInfo.types.js.map +1 -1
- 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 +38 -6
- 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/HeliumPaywallSdk.podspec +1 -1
- package/ios/HeliumPaywallSdkModule.swift +0 -3
- package/package.json +1 -1
- package/src/HeliumExperimentInfo.types.ts +12 -2
- package/src/HeliumPaywallSdk.types.ts +30 -3
- package/src/index.ts +41 -6
- package/src/revenuecat/revenuecat.ts +246 -127
|
@@ -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
|
}
|