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
@@ -0,0 +1,126 @@
1
+ import Purchases, { PURCHASES_ERROR_CODE } from 'react-native-purchases';
2
+ // Rename the factory function
3
+ export function createRevenueCatPurchaseConfig(config) {
4
+ const rcHandler = new RevenueCatHeliumHandler(config?.apiKey);
5
+ return {
6
+ apiKey: config?.apiKey,
7
+ makePurchase: rcHandler.makePurchase.bind(rcHandler),
8
+ restorePurchases: rcHandler.restorePurchases.bind(rcHandler),
9
+ };
10
+ }
11
+ export class RevenueCatHeliumHandler {
12
+ productIdToPackageMapping = {};
13
+ isMappingInitialized = false;
14
+ initializationPromise = null;
15
+ constructor(apiKey) {
16
+ if (apiKey) {
17
+ Purchases.configure({ apiKey });
18
+ }
19
+ else {
20
+ }
21
+ this.initializePackageMapping();
22
+ }
23
+ async initializePackageMapping() {
24
+ if (this.initializationPromise) {
25
+ return this.initializationPromise;
26
+ }
27
+ this.initializationPromise = (async () => {
28
+ try {
29
+ const offerings = await Purchases.getOfferings();
30
+ if (offerings.current?.availablePackages) {
31
+ offerings.current.availablePackages.forEach((pkg) => {
32
+ if (pkg.product?.identifier) {
33
+ this.productIdToPackageMapping[pkg.product.identifier] = pkg;
34
+ }
35
+ });
36
+ }
37
+ else {
38
+ }
39
+ this.isMappingInitialized = true;
40
+ }
41
+ catch (error) {
42
+ this.isMappingInitialized = false;
43
+ }
44
+ finally {
45
+ this.initializationPromise = null;
46
+ }
47
+ })();
48
+ return this.initializationPromise;
49
+ }
50
+ async ensureMappingInitialized() {
51
+ if (!this.isMappingInitialized && !this.initializationPromise) {
52
+ await this.initializePackageMapping();
53
+ }
54
+ else if (this.initializationPromise) {
55
+ await this.initializationPromise;
56
+ }
57
+ }
58
+ async makePurchase(productId) {
59
+ await this.ensureMappingInitialized();
60
+ const pkg = this.productIdToPackageMapping[productId];
61
+ if (!pkg) {
62
+ return { status: 'failed', error: `RevenueCat Package not found for ID: ${productId}` };
63
+ }
64
+ try {
65
+ const { customerInfo } = await Purchases.purchasePackage(pkg);
66
+ const isActive = this.isProductActive(customerInfo, productId);
67
+ if (isActive) {
68
+ return { status: 'purchased' };
69
+ }
70
+ else {
71
+ // This case might occur if the purchase succeeded but the entitlement wasn't immediately active
72
+ // or if a different product became active.
73
+ // Consider if polling/listening might be needed here too, similar to pending.
74
+ // For now, returning failed as the specific product isn't confirmed active.
75
+ return { status: 'failed', error: 'Purchase possibly complete but entitlement/subscription not active for this product.' };
76
+ }
77
+ }
78
+ catch (error) {
79
+ const purchasesError = error;
80
+ if (purchasesError?.code === PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {
81
+ // Wait for a terminal state for up to 5 seconds
82
+ return new Promise((resolve) => {
83
+ // Define the listener function separately to remove it later
84
+ const updateListener = (updatedCustomerInfo) => {
85
+ const isActive = this.isProductActive(updatedCustomerInfo, productId);
86
+ if (isActive) {
87
+ clearTimeout(timeoutId);
88
+ // Remove listener using the function reference
89
+ Purchases.removeCustomerInfoUpdateListener(updateListener);
90
+ resolve({ status: 'purchased' });
91
+ }
92
+ };
93
+ const timeoutId = setTimeout(() => {
94
+ // Remove listener using the function reference on timeout
95
+ Purchases.removeCustomerInfoUpdateListener(updateListener);
96
+ resolve({ status: 'pending' });
97
+ }, 5000);
98
+ // Add the listener
99
+ Purchases.addCustomerInfoUpdateListener(updateListener);
100
+ });
101
+ }
102
+ if (purchasesError?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
103
+ return { status: 'cancelled' };
104
+ }
105
+ // Handle other errors
106
+ return { status: 'failed', error: purchasesError?.message || 'RevenueCat purchase failed.' };
107
+ }
108
+ }
109
+ // Helper function to check if a product is active in CustomerInfo
110
+ isProductActive(customerInfo, productId) {
111
+ return Object.values(customerInfo.entitlements.active).some((entitlement) => entitlement.productIdentifier === productId)
112
+ || customerInfo.activeSubscriptions.includes(productId)
113
+ || customerInfo.allPurchasedProductIdentifiers.includes(productId);
114
+ }
115
+ async restorePurchases() {
116
+ try {
117
+ const customerInfo = await Purchases.restorePurchases();
118
+ const isActive = Object.keys(customerInfo.entitlements.active).length > 0;
119
+ return isActive;
120
+ }
121
+ catch (error) {
122
+ return false;
123
+ }
124
+ }
125
+ }
126
+ //# sourceMappingURL=revenuecat.js.map
@@ -0,0 +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 } from 'react-native-purchases';\nimport type { PurchasesError, PurchasesPackage, CustomerInfoUpdateListener, CustomerInfo, PurchasesEntitlementInfo } from 'react-native-purchases';\nimport {HeliumPurchaseConfig, HeliumPurchaseResult} from \"../HeliumPaywallSdk.types\";\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 apiKey: config?.apiKey,\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 constructor(apiKey?: string) {\n if (apiKey) {\n Purchases.configure({ apiKey });\n } else {\n }\n 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 const offerings = await Purchases.getOfferings();\n if (offerings.current?.availablePackages) {\n offerings.current.availablePackages.forEach((pkg: PurchasesPackage) => {\n if (pkg.product?.identifier) {\n this.productIdToPackageMapping[pkg.product.identifier] = pkg;\n }\n });\n } else {\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\n const pkg: PurchasesPackage | undefined = this.productIdToPackageMapping[productId];\n if (!pkg) {\n return { status: 'failed', error: `RevenueCat Package not found for ID: ${productId}` };\n }\n\n try {\n const { customerInfo } = await Purchases.purchasePackage(pkg);\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"]}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android", "web"],
3
+ "apple": {
4
+ "modules": ["HeliumPaywallSdkModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.paywallsdk.HeliumPaywallSdkModule"]
8
+ }
9
+ }
@@ -0,0 +1,30 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'HeliumPaywallSdk'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.homepage = package['homepage']
13
+ s.platforms = {
14
+ :ios => '15.1',
15
+ :tvos => '15.1'
16
+ }
17
+ s.swift_version = '5.4'
18
+ s.source = { git: 'https://github.com/salami/expo-paywall-sdk' }
19
+ s.static_framework = true
20
+
21
+ s.dependency 'ExpoModulesCore'
22
+ s.dependency 'Helium', '2.0.11'
23
+
24
+ # Swift/Objective-C compatibility
25
+ s.pod_target_xcconfig = {
26
+ 'DEFINES_MODULE' => 'YES',
27
+ }
28
+
29
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
30
+ end
@@ -0,0 +1,249 @@
1
+ import ExpoModulesCore
2
+ import Helium
3
+ import SwiftUI
4
+
5
+ // Define purchase error enum
6
+ enum PurchaseError: LocalizedError {
7
+ case unknownStatus(status: String)
8
+ case purchaseFailed(errorMsg: String)
9
+
10
+ var errorDescription: String? {
11
+ switch self {
12
+ case let .unknownStatus(status):
13
+ return "Purchased not successful due to unknown status - \(status)."
14
+ case let .purchaseFailed(errorMsg):
15
+ return errorMsg
16
+ }
17
+ }
18
+ }
19
+
20
+ public class HeliumPaywallSdkModule: Module {
21
+ // Single continuations for ongoing operations
22
+ private var currentProductId: String? = nil
23
+ private var purchaseContinuation: CheckedContinuation<HeliumPaywallTransactionStatus, Never>? = nil
24
+ private var restoreContinuation: CheckedContinuation<Bool, Never>? = nil
25
+
26
+ // Each module class must implement the definition function. The definition consists of components
27
+ // that describes the module's functionality and behavior.
28
+ // See https://docs.expo.dev/modules/module-api for more details about available components.
29
+ public func definition() -> ModuleDefinition {
30
+ // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
31
+ // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
32
+ // The module will be accessible from `requireNativeModule('HeliumPaywallSdk')` in JavaScript.
33
+ Name("HeliumPaywallSdk")
34
+
35
+ // Sets constant properties on the module. Can take a dictionary or a closure that returns a dictionary.
36
+ // Constants([
37
+ // "PI": Double.pi
38
+ // ])
39
+
40
+ // Defines event names that the module can send to JavaScript.
41
+ Events("onHeliumPaywallEvent")
42
+
43
+ Events("onDelegateActionEvent")
44
+
45
+ // todo use Record here? https://docs.expo.dev/modules/module-api/#records
46
+ Function("initialize") { (config: [String : Any]) in
47
+ let userTraitsMap = config["customUserTraits"] as? [String : Any]
48
+
49
+ // Create delegate with closures that send events to JavaScript
50
+ let delegate = InternalDelegate(
51
+ eventHandler: { [weak self] event in
52
+ self?.sendEvent("onHeliumPaywallEvent", event.toDictionary())
53
+ },
54
+ purchaseHandler: { [weak self] productId in
55
+ guard let self else { return .failed(PurchaseError.purchaseFailed(errorMsg: "Module not active!")) }
56
+ // Check if there's already a purchase in progress and cancel it
57
+ if let existingContinuation = self.purchaseContinuation {
58
+ existingContinuation.resume(returning: .cancelled)
59
+ self.purchaseContinuation = nil
60
+ self.currentProductId = nil
61
+ }
62
+
63
+ return await withCheckedContinuation { continuation in
64
+ // Store the continuation and product ID
65
+ self.currentProductId = productId
66
+ self.purchaseContinuation = continuation
67
+
68
+ // Send event to JavaScript
69
+ self.sendEvent("onDelegateActionEvent", [
70
+ "type": "purchase",
71
+ "productId": productId
72
+ ])
73
+ }
74
+ },
75
+ restoreHandler: { [weak self] in
76
+ guard let self else { return false }
77
+ // Check if there's already a restore in progress and cancel it
78
+ if let existingContinuation = self.restoreContinuation {
79
+ existingContinuation.resume(returning: false)
80
+ self.restoreContinuation = nil
81
+ }
82
+
83
+ return await withCheckedContinuation { continuation in
84
+ // Store the continuation
85
+ self.restoreContinuation = continuation
86
+
87
+ // Send event to JavaScript
88
+ self.sendEvent("onDelegateActionEvent", [
89
+ "type": "restore"
90
+ ])
91
+ }
92
+ }
93
+ )
94
+
95
+ Helium.shared.initialize(
96
+ apiKey: config["apiKey"] as? String ?? "",
97
+ heliumPaywallDelegate: delegate,
98
+ fallbackPaywall: FallbackView(),
99
+ customUserId: config["customUserId"] as? String,
100
+ customAPIEndpoint: config["customAPIEndpoint"] as? String,
101
+ customUserTraits: userTraitsMap != nil ? HeliumUserTraits(userTraitsMap!) : nil,
102
+ revenueCatAppUserId: config["revenueCatAppUserId"] as? String
103
+ )
104
+ }
105
+
106
+ // Function for JavaScript to provide purchase result
107
+ Function("handlePurchaseResult") { [weak self] (statusString: String, errorMsg: String?) in
108
+ guard let continuation = self?.purchaseContinuation else {
109
+ return
110
+ }
111
+
112
+ // Parse status string
113
+ let lowercasedStatus = statusString.lowercased()
114
+ let status: HeliumPaywallTransactionStatus
115
+
116
+ switch lowercasedStatus {
117
+ case "purchased": status = .purchased
118
+ case "cancelled": status = .cancelled
119
+ case "restored": status = .restored
120
+ case "pending": status = .pending
121
+ case "failed": status = .failed(PurchaseError.purchaseFailed(errorMsg: errorMsg ?? "Unexpected error."))
122
+ default: status = .failed(PurchaseError.unknownStatus(status: lowercasedStatus))
123
+ }
124
+
125
+ // Clear the references
126
+ self?.purchaseContinuation = nil
127
+ self?.currentProductId = nil
128
+
129
+ // Resume the continuation with the status
130
+ continuation.resume(returning: status)
131
+ }
132
+
133
+ // Function for JavaScript to provide restore result
134
+ Function("handleRestoreResult") { [weak self] (success: Bool) in
135
+ guard let continuation = self?.restoreContinuation else {
136
+ return
137
+ }
138
+
139
+ self?.restoreContinuation = nil
140
+ continuation.resume(returning: success)
141
+ }
142
+
143
+ Function("presentUpsell") { (trigger: String) in
144
+ Helium.shared.presentUpsell(trigger: trigger);
145
+ }
146
+
147
+ Function("hideUpsell") {
148
+ let _ = Helium.shared.hideUpsell();
149
+ }
150
+
151
+ Function("hideAllUpsells") {
152
+ Helium.shared.hideAllUpsells();
153
+ }
154
+
155
+ Function("getDownloadStatus") {
156
+ return Helium.shared.getDownloadStatus().rawValue;
157
+ }
158
+
159
+ Function("fallbackOpenOrCloseEvent") { (trigger: String?, isOpen: Bool, viewType: String?) in
160
+ HeliumPaywallDelegateWrapper.shared.onFallbackOpenCloseEvent(trigger: trigger, isOpen: isOpen, viewType: viewType)
161
+ }
162
+
163
+ // Defines a JavaScript function that always returns a Promise and whose native code
164
+ // is by default dispatched on the different thread than the JavaScript runtime runs on.
165
+ // AsyncFunction("setValueAsync") { (value: String) in
166
+ // // Send an event to JavaScript.
167
+ // self.sendEvent("onHeliumPaywallEvent", [
168
+ // "value": value
169
+ // ])
170
+ // }
171
+
172
+ // Enables the module to be used as a native view. Definition components that are accepted as part of the
173
+ // view definition: Prop, Events.
174
+ View(HeliumPaywallSdkView.self) {
175
+ // Defines a setter for the `url` prop.
176
+ Prop("url") { (view: HeliumPaywallSdkView, url: URL) in
177
+ if view.webView.url != url {
178
+ view.webView.load(URLRequest(url: url))
179
+ }
180
+ }
181
+
182
+ Events("onLoad")
183
+ }
184
+ }
185
+ }
186
+
187
+ fileprivate class InternalDelegate: HeliumPaywallDelegate {
188
+ private let eventHandler: (HeliumPaywallEvent) -> Void
189
+ private let purchaseHandler: (String) async -> HeliumPaywallTransactionStatus
190
+ private let restoreHandler: () async -> Bool
191
+
192
+ init(
193
+ eventHandler: @escaping (HeliumPaywallEvent) -> Void,
194
+ purchaseHandler: @escaping (String) async -> HeliumPaywallTransactionStatus,
195
+ restoreHandler: @escaping () async -> Bool
196
+ ) {
197
+ self.eventHandler = eventHandler
198
+ self.purchaseHandler = purchaseHandler
199
+ self.restoreHandler = restoreHandler
200
+ }
201
+
202
+ public func makePurchase(productId: String) async -> HeliumPaywallTransactionStatus {
203
+ return await purchaseHandler(productId)
204
+ }
205
+
206
+ public func restorePurchases() async -> Bool {
207
+ return await restoreHandler()
208
+ }
209
+
210
+ public func onHeliumPaywallEvent(event: HeliumPaywallEvent) {
211
+ eventHandler(event)
212
+ }
213
+ }
214
+
215
+ fileprivate struct FallbackView: View {
216
+ @Environment(\.presentationMode) var presentationMode
217
+
218
+ var body: some View {
219
+ VStack(spacing: 20) {
220
+ Spacer()
221
+
222
+ Text("Fallback Paywall")
223
+ .font(.title)
224
+ .fontWeight(.bold)
225
+
226
+ Text("Something went wrong loading the paywall")
227
+ .font(.body)
228
+ .multilineTextAlignment(.center)
229
+ .foregroundColor(.secondary)
230
+
231
+ Spacer()
232
+
233
+ Button(action: {
234
+ presentationMode.wrappedValue.dismiss()
235
+ }) {
236
+ Text("Close")
237
+ .font(.headline)
238
+ .foregroundColor(.white)
239
+ .frame(maxWidth: .infinity)
240
+ .padding()
241
+ .background(Color.blue)
242
+ .cornerRadius(10)
243
+ }
244
+ .padding(.horizontal, 40)
245
+ .padding(.bottom, 40)
246
+ }
247
+ .padding()
248
+ }
249
+ }
@@ -0,0 +1,38 @@
1
+ import ExpoModulesCore
2
+ import WebKit
3
+
4
+ // This view will be used as a native component. Make sure to inherit from `ExpoView`
5
+ // to apply the proper styling (e.g. border radius and shadows).
6
+ class HeliumPaywallSdkView: ExpoView {
7
+ let webView = WKWebView()
8
+ let onLoad = EventDispatcher()
9
+ var delegate: WebViewDelegate?
10
+
11
+ required init(appContext: AppContext? = nil) {
12
+ super.init(appContext: appContext)
13
+ clipsToBounds = true
14
+ delegate = WebViewDelegate { url in
15
+ self.onLoad(["url": url])
16
+ }
17
+ webView.navigationDelegate = delegate
18
+ addSubview(webView)
19
+ }
20
+
21
+ override func layoutSubviews() {
22
+ webView.frame = bounds
23
+ }
24
+ }
25
+
26
+ class WebViewDelegate: NSObject, WKNavigationDelegate {
27
+ let onUrlChange: (String) -> Void
28
+
29
+ init(onUrlChange: @escaping (String) -> Void) {
30
+ self.onUrlChange = onUrlChange
31
+ }
32
+
33
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation) {
34
+ if let url = webView.url {
35
+ onUrlChange(url.absoluteString)
36
+ }
37
+ }
38
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "expo-helium",
3
+ "version": "0.8.0",
4
+ "description": "Helium paywalls expo sdk",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "expo-module build",
9
+ "clean": "expo-module clean",
10
+ "lint": "expo-module lint",
11
+ "test": "expo-module test",
12
+ "prepare": "expo-module prepare",
13
+ "prepublishOnly": "expo-module prepublishOnly",
14
+ "expo-module": "expo-module",
15
+ "open:ios": "xed example/ios",
16
+ "open:android": "open -a \"Android Studio\" example/android"
17
+ },
18
+ "keywords": [
19
+ "react-native",
20
+ "expo",
21
+ "expo-helium",
22
+ "helium-expo-sdk",
23
+ "expo-helium-sdk",
24
+ "HeliumPaywallSdk"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/cloudcaptainai/helium-expo-sdk.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/cloudcaptainai/helium-expo-sdk/issues"
32
+ },
33
+ "author": "Anish Doshi <anish@tryhelium.com> (https://tryhelium.com)",
34
+ "license": "MIT",
35
+ "homepage": "https://github.com/cloudcaptainai/helium-expo-sdk/#readme",
36
+ "dependencies": {},
37
+ "devDependencies": {
38
+ "@types/react": "~19.0.0",
39
+ "expo-module-scripts": "^4.1.9",
40
+ "expo": "~53.0.0",
41
+ "react-native": "0.79.1",
42
+ "react-native-purchases": ">=5.1.0"
43
+ },
44
+ "peerDependencies": {
45
+ "expo": "*",
46
+ "react": "*",
47
+ "react-native": "*",
48
+ "react-native-purchases": ">=5.1.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "react-native-purchases": {
52
+ "optional": true
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,95 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+
3
+ export type OnLoadEventPayload = {
4
+ url: string;
5
+ };
6
+
7
+ export type HeliumPaywallSdkModuleEvents = {
8
+ onHeliumPaywallEvent: (params: HeliumPaywallEvent) => void;
9
+ onDelegateActionEvent: (params: DelegateActionEvent) => void;
10
+ };
11
+ export type HeliumPaywallEvent = {
12
+ type: string;
13
+ triggerName?: string;
14
+ paywallTemplateName?: string;
15
+ productKey?: string;
16
+ ctaName?: string;
17
+ configId?: string;
18
+ numAttempts?: number;
19
+ downloadTimeTakenMS?: number;
20
+ webviewRenderTimeTakenMS?: number;
21
+ imagesDownloadTimeTakenMS?: number;
22
+ fontsDownloadTimeTakenMS?: number;
23
+ bundleDownloadTimeMS?: number;
24
+ dismissAll?: boolean;
25
+ errorDescription?: string;
26
+ };
27
+ export type DelegateActionEvent = {
28
+ type: 'purchase' | 'restore';
29
+ productId?: string;
30
+ };
31
+
32
+ export type HeliumPaywallSdkViewProps = {
33
+ url: string;
34
+ onLoad: (event: { nativeEvent: OnLoadEventPayload }) => void;
35
+ style?: StyleProp<ViewStyle>;
36
+ };
37
+
38
+ export type HeliumTransactionStatus = 'purchased' | 'failed' | 'cancelled' | 'pending' | 'restored';
39
+ export type HeliumPurchaseResult = {
40
+ status: HeliumTransactionStatus;
41
+ error?: string; // Optional error message
42
+ };
43
+ export type HeliumDownloadStatus = 'downloadSuccess' | 'downloadFailure' | 'inProgress' | 'notDownloadedYet';
44
+
45
+ // --- Purchase Configuration Types ---
46
+
47
+ /** Interface for providing custom purchase handling logic. */
48
+
49
+ export interface HeliumPurchaseConfig {
50
+ makePurchase: (productId: string) => Promise<HeliumPurchaseResult>;
51
+ restorePurchases: () => Promise<boolean>;
52
+
53
+ /** Optional RevenueCat API Key. If not provided, RevenueCat must be configured elsewhere. */
54
+ apiKey?: string;
55
+ }
56
+
57
+ // Helper function for creating Custom Purchase Config
58
+ export function createCustomPurchaseConfig(callbacks: {
59
+ makePurchase: (productId: string) => Promise<HeliumPurchaseResult>;
60
+ restorePurchases: () => Promise<boolean>;
61
+ }): HeliumPurchaseConfig {
62
+ return {
63
+ makePurchase: callbacks.makePurchase,
64
+ restorePurchases: callbacks.restorePurchases,
65
+ };
66
+ }
67
+
68
+ export interface HeliumConfig {
69
+ /** Your Helium API Key */
70
+ apiKey: string;
71
+ /** Configuration for handling purchases. Can be custom functions or a pre-built handler config. */
72
+ purchaseConfig: HeliumPurchaseConfig;
73
+ /** Callback for receiving all Helium paywall events. */
74
+ onHeliumPaywallEvent: (event: HeliumPaywallEvent) => void; // Still mandatory
75
+
76
+ // Optional configurations
77
+ triggers?: string[];
78
+ customUserId?: string;
79
+ customAPIEndpoint?: string;
80
+ customUserTraits?: Record<string, any>;
81
+ revenueCatAppUserId?: string;
82
+ }
83
+
84
+ export interface NativeHeliumConfig {
85
+ apiKey: string;
86
+ customUserId?: string;
87
+ customAPIEndpoint?: string;
88
+ customUserTraits?: Record<string, any>;
89
+ revenueCatAppUserId?: string;
90
+ }
91
+
92
+ export const HELIUM_CTA_NAMES = {
93
+ SCHEDULE_CALL: 'schedule_call',
94
+ SUBSCRIBE_BUTTON: 'subscribe_button',
95
+ }
@@ -0,0 +1,36 @@
1
+ import { NativeModule, requireNativeModule } from "expo";
2
+
3
+ import {
4
+ HeliumDownloadStatus,
5
+ HeliumPaywallSdkModuleEvents,
6
+ HeliumTransactionStatus,
7
+ NativeHeliumConfig,
8
+ } from "./HeliumPaywallSdk.types";
9
+
10
+ declare class HeliumPaywallSdkModule extends NativeModule<HeliumPaywallSdkModuleEvents> {
11
+ initialize(config: NativeHeliumConfig): void;
12
+
13
+ presentUpsell(triggerName: string): void;
14
+
15
+ hideUpsell(): void;
16
+
17
+ hideAllUpsells(): void;
18
+
19
+ getDownloadStatus(): HeliumDownloadStatus;
20
+
21
+ fallbackOpenOrCloseEvent(
22
+ trigger: string,
23
+ isOpen: boolean,
24
+ viewType: string,
25
+ ): void;
26
+
27
+ handlePurchaseResult(
28
+ statusString: HeliumTransactionStatus,
29
+ errorMsg?: string,
30
+ ): void;
31
+
32
+ handleRestoreResult(success: boolean): void;
33
+ }
34
+
35
+ // This call loads the native module object from the JSI.
36
+ export default requireNativeModule<HeliumPaywallSdkModule>("HeliumPaywallSdk");
@@ -0,0 +1,11 @@
1
+ import { requireNativeView } from 'expo';
2
+ import * as React from 'react';
3
+
4
+ import { HeliumPaywallSdkViewProps } from './HeliumPaywallSdk.types';
5
+
6
+ const NativeView: React.ComponentType<HeliumPaywallSdkViewProps> =
7
+ requireNativeView('HeliumPaywallSdk');
8
+
9
+ export default function HeliumPaywallSdkView(props: HeliumPaywallSdkViewProps) {
10
+ return <NativeView {...props} />;
11
+ }