@tryheliumai/paywall-sdk-react-native 0.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.
Files changed (54) hide show
  1. package/LICENSE +20 -0
  2. package/PaywallSdkReactNative.podspec +28 -0
  3. package/README.md +224 -0
  4. package/android/build.gradle +88 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/AndroidManifestNew.xml +2 -0
  8. package/android/src/main/java/com/paywallsdkreactnative/PaywallSdkReactNativeModule.kt +25 -0
  9. package/android/src/main/java/com/paywallsdkreactnative/PaywallSdkReactNativePackage.kt +17 -0
  10. package/ios/HeliumSwiftInterface.swift +269 -0
  11. package/ios/HeliumUpsellViewManager.m +17 -0
  12. package/ios/HeliumUpsellViewManager.swift +95 -0
  13. package/ios/PaywallSdkReactNative-Bridging-Header.h +2 -0
  14. package/ios/RCTHeliumBridge.m +35 -0
  15. package/lib/commonjs/index.js +37 -0
  16. package/lib/commonjs/index.js.map +1 -0
  17. package/lib/commonjs/native-interface.js +128 -0
  18. package/lib/commonjs/native-interface.js.map +1 -0
  19. package/lib/commonjs/purchase-handlers.js +47 -0
  20. package/lib/commonjs/purchase-handlers.js.map +1 -0
  21. package/lib/commonjs/types.js +2 -0
  22. package/lib/commonjs/types.js.map +1 -0
  23. package/lib/module/index.js +4 -0
  24. package/lib/module/index.js.map +2 -0
  25. package/lib/module/native-interface.js +117 -0
  26. package/lib/module/native-interface.js.map +1 -0
  27. package/lib/module/purchase-handlers.js +42 -0
  28. package/lib/module/purchase-handlers.js.map +1 -0
  29. package/lib/module/types.js +2 -0
  30. package/lib/module/types.js.map +1 -0
  31. package/lib/typescript/commonjs/package.json +1 -0
  32. package/lib/typescript/commonjs/src/index.d.ts +3 -0
  33. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  34. package/lib/typescript/commonjs/src/native-interface.d.ts +13 -0
  35. package/lib/typescript/commonjs/src/native-interface.d.ts.map +1 -0
  36. package/lib/typescript/commonjs/src/purchase-handlers.d.ts +19 -0
  37. package/lib/typescript/commonjs/src/purchase-handlers.d.ts.map +1 -0
  38. package/lib/typescript/commonjs/src/types.d.ts +19 -0
  39. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  40. package/lib/typescript/module/package.json +1 -0
  41. package/lib/typescript/module/src/index.d.ts +3 -0
  42. package/lib/typescript/module/src/index.d.ts.map +1 -0
  43. package/lib/typescript/module/src/native-interface.d.ts +13 -0
  44. package/lib/typescript/module/src/native-interface.d.ts.map +1 -0
  45. package/lib/typescript/module/src/purchase-handlers.d.ts +19 -0
  46. package/lib/typescript/module/src/purchase-handlers.d.ts.map +1 -0
  47. package/lib/typescript/module/src/types.d.ts +19 -0
  48. package/lib/typescript/module/src/types.d.ts.map +1 -0
  49. package/package.json +188 -0
  50. package/src/index.ts +2 -0
  51. package/src/index.tsx +2 -0
  52. package/src/native-interface.tsx +134 -0
  53. package/src/purchase-handlers.ts +39 -0
  54. package/src/types.ts +21 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anish Doshi
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+ folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
5
+
6
+ Pod::Spec.new do |s|
7
+ s.name = "PaywallSdkReactNative"
8
+ s.version = package["version"]
9
+ s.summary = package["description"]
10
+ s.homepage = package["homepage"]
11
+ s.license = package["license"]
12
+ s.authors = package["author"]
13
+
14
+ s.platforms = { :ios => "14.0" }
15
+ s.source = { :git => "https://github.com/cloudcaptainai/helium-react-native-sdk.git", :tag => "#{s.version}" }
16
+
17
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
18
+
19
+ s.dependency 'Helium', '~> 1.5.6'
20
+
21
+ # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
22
+ # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
23
+ if respond_to?(:install_modules_dependencies, true)
24
+ install_modules_dependencies(s)
25
+ else
26
+ s.dependency "React-Core"
27
+ end
28
+ end
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ ---
2
+ title: 'SDK Quickstart (React Native)'
3
+ description: 'Integrate Helium into your React Native App'
4
+ icon: 'code'
5
+ ---
6
+
7
+ ## **Background**
8
+
9
+ Get set up with the Helium SDK for iOS in 5 minutes. Reach out over your Helium slack channel, or email founders@tryhelium.com for any questions.
10
+ ## **Installation**
11
+
12
+ Install **@tryheliumai/paywall-sdk-react-native** using your preferred package manager:
13
+
14
+ ```bash
15
+ npm install @tryheliumai/paywall-sdk-react-native
16
+ # or
17
+ yarn add @tryheliumai/paywall-sdk-react-native
18
+ ```
19
+
20
+ The package should be autolinked. If not, you can manually link it by running:
21
+
22
+ ```bash
23
+ cd ios
24
+ npx pod-install
25
+ cd ..
26
+ npx react-native link @tryheliumai/paywall-sdk-react-native
27
+ ```
28
+
29
+ ## **Configuration**
30
+
31
+ ### Provider Setup
32
+
33
+ Wrap your app's root component with `HeliumProvider`. This provider makes Helium's functionality available throughout your app:
34
+
35
+ ```tsx
36
+ import { HeliumProvider } from '@tryheliumai/paywall-sdk-react-native';
37
+
38
+ function App() {
39
+ return (
40
+ <HeliumProvider
41
+ apiKey="<your-helium-api-key>"
42
+ fallbackComponent={YourFallbackComponent}
43
+ >
44
+ <YourAppComponent />
45
+ </HeliumProvider>
46
+ );
47
+ }
48
+ ```
49
+
50
+ ### Initialization
51
+
52
+ Initialize Helium by calling `Helium.initialize()` early in your app's lifecycle, typically in your root component:
53
+
54
+ ```tsx
55
+ import { initializeHelium } from '@tryheliumai/paywall-sdk-react-native';
56
+
57
+ function App() {
58
+ useEffect(() => {
59
+ initializeHelium({
60
+ // Helium provided api key
61
+ apiKey: '<your-helium-api-key>',
62
+
63
+ // Purchase handlers: described in next section
64
+ delegate: yourDelegate
65
+
66
+ // Custom user id - e.g. your amplitude analytics user id.
67
+ customUserId: '<your-custom-user-id>',
68
+
69
+ // Helium provided custom API endpoint
70
+ customApiEndpoint: '<your-custom-api-endpoint>',
71
+
72
+ // Custom user traits
73
+ customUserTraits: {
74
+ "example_trait": "example_value",
75
+ },
76
+ });
77
+ }, []);
78
+ }
79
+ ```
80
+
81
+ ### Payment Delegate Implementation
82
+
83
+ Create a payment delegate object that implements the `HeliumPaymentDelegate` interface. This delegate handles purchase logic for your paywalls:
84
+
85
+ ```typescript
86
+ export type HeliumTransactionStatus =
87
+ | { type: 'purchased' }
88
+ | { type: 'cancelled' }
89
+ | { type: 'abandoned' }
90
+ | { type: 'failed', error: Error }
91
+ | { type: 'restored' }
92
+ | { type: 'pending' };
93
+
94
+ export interface HeliumPaymentDelegate {
95
+ // [REQUIRED] Trigger the purchase of a product
96
+ makePurchase: (productId: string) => Promise<HeliumTransactionStatus>;
97
+
98
+ // [OPTIONAL] Restore existing subscriptions
99
+ restorePurchases?: () => Promise<boolean>;
100
+
101
+ // [OPTIONAL] Handle Helium paywall events
102
+ onPaywallEvent?: (event: HeliumPaywallEvent) => void;
103
+
104
+ // [OPTIONAL] Provides custom variables for dynamic paywall content
105
+ getCustomVariables?: () => Record<string, any>;
106
+ }
107
+ ```
108
+
109
+ ### Example Delegate
110
+
111
+ Here's an example delegate using React Native's in-app purchases:
112
+
113
+ ```typescript
114
+ import * as RNIap from 'react-native-iap';
115
+
116
+ const paymentDelegate: HeliumPaymentDelegate = {
117
+ async makePurchase(productId: string): Promise<HeliumTransactionStatus> {
118
+ try {
119
+ const products = await RNIap.getProducts([productId]);
120
+ const purchase = await RNIap.requestPurchase(productId);
121
+
122
+ if (purchase) {
123
+ return { type: 'purchased' };
124
+ }
125
+ return { type: 'cancelled' };
126
+ } catch (error) {
127
+ return { type: 'failed', error };
128
+ }
129
+ },
130
+
131
+ async restorePurchases(): Promise<boolean> {
132
+ try {
133
+ await RNIap.initConnection();
134
+ const restored = await RNIap.restorePurchases();
135
+ return restored.length > 0;
136
+ } catch (error) {
137
+ console.error('Restore failed:', error);
138
+ return false;
139
+ }
140
+ },
141
+
142
+ getCustomVariables() {
143
+ return {
144
+ userSubscriptionStatus: checkUserSubscriptionStatus(),
145
+ userIntent: checkUserIntent(),
146
+ };
147
+ }
148
+ };
149
+ ```
150
+
151
+ ### Checking Download Status
152
+
153
+ Monitor the status of paywall configuration downloads using the `useHeliumStatus` hook:
154
+
155
+ ```typescript
156
+ import { useHeliumStatus } from '@tryhelium/paywall-sdk';
157
+
158
+ function YourComponent() {
159
+ const status = useHeliumStatus();
160
+
161
+ useEffect(() => {
162
+ switch (status.type) {
163
+ case 'not_downloaded':
164
+ console.log('Download not started or in progress');
165
+ break;
166
+ case 'success':
167
+ console.log('Download successful with config ID:', status.configId);
168
+ break;
169
+ case 'error':
170
+ console.log('Download failed');
171
+ break;
172
+ }
173
+ }, [status]);
174
+
175
+ return <YourContent />;
176
+ }
177
+ ```
178
+
179
+ ## **Presenting Paywalls**
180
+
181
+ ### Using the Hook
182
+
183
+ Use the `presentPaywall` method in any component under the `HeliumProvider` to present a paywall:
184
+
185
+ ```typescript
186
+ import { presentPaywall } from '@tryhelium/paywall-sdk';
187
+
188
+ function YourComponent() {
189
+ const handlePremiumPress = useCallback(async () => {
190
+ await presentPaywall({ trigger: 'premium_feature_press' });
191
+ }, [presentPaywall]);
192
+
193
+ return (
194
+ <Button title="Try Premium" onPress={handlePremiumPress} />
195
+ );
196
+ }
197
+ ```
198
+
199
+ ### Custom Navigation
200
+
201
+ Handle custom navigation or dismissal by implementing the `onPaywallEvent` method in your payment delegate:
202
+
203
+ ```typescript
204
+ const paymentDelegate: HeliumPaymentDelegate = {
205
+ // ... other methods
206
+
207
+ onPaywallEvent(event: HeliumPaywallEvent) {
208
+ switch (event.type) {
209
+ case 'cta_pressed':
210
+ const { ctaName, trigger, templateName } = event;
211
+ if (ctaName === 'dismiss') {
212
+ // Handle custom dismissal
213
+ }
214
+ break;
215
+ // ... handle other events
216
+ }
217
+ }
218
+ };
219
+ ```
220
+
221
+ ## **Paywall Events**
222
+
223
+ Helium emits various events during the lifecycle of a paywall. You can handle these events in your payment delegate. See the iOS docs
224
+ for more details.
@@ -0,0 +1,88 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = {name ->
3
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['PaywallSdkReactNative_' + name]
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ // noinspection DifferentKotlinGradleVersion
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
15
+ }
16
+ }
17
+
18
+
19
+ def isNewArchitectureEnabled() {
20
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
21
+ }
22
+
23
+ apply plugin: "com.android.library"
24
+ apply plugin: "kotlin-android"
25
+
26
+ if (isNewArchitectureEnabled()) {
27
+ apply plugin: "com.facebook.react"
28
+ }
29
+
30
+ def getExtOrIntegerDefault(name) {
31
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["PaywallSdkReactNative_" + name]).toInteger()
32
+ }
33
+
34
+ def supportsNamespace() {
35
+ def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
36
+ def major = parsed[0].toInteger()
37
+ def minor = parsed[1].toInteger()
38
+
39
+ // Namespace support was added in 7.3.0
40
+ return (major == 7 && minor >= 3) || major >= 8
41
+ }
42
+
43
+ android {
44
+ if (supportsNamespace()) {
45
+ namespace "com.paywallsdkreactnative"
46
+
47
+ sourceSets {
48
+ main {
49
+ manifest.srcFile "src/main/AndroidManifestNew.xml"
50
+ }
51
+ }
52
+ }
53
+
54
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
55
+
56
+ defaultConfig {
57
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
58
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
59
+ }
60
+
61
+ buildTypes {
62
+ release {
63
+ minifyEnabled false
64
+ }
65
+ }
66
+
67
+ lintOptions {
68
+ disable "GradleCompatible"
69
+ }
70
+
71
+ compileOptions {
72
+ sourceCompatibility JavaVersion.VERSION_1_8
73
+ targetCompatibility JavaVersion.VERSION_1_8
74
+ }
75
+ }
76
+
77
+ repositories {
78
+ mavenCentral()
79
+ google()
80
+ }
81
+
82
+ def kotlin_version = getExtOrDefault("kotlinVersion")
83
+
84
+ dependencies {
85
+ implementation "com.facebook.react:react-android"
86
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
87
+ }
88
+
@@ -0,0 +1,5 @@
1
+ PaywallSdkReactNative_kotlinVersion=2.0.21
2
+ PaywallSdkReactNative_minSdkVersion=24
3
+ PaywallSdkReactNative_targetSdkVersion=34
4
+ PaywallSdkReactNative_compileSdkVersion=35
5
+ PaywallSdkReactNative_ndkVersion=27.1.12297006
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.paywallsdkreactnative">
3
+ </manifest>
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,25 @@
1
+ package com.paywallsdkreactnative
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
5
+ import com.facebook.react.bridge.ReactMethod
6
+ import com.facebook.react.bridge.Promise
7
+
8
+ class PaywallSdkReactNativeModule(reactContext: ReactApplicationContext) :
9
+ ReactContextBaseJavaModule(reactContext) {
10
+
11
+ override fun getName(): String {
12
+ return NAME
13
+ }
14
+
15
+ // Example method
16
+ // See https://reactnative.dev/docs/native-modules-android
17
+ @ReactMethod
18
+ fun multiply(a: Double, b: Double, promise: Promise) {
19
+ promise.resolve(a * b)
20
+ }
21
+
22
+ companion object {
23
+ const val NAME = "PaywallSdkReactNative"
24
+ }
25
+ }
@@ -0,0 +1,17 @@
1
+ package com.paywallsdkreactnative
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+
9
+ class PaywallSdkReactNativePackage : ReactPackage {
10
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
11
+ return listOf(PaywallSdkReactNativeModule(reactContext))
12
+ }
13
+
14
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
15
+ return emptyList()
16
+ }
17
+ }
@@ -0,0 +1,269 @@
1
+ //
2
+ // HeliumSwiftInterface.swift
3
+ // HeliumBridgeNative
4
+ //
5
+ // Created by Anish Doshi on 2/11/25.
6
+ //
7
+
8
+ import Foundation
9
+ import Helium
10
+ import SwiftUI
11
+ import UIKit
12
+ import React
13
+ import AnyCodable
14
+ import Combine
15
+
16
+ struct UIViewWrapper: UIViewRepresentable, View {
17
+ let view: UIView
18
+
19
+ func makeUIView(context: Context) -> UIView {
20
+ return view
21
+ }
22
+
23
+ func updateUIView(_ uiView: UIView, context: Context) {
24
+ }
25
+ }
26
+
27
+ class PurchaseState: ObservableObject {
28
+ struct PurchaseResponse {
29
+ let transactionId: String
30
+ let status: String
31
+ }
32
+
33
+ @Published var pendingResponses: [String: (PurchaseResponse) -> Void] = [:]
34
+ }
35
+
36
+
37
+ class BridgingPaywallDelegate: HeliumPaywallDelegate {
38
+ private let purchaseState = PurchaseState()
39
+ private weak var bridge: HeliumBridge?
40
+
41
+ init(
42
+ bridge: HeliumBridge
43
+ ) {
44
+ self.bridge = bridge
45
+ }
46
+
47
+ public func makePurchase(productId: String) async -> HeliumPaywallTransactionStatus {
48
+ return await withCheckedContinuation { continuation in
49
+ let transactionId = UUID().uuidString
50
+
51
+ // Store continuation callback
52
+ purchaseState.pendingResponses[transactionId] = { response in
53
+ let status: HeliumPaywallTransactionStatus = switch response.status {
54
+ case "purchased": .purchased
55
+ case "cancelled": .cancelled
56
+ case "restored": .restored
57
+ case "pending": .pending
58
+ default: .failed(NSError())
59
+ }
60
+ continuation.resume(returning: status)
61
+ }
62
+
63
+ // Send event to initiate purchase
64
+ bridge?.sendEvent(
65
+ withName: "helium_make_purchase",
66
+ body: [
67
+ "productId": productId,
68
+ "transactionId": transactionId,
69
+ "status": "starting"
70
+ ]
71
+ )
72
+ }
73
+ }
74
+
75
+ func handlePurchaseResponse(_ response: NSDictionary) {
76
+ guard let transactionId = response["transactionId"] as? String,
77
+ let status = response["status"] as? String,
78
+ let callback = purchaseState.pendingResponses[transactionId] else {
79
+ return
80
+ }
81
+
82
+ // Remove callback before executing to prevent multiple calls
83
+ purchaseState.pendingResponses.removeValue(forKey: transactionId)
84
+
85
+ callback(PurchaseState.PurchaseResponse(
86
+ transactionId: transactionId,
87
+ status: status
88
+ ))
89
+ }
90
+
91
+
92
+ func restorePurchases() async -> Bool {
93
+ return await withCheckedContinuation { continuation in
94
+ let transactionId = UUID().uuidString
95
+
96
+ // Store continuation callback
97
+ purchaseState.pendingResponses[transactionId] = { response in
98
+ // Convert string status to bool
99
+ let success = response.status == "restored"
100
+ continuation.resume(returning: success)
101
+ }
102
+
103
+ // Send event to initiate restore
104
+ bridge?.sendEvent(
105
+ withName: "helium_restore_purchases",
106
+ body: [
107
+ "transactionId": transactionId,
108
+ "status": "starting"
109
+ ]
110
+ )
111
+ }
112
+ }
113
+
114
+ func handleRestoreResponse(_ response: NSDictionary) {
115
+ guard let transactionId = response["transactionId"] as? String,
116
+ let status = response["status"] as? String,
117
+ let callback = purchaseState.pendingResponses[transactionId] else {
118
+ return
119
+ }
120
+
121
+ // Remove callback before executing to prevent multiple calls
122
+ purchaseState.pendingResponses.removeValue(forKey: transactionId)
123
+
124
+ callback(PurchaseState.PurchaseResponse(
125
+ transactionId: transactionId,
126
+ status: status
127
+ ))
128
+ }
129
+
130
+ func onHeliumPaywallEvent(event: HeliumPaywallEvent) {
131
+ let eventDict = event.toDictionary()
132
+ bridge?.sendEvent(
133
+ withName: "helium_paywall_event",
134
+ body: eventDict
135
+ )
136
+ }
137
+
138
+ func getCustomVariableValues() -> [String: Any?] {
139
+ return [:];
140
+ }
141
+ }
142
+
143
+ @objc(HeliumBridge)
144
+ class HeliumBridge: RCTEventEmitter {
145
+ private var bridgingDelegate: BridgingPaywallDelegate?
146
+ var customVariables: [String: Any?] = [:]
147
+ private var cancellables = Set<AnyCancellable>()
148
+
149
+ @objc(init)
150
+ public override init() {
151
+ super.init()
152
+ }
153
+
154
+ public override func supportedEvents() -> [String] {
155
+ return [
156
+ "helium_paywall_event",
157
+ "helium_make_purchase",
158
+ "helium_restore_purchases",
159
+ "helium_download_state_changed"
160
+ ]
161
+ }
162
+
163
+ @objc
164
+ override static func requiresMainQueueSetup() -> Bool {
165
+ return true
166
+ }
167
+
168
+ @objc
169
+ public func initialize(
170
+ _ config: NSDictionary,
171
+ customVariableValues:NSDictionary
172
+ ) {
173
+ guard let apiKey = config["apiKey"] as? String,
174
+ let viewTag = config["fallbackPaywall"] as? NSNumber else {
175
+ return
176
+ }
177
+
178
+ let triggers = config["triggers"] as? [String]
179
+ let customUserId = config["customUserId"] as? String
180
+ let customAPIEndpoint = config["customAPIEndpoint"] as? String
181
+ let customUserTraits = config["customUserTraits"] as? [String: AnyCodable];
182
+
183
+ self.bridgingDelegate = BridgingPaywallDelegate(
184
+ bridge: self
185
+ )
186
+
187
+ HeliumFetchedConfigManager.shared.$downloadStatus
188
+ .sink { newValue in
189
+ var newValueString = "";
190
+ switch (newValue) {
191
+ case .downloadFailure:
192
+ newValueString = "downloadFailure";
193
+ break;
194
+ case .downloadSuccess:
195
+ newValueString = "downloadSuccess";
196
+ break;
197
+ case .inProgress:
198
+ newValueString = "inProgress";
199
+ break;
200
+ case .notDownloadedYet:
201
+ newValueString = "notDownloadedYet";
202
+ break;
203
+ }
204
+ self.sendEvent(
205
+ withName: "helium_download_state_changed",
206
+ body: [
207
+ "status": newValueString
208
+ ]
209
+ )
210
+ }
211
+ .store(in: &cancellables)
212
+
213
+ // Always do view lookup on main queue
214
+ DispatchQueue.main.async {
215
+ guard let bridge = self.bridge,
216
+ let fallbackPaywall = bridge.uiManager.view(forReactTag: viewTag) else {
217
+ return
218
+ }
219
+
220
+ let wrappedView = UIViewWrapper(view: fallbackPaywall)
221
+
222
+ // Move initialization off main queue
223
+ DispatchQueue.global().async {
224
+ Helium.shared.initialize(
225
+ apiKey: apiKey,
226
+ heliumPaywallDelegate: self.bridgingDelegate!,
227
+ fallbackPaywall: wrappedView,
228
+ triggers: triggers,
229
+ customUserId: customUserId,
230
+ customAPIEndpoint: customAPIEndpoint,
231
+ customUserTraits: HeliumUserTraits(customUserTraits ?? [:])
232
+ )
233
+ }
234
+ }
235
+ }
236
+
237
+ @objc
238
+ public func handlePurchaseResponse(_ response: NSDictionary) {
239
+ bridgingDelegate?.handlePurchaseResponse(response)
240
+ }
241
+
242
+ @objc
243
+ public func handleRestoreResponse(_ response: NSDictionary) {
244
+ bridgingDelegate?.handleRestoreResponse(response)
245
+ }
246
+
247
+ @objc
248
+ public func upsellViewForTrigger(
249
+ _ trigger: String,
250
+ resolver: @escaping RCTPromiseResolveBlock,
251
+ rejecter: @escaping RCTPromiseRejectBlock
252
+ ) {
253
+ let swiftUIView = Helium.shared.upsellViewForTrigger(trigger: trigger)
254
+ let hostingController = UIHostingController(rootView: swiftUIView)
255
+ resolver(hostingController.view)
256
+ }
257
+
258
+ @objc
259
+ public func presentUpsell(
260
+ _ trigger: String
261
+ ) {
262
+ Helium.shared.presentUpsell(trigger: trigger);
263
+ }
264
+
265
+ @objc
266
+ public func hideUpsell() {
267
+ _ = Helium.shared.hideUpsell();
268
+ }
269
+ }