react-native-hyperswitch-dev-sdk 0.4.2 → 0.4.4

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 (34) hide show
  1. package/HyperswitchSdkReactNative.podspec +4 -0
  2. package/android/build.gradle +0 -6
  3. package/android/hyperswitch_autolinking.gradle +1 -1
  4. package/ios/Modules/ReactNative/HyperModule.h +25 -0
  5. package/ios/Modules/ReactNative/HyperModule.mm +169 -0
  6. package/ios/Modules/ReactNative/HyperswitchModule.swift +223 -165
  7. package/ios/Modules/ReactNative/NativeHyperswitchModule.mm +17 -12
  8. package/ios/Modules/ReactNative/NativeHyperswitchModuleImpl.swift +18 -7
  9. package/ios/Modules/ReactNative/NativePaymentElementModule.mm +122 -122
  10. package/ios/Views/Fabric/NativePaymentElementView.mm +8 -0
  11. package/ios/Views/Native/NativePaymentWidget.swift +26 -22
  12. package/ios/hyperswitchSDK/Core/HyperCVCWidget/CVCWidget.swift +3 -2
  13. package/ios/hyperswitchSDK/Core/HyperPaymentCardTextField/PaymentHandler.swift +1 -1
  14. package/ios/hyperswitchSDK/Core/HyperPaymentSheet/PaymentSheetView+UIKit.swift +79 -13
  15. package/ios/hyperswitchSDK/Core/HyperPaymentSheet/PaymentSheetView.swift +2 -2
  16. package/ios/hyperswitchSDK/Core/HyperPaymentWidget/PaymentWidget.swift +5 -4
  17. package/ios/hyperswitchSDK/Core/HyperSession/PaymentSession+UIKit.swift +14 -0
  18. package/ios/hyperswitchSDK/Core/NativeModule/{HyperModule.swift → HyperModuleImpl.swift} +156 -113
  19. package/ios/hyperswitchSDK/Core/NativeModule/RNHeadlessManager.swift +58 -22
  20. package/ios/hyperswitchSDK/Core/NativeModule/RNViewManager.swift +84 -29
  21. package/ios/hyperswitchSDK/Core/NativeModule/hyper-Bridging-Header.h +2 -0
  22. package/ios/hyperswitchSDK/Public/RNBridgeFactory.h +28 -0
  23. package/ios/hyperswitchSDK/Public/RNBridgeFactory.mm +167 -0
  24. package/ios/hyperswitchSDK/Shared/PaymentSheet.swift +1 -1
  25. package/lib/commonjs/index.bundle.js +1 -1
  26. package/lib/commonjs/index.bundle.js.map +1 -1
  27. package/lib/module/index.bundle.js +1 -1
  28. package/lib/module/index.bundle.js.map +1 -1
  29. package/package.json +6 -2
  30. package/src/context/PaymentSession.ts +2 -0
  31. package/src/specs/NativeHyperswitchModule.ts +2 -2
  32. package/ios/Modules/ReactNative/HyperswitchSdkReactNative.h +0 -5
  33. package/ios/Modules/ReactNative/HyperswitchSdkReactNative.mm +0 -121
  34. package/ios/hyperswitchSDK/Core/NativeModule/HyperModule.m +0 -31
@@ -16,7 +16,7 @@ public class PaymentWidget: UIControl {
16
16
  private var configurationDict: [String: Any]?
17
17
  private var widgetReactTag: NSNumber?
18
18
  internal var rootReactTag: NSNumber? { widgetReactTag }
19
- private var rootView: RCTRootView?
19
+ private var rootView: UIView?
20
20
  private var initCallback: ((PaymentResult) -> Void)?
21
21
  private var shouldProceedWithPaymentCallback: ((PaymentRequestData, @escaping (Bool) -> Void) -> Void)?
22
22
  private var cancellables = Set<AnyCancellable>()
@@ -124,7 +124,7 @@ public class PaymentWidget: UIControl {
124
124
  .sink { [weak self] in
125
125
  guard let self = self else { return }
126
126
  let payload: [String: Any] = ["rootTag": self.widgetReactTag ?? -1]
127
- self.rootView?.bridge.enqueueJSCall(
127
+ RNViewManager.sharedInstance.bridge?.enqueueJSCall(
128
128
  "RCTDeviceEventEmitter",
129
129
  method: "emit",
130
130
  args: ["updateIntentInit", payload],
@@ -141,7 +141,7 @@ public class PaymentWidget: UIControl {
141
141
  "rootTag": self.widgetReactTag ?? -1,
142
142
  "sdkAuthorization": sdkAuthorization,
143
143
  ]
144
- self.rootView?.bridge.enqueueJSCall(
144
+ RNViewManager.sharedInstance.bridge?.enqueueJSCall(
145
145
  "RCTDeviceEventEmitter",
146
146
  method: "emit",
147
147
  args: ["updateIntentComplete", payload],
@@ -156,7 +156,7 @@ public class PaymentWidget: UIControl {
156
156
  "rootTag": self.widgetReactTag ?? -1,
157
157
  "actionType": "CONFIRM_PAYMENT_ACTION",
158
158
  ]
159
- self.rootView?.bridge.enqueueJSCall(
159
+ RNViewManager.sharedInstance.bridge?.enqueueJSCall(
160
160
  "RCTDeviceEventEmitter",
161
161
  method: "emit",
162
162
  args: ["triggerWidgetAction", payload],
@@ -210,3 +210,4 @@ public class PaymentWidget: UIControl {
210
210
  }
211
211
  }
212
212
  }
213
+
@@ -73,6 +73,20 @@ extension PaymentSession {
73
73
  paymentSheet.presentWithParams(from: viewController, props: params, completion: completion)
74
74
  }
75
75
 
76
+ /// React Native / TurboModule overload — resolves with the raw JSON string from the JS
77
+ /// bundle so the result reaches the promise without any re-serialisation.
78
+ internal func presentPaymentSheetWithParams(
79
+ viewController: UIViewController,
80
+ params: [String: Any],
81
+ rawCompletion: @escaping (String) -> Void
82
+ ) {
83
+ let paymentSheet = PaymentSheet(
84
+ paymentSessionConfiguration: paymentSessionConfiguration,
85
+ hyperswitchConfiguration: hyperswitchConfiguration ?? nil
86
+ )
87
+ paymentSheet.presentWithParams(from: viewController, props: params, rawCompletion: rawCompletion)
88
+ }
89
+
76
90
  public func getCustomerSavedPaymentMethods(
77
91
  _ func_: @escaping (PaymentSessionHandler) -> Void,
78
92
  configuration: SavedPaymentMethodsConfiguration? = nil
@@ -1,90 +1,187 @@
1
1
  //
2
- // HyperModule.swift
3
- // Hyperswitch
2
+ // HyperModuleImpl.swift
3
+ // Pods
4
4
  //
5
- // Created by Harshit Srivastava on 07/03/24.
5
+ // Created by Kuntimaddi Manideep on 22/07/26.
6
6
  //
7
7
 
8
8
  import Foundation
9
9
  import React
10
10
 
11
- @objc(HyperModule)
12
- internal class HyperModule: RCTEventEmitter {
11
+ @objc(HyperModuleImpl)
12
+ public class HyperModuleImpl: NSObject {
13
+
14
+ @objc public static let shared = HyperModuleImpl()
15
+
16
+ /// The RN-registered event emitter (the ObjC++ `HyperModule` TurboModule) through
17
+ /// which JS-facing events are dispatched and the bridge / uiManager is reached.
18
+ @objc public weak var eventEmitter: RCTEventEmitter?
13
19
 
14
20
  private let applePayPaymentHandler = ApplePayHandler()
15
21
  private let expressCheckoutHandler = ExpressCheckoutLauncher()
16
22
  private var presentCallback: RCTResponseSenderBlock? = nil
17
- internal static var shared: HyperModule?
18
23
 
19
- override init() {
24
+ /// Direct reference to the currently-presented PaymentSheet.
25
+ /// Set by PaymentSheetView+UIKit when the sheet is presented; cleared in exitPaymentsheet.
26
+ /// Avoids relying on bridge.uiManager.addUIBlock (which doesn't find Fabric views).
27
+ private var activePaymentSheet: PaymentSheet?
28
+ private weak var activePaymentSheetVC: HyperUIViewController?
29
+
30
+ private override init() {
20
31
  super.init()
21
- HyperModule.shared = self
22
32
  }
23
33
 
24
- @objc
25
- internal override static func requiresMainQueueSetup() -> Bool {
26
- return true
27
- }
34
+ // MARK: - Payment sheet registration (called from PaymentSheetView+UIKit)
28
35
 
29
- @objc
30
- internal override func supportedEvents() -> [String] {
31
- return ["confirm", "confirmEC", "triggerWidgetAction", "updateIntentInit", "updateIntentComplete"]
36
+ internal func registerPaymentSheet(_ sheet: PaymentSheet, vc: HyperUIViewController) {
37
+ activePaymentSheet = sheet
38
+ activePaymentSheetVC = vc
32
39
  }
33
40
 
34
- @objc
35
- internal func confirm(data: [String: Any]) {
36
- self.sendEvent(withName: "confirm", body: data)
41
+ /// The bridge backing the RN-registered emitter (embedded runtime is bridge-based).
42
+ private var bridge: RCTBridge? { eventEmitter?.bridge }
43
+
44
+ // MARK: - Events
45
+
46
+ @objc public func confirm(data: [String: Any]) {
47
+ eventEmitter?.sendEvent(withName: "confirm", body: data)
37
48
  }
38
49
  // MARK: WIP
39
- // @objc func confirmEC(data: [String: Any]) {
40
- // self.sendEvent(withName: "confirmEC", body: data)
50
+ // @objc public func confirmEC(data: [String: Any]) {
51
+ // eventEmitter?.sendEvent(withName: "confirmEC", body: data)
41
52
  // }
42
53
 
43
- @objc
44
- private func sendMessageToNative(_ rnMessage: String) {}
54
+ // MARK: - Generic message passing
45
55
 
46
- @objc
47
- private func launchWidgetPaymentSheet(_ request: NSMutableDictionary, _ callback: @escaping RCTResponseSenderBlock) {
48
- expressCheckoutHandler.launchPaymentSheet(paymentResult: request, callBack: callback)
56
+ @objc public func sendMessageToNative(_ rnMessage: String) {}
57
+
58
+ // MARK: - Apple Pay
59
+
60
+ @objc public func launchApplePay(_ rnMessage: String, callback: @escaping RCTResponseSenderBlock) {
61
+ applePayPaymentHandler.startPayment(rnMessage: rnMessage, rnCallback: callback, presentCallback: self.presentCallback)
49
62
  }
50
63
 
51
- @objc
52
- private func onAddPaymentMethod(_ rnMessage: String) {
53
- PaymentMethodManagementWidget.onAddPaymentMethod?()
64
+ @objc public func startApplePay(_ rnMessage: String, callback: @escaping RCTResponseSenderBlock) {
65
+ callback([])
54
66
  }
55
67
 
56
- @objc
57
- private func launchApplePay(_ rnMessage: String, _ rnCallback: @escaping RCTResponseSenderBlock) {
58
- applePayPaymentHandler.startPayment(rnMessage: rnMessage, rnCallback: rnCallback, presentCallback: self.presentCallback)
68
+ @objc public func presentApplePay(_ rnMessage: String, callback: @escaping RCTResponseSenderBlock) {
69
+ self.presentCallback = callback
59
70
  }
60
71
 
61
- @objc
62
- private func startApplePay(_ rnMessage: String, _ rnCallback: @escaping RCTResponseSenderBlock) {
63
- rnCallback([])
72
+ // MARK: - Widget
73
+
74
+ @objc public func launchWidgetPaymentSheet(_ requestObj: String, callback: @escaping RCTResponseSenderBlock) {
75
+ let request = HyperModuleImpl.jsonObject(from: requestObj)
76
+ expressCheckoutHandler.launchPaymentSheet(paymentResult: request, callBack: callback)
64
77
  }
65
78
 
66
- @objc
67
- private func presentApplePay(_ rnMessage: String, _ rnCallback: @escaping RCTResponseSenderBlock) {
68
- self.presentCallback = rnCallback
79
+ @objc public func onAddPaymentMethod(_ rnMessage: String) {
80
+ PaymentMethodManagementWidget.onAddPaymentMethod?()
69
81
  }
70
82
 
71
- @objc
72
- private func exitPaymentsheet(_ reactTag: NSNumber, _ rnMessage: String, _ reset: Bool) {
73
- let result = paymentResult(from: rnMessage)
74
- withPaymentSheet(reactTag) { vc, sheet in
75
- sheet?.completion?(result)
76
- vc?.dismiss(animated: false, completion: nil)
83
+ // MARK: - Payment sheet exits
84
+
85
+ @objc public func exitPaymentsheet(_ reactTag: NSNumber, result rnMessage: String, reset: Bool) {
86
+ // Consume and clear so they can't be called a second time.
87
+ let sheet = activePaymentSheet
88
+ let vc = activePaymentSheetVC
89
+ activePaymentSheet = nil
90
+ activePaymentSheetVC = nil
91
+
92
+ // Pass the raw JS result string directly — no PaymentResult conversion.
93
+ // PaymentSheet.completion is (String) -> Void, so this goes straight to
94
+ // the promise resolve in HyperswitchModule.presentPaymentSheet.
95
+ sheet?.completion?(rnMessage)
96
+
97
+ // Dismiss the VC, then release the rootView so the Fabric surface is torn down.
98
+ vc?.dismiss(animated: false) {
99
+ RNViewManager.sharedInstance.rootView?.removeFromSuperview()
100
+ RNViewManager.sharedInstance.rootView = nil
77
101
  }
78
102
  }
79
103
 
80
- @objc
81
- private func exitWidgetPaymentsheet(_ reactTag: NSNumber, _ rnMessage: String, _ reset: Bool) {
104
+ @objc public func exitWidgetPaymentsheet(_ reactTag: NSNumber, result rnMessage: String, reset: Bool) {
82
105
  let result = paymentResult(from: rnMessage)
83
106
  withWidget(reactTag) { w in
84
107
  w.handleConfirmPaymentResponse(result)
85
108
  }
86
109
  }
87
110
 
111
+ @objc public func exitPaymentMethodManagement(_ reactTag: NSNumber, result rnMessage: String, reset: Bool) {
112
+ exitSheet(rnMessage)
113
+ }
114
+
115
+ @objc public func exitCardForm(_ rnMessage: String) {
116
+ exitCardFormInternal(rnMessage)
117
+ }
118
+
119
+ // MARK: - Payment result / events
120
+
121
+ @objc public func notifyWidgetPaymentResult(_ rootTag: NSNumber, result rnMessage: String) {
122
+ let result = paymentResult(from: rnMessage)
123
+ guard case .failed = result else { return }
124
+ withNativePaymentWidgetView(rootTag) { view in
125
+ view.handleConfirmPaymentResponse(result)
126
+ }
127
+ }
128
+
129
+ @objc public func onUpdateIntentEvent(_ rootTag: NSNumber, type: String, result: String) {
130
+ withWidget(rootTag) { widget in
131
+ widget.handleUpdateIntentEvent(type: type, result: result)
132
+ }
133
+ }
134
+
135
+ @objc public func emitPaymentEvent(_ rootTag: NSNumber, eventType: String, payload: NSDictionary) {
136
+ let map = (payload as? [String: Any]) ?? [:]
137
+ resolveSubscribingTarget(rootTag) { target in
138
+ if let widget = target as? PaymentWidget, widget.paymentEventListener != nil {
139
+ widget.dispatchPaymentEvent(type: eventType, payload: map)
140
+ } else if let cvc = target as? CVCWidget, cvc.paymentEventListener != nil {
141
+ cvc.dispatchPaymentEvent(type: eventType, payload: map)
142
+ } else if let sheet = target as? PaymentSheet, sheet.paymentEventListener != nil {
143
+ sheet.dispatchPaymentEvent(type: eventType, payload: map)
144
+ }
145
+ }
146
+ }
147
+
148
+ @objc public func onPaymentConfirmButtonClick(_ rootTag: NSNumber, payload: String, callback: @escaping RCTResponseSenderBlock) {
149
+ resolveSubscribingTarget(rootTag) { target in
150
+ if let widget = target as? PaymentWidget {
151
+ widget.handleShouldProceedWithPayment(payload: payload) { shouldProceed in
152
+ callback([shouldProceed])
153
+ }
154
+ } else if let sheet = target as? PaymentSheet {
155
+ sheet.handleShouldProceedWithPayment(payload: payload) { shouldProceed in
156
+ callback([shouldProceed])
157
+ }
158
+ } else {
159
+ callback([true])
160
+ }
161
+ }
162
+ }
163
+
164
+ // MARK: - 3DS / DDC iframe bridge
165
+
166
+ @objc public func openIframeBridge(_ url: String, timeoutMs: NSNumber, callback: @escaping RCTResponseSenderBlock) {
167
+ DispatchQueue.main.async {
168
+ let headlessWebView = HeadlessWebView(url: url, timeoutMs: timeoutMs, callback: callback)
169
+ headlessWebView.startFlow()
170
+ }
171
+ }
172
+
173
+ // MARK: - Helpers
174
+
175
+ private static func jsonObject(from string: String) -> NSMutableDictionary {
176
+ guard
177
+ let data = string.data(using: .utf8),
178
+ let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [AnyHashable: Any]
179
+ else {
180
+ return NSMutableDictionary()
181
+ }
182
+ return NSMutableDictionary(dictionary: obj)
183
+ }
184
+
88
185
  private func paymentResult(from rnMessage: String) -> PaymentResult {
89
186
  guard let data = rnMessage.data(using: .utf8) else {
90
187
  return .failed(
@@ -132,42 +229,7 @@ internal class HyperModule: RCTEventEmitter {
132
229
  }
133
230
  }
134
231
 
135
- @objc
136
- private func exitPaymentMethodManagement(_ reactTag: NSNumber, _ rnMessage: String, _ reset: Bool) {
137
- exitSheet(rnMessage)
138
- }
139
-
140
- @objc
141
- private func notifyWidgetPaymentResult(_ rootTag: NSNumber, _ rnMessage: String) {
142
- let result = paymentResult(from: rnMessage)
143
- guard case .failed = result else { return }
144
- withNativePaymentWidgetView(rootTag) { view in
145
- view.handleConfirmPaymentNotification(result)
146
- }
147
- }
148
-
149
- @objc
150
- private func onUpdateIntentEvent(_ rootTag: NSNumber, _ type: String, _ result: String) {
151
- withWidget(rootTag) { widget in
152
- widget.handleUpdateIntentEvent(type: type, result: result)
153
- }
154
- }
155
-
156
- @objc func emitPaymentEvent(_ rootTag: NSNumber, _ eventType: String, _ payload: NSDictionary) {
157
- let map = (payload as? [String: Any]) ?? [:]
158
- resolveSubscribingTarget(rootTag) { target in
159
- if let widget = target as? PaymentWidget, widget.paymentEventListener != nil {
160
- widget.dispatchPaymentEvent(type: eventType, payload: map)
161
- } else if let cvc = target as? CVCWidget, cvc.paymentEventListener != nil {
162
- cvc.dispatchPaymentEvent(type: eventType, payload: map)
163
- } else if let sheet = target as? PaymentSheet, sheet.paymentEventListener != nil {
164
- sheet.dispatchPaymentEvent(type: eventType, payload: map)
165
- }
166
- }
167
- }
168
-
169
- @objc
170
- private func exitCardForm(_ rnMessage: String) {
232
+ private func exitCardFormInternal(_ rnMessage: String) {
171
233
  var response: String?
172
234
  var error: NSError?
173
235
 
@@ -206,7 +268,6 @@ internal class HyperModule: RCTEventEmitter {
206
268
  }
207
269
  }
208
270
 
209
- @objc
210
271
  private func exitSheet(_ rnMessage: String) {
211
272
  var response: String?
212
273
  var error: NSError?
@@ -252,34 +313,10 @@ internal class HyperModule: RCTEventEmitter {
252
313
  }
253
314
  }
254
315
 
255
- @objc
256
- private func onPaymentConfirmButtonClick(_ rootTag: NSNumber, _ payload: String, _ callback: @escaping RCTResponseSenderBlock) {
257
- resolveSubscribingTarget(rootTag) { target in
258
- if let widget = target as? PaymentWidget {
259
- widget.handleShouldProceedWithPayment(payload: payload) { shouldProceed in
260
- callback([shouldProceed])
261
- }
262
- } else if let sheet = target as? PaymentSheet {
263
- sheet.handleShouldProceedWithPayment(payload: payload) { shouldProceed in
264
- callback([shouldProceed])
265
- }
266
- } else {
267
- callback([true])
268
- }
269
- }
270
- }
271
-
272
- @objc
273
- private func openIframeBridge(_ url: String, _ timeoutMs: NSNumber, _ callback: @escaping RCTResponseSenderBlock) {
274
- DispatchQueue.main.async {
275
- let headlessWebView = HeadlessWebView(url: url, timeoutMs: timeoutMs, callback: callback)
276
- headlessWebView.startFlow()
277
- }
278
- }
279
-
280
316
  private func withWidget(_ rootTag: NSNumber, _ block: @escaping (PaymentWidget) -> Void) {
317
+ guard let bridge = self.bridge else { return }
281
318
  RCTGetUIManagerQueue().async {
282
- self.bridge.uiManager.addUIBlock { _, viewRegistry in
319
+ bridge.uiManager.addUIBlock { _, viewRegistry in
283
320
  guard let view = viewRegistry?[rootTag] else { return }
284
321
  var current: UIView? = view
285
322
  while let v = current {
@@ -293,13 +330,14 @@ internal class HyperModule: RCTEventEmitter {
293
330
  }
294
331
  }
295
332
 
296
- private func withNativePaymentWidgetView(_ rootTag: NSNumber, _ block: @escaping (NativePaymentWidgetView) -> Void) {
333
+ private func withNativePaymentWidgetView(_ rootTag: NSNumber, _ block: @escaping (PaymentWidget) -> Void) {
334
+ guard let bridge = self.bridge else { return }
297
335
  RCTGetUIManagerQueue().async {
298
- self.bridge.uiManager.addUIBlock { _, viewRegistry in
336
+ bridge.uiManager.addUIBlock { _, viewRegistry in
299
337
  guard let view = viewRegistry?[rootTag] else { return }
300
338
  var current: UIView? = view
301
339
  while let v = current {
302
- if let nativeWidget = v as? NativePaymentWidgetView {
340
+ if let nativeWidget = v as? PaymentWidget {
303
341
  block(nativeWidget)
304
342
  return
305
343
  }
@@ -310,8 +348,12 @@ internal class HyperModule: RCTEventEmitter {
310
348
  }
311
349
 
312
350
  private func resolveSubscribingTarget(_ rootTag: NSNumber, _ block: @escaping (AnyObject?) -> Void) {
351
+ guard let bridge = self.bridge else {
352
+ DispatchQueue.main.async { block(nil) }
353
+ return
354
+ }
313
355
  RCTGetUIManagerQueue().async {
314
- self.bridge.uiManager.addUIBlock { _, viewRegistry in
356
+ bridge.uiManager.addUIBlock { _, viewRegistry in
315
357
  guard let view = viewRegistry?[rootTag] else {
316
358
  DispatchQueue.main.async { block(nil) }
317
359
  return
@@ -331,8 +373,9 @@ internal class HyperModule: RCTEventEmitter {
331
373
  }
332
374
 
333
375
  private func withPaymentSheet(_ rootTag: NSNumber, _ block: @escaping (UIViewController?, PaymentSheet?) -> Void) {
376
+ guard let bridge = self.bridge else { return }
334
377
  RCTGetUIManagerQueue().async {
335
- self.bridge.uiManager.addUIBlock { _, viewRegistry in
378
+ bridge.uiManager.addUIBlock { _, viewRegistry in
336
379
  let view = viewRegistry?[rootTag]
337
380
  let vc = view?.reactViewController() as? HyperUIViewController
338
381
  let sheet = vc?.paymentSheet
@@ -2,46 +2,82 @@
2
2
  // RNHeadlessManager.swift
3
3
  // Hyperswitch
4
4
  //
5
+ // Hosts the SDK's headless React Native runtime (the "HyperHeadless" module)
6
+ // that drives headless payment sessions without any visible UI.
7
+ //
8
+ // New Architecture note:
9
+ // ----------------------
10
+ // Mirrors RNViewManager: uses the full RCTReactNativeFactory rather than the
11
+ // legacy RCTBridge(delegate:launchOptions:) + RCTRootView(bridge:…) pair.
12
+ // The raw bridge init is incompatible with RCTSetNewArchEnabled(YES) (set
13
+ // globally by the host app's RN runtime), which caused EXC_BAD_ACCESS at
14
+ // offset 0x69f when accessing the bridge's C++ internals via RCTRootView.
15
+ //
16
+ // reinvalidateBridge() is preserved for callers (PaymentSession+UIKit) that
17
+ // need to reset the headless runtime between sessions; it now tears down and
18
+ // lazily recreates the RCTReactNativeFactory instead of the bare bridge.
19
+ //
5
20
  // Created by Shivam Shashank on 09/11/22.
6
21
  //
7
22
 
8
23
  import Foundation
9
24
  import React
25
+ import React_RCTAppDelegate
26
+ import ReactAppDependencyProvider
10
27
 
11
- internal class RNHeadlessManager: NSObject {
28
+ internal class RNHeadlessManager: RCTDefaultReactNativeFactoryDelegate {
12
29
 
13
30
  internal var responseHandler: RNResponseHandler?
14
- internal var rootView: RCTRootView?
31
+ internal var rootView: UIView?
15
32
 
16
- internal lazy var bridgeHeadless: RCTBridge = {
17
- RCTBridge.init(delegate: self, launchOptions: nil)
18
- }()
33
+ private var reactNativeFactory: RCTReactNativeFactory?
19
34
 
20
35
  internal static let sharedInstance = RNHeadlessManager()
21
36
 
22
- internal func viewForModule(_ moduleName: String, initialProperties: [String: Any]?) -> RCTRootView {
23
- let rootView: RCTRootView = RCTRootView(
24
- bridge: self.bridgeHeadless,
25
- moduleName: moduleName,
37
+ // MARK: - Factory lifecycle
38
+
39
+ /// Lazily creates the RCTReactNativeFactory (and its bridge) on first use.
40
+ private func factory() -> RCTReactNativeFactory {
41
+ if let existing = reactNativeFactory {
42
+ return existing
43
+ }
44
+ self.dependencyProvider = RCTAppDependencyProvider()
45
+ let created = RCTReactNativeFactory(delegate: self)
46
+ reactNativeFactory = created
47
+ return created
48
+ }
49
+
50
+ // MARK: - View creation
51
+
52
+ internal func viewForModule(_ moduleName: String, initialProperties: [String: Any]?) -> UIView {
53
+ let view = factory().rootViewFactory.view(
54
+ withModuleName: moduleName,
26
55
  initialProperties: initialProperties
27
56
  )
28
- self.rootView = rootView
29
- return rootView
57
+ self.rootView = view
58
+ return view
30
59
  }
31
60
 
61
+ /// Tear down the current factory (and its bridge) so the next call to
62
+ /// viewForModule starts a fresh JS runtime for the new headless session.
32
63
  internal func reinvalidateBridge() {
33
- self.bridgeHeadless.invalidate()
34
- self.bridgeHeadless = RCTBridge.init(delegate: self, launchOptions: nil)
64
+ reactNativeFactory = nil
65
+ rootView = nil
35
66
  }
36
- }
37
67
 
38
- extension RNHeadlessManager: RCTBridgeDelegate {
39
- func sourceURL(for bridge: RCTBridge) -> URL? {
40
- switch Helper.getInfoPlist("HyperswitchSource") {
41
- case "LocalHosted":
42
- return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
43
- default:
44
- return Bundle(for: RNHeadlessManager.self).url(forResource: "hyperswitch", withExtension: "bundle")
45
- }
68
+ // MARK: - RCTDefaultReactNativeFactoryDelegate
69
+
70
+ /// Use bridge mode (not bridgeless) so the headless module can use the
71
+ /// legacy RCTBridge event / callback APIs.
72
+ public override func bridgelessEnabled() -> Bool {
73
+ return false
74
+ }
75
+
76
+ override func sourceURL(for bridge: RCTBridge) -> URL? {
77
+ return bundleURL()
78
+ }
79
+
80
+ public override func bundleURL() -> URL? {
81
+ return Bundle(for: RNHeadlessManager.self).url(forResource: "hyperswitch", withExtension: "bundle")
46
82
  }
47
83
  }
@@ -2,54 +2,109 @@
2
2
  // RNViewManager.swift
3
3
  // Hyperswitch
4
4
  //
5
+ // Hosts the SDK's embedded React Native runtime that renders the payment sheet
6
+ // from the packaged `hyperswitch.bundle`.
7
+ //
8
+ // New Architecture note:
9
+ // ----------------------
10
+ // The embedded runtime MUST be created through the full `RCTReactNativeFactory`.
11
+ // That initializer performs the essential RN setup — feature flags,
12
+ // `RCTSetNewArchEnabled`, `RCTEnableTurboModule`, the Fabric component provider —
13
+ // and wires a TurboModule manager that can resolve RN's own CORE modules
14
+ // (`DeviceInfo`, `ExceptionsManager`, `RedBox`, …). Building the low-level
15
+ // `RCTRootViewFactory` directly (as the old `RNBridgeFactory` did) leaves the
16
+ // runtime under-initialised, so `getEnforcing('DeviceInfo')` fails and the bundle
17
+ // can't boot — the sheet renders blank.
18
+ //
19
+ // We keep the *bridge* alive by overriding `bridgelessEnabled` → false, because
20
+ // the SDK's widget commands still use `bridge.enqueueJSCall` / `bridge.uiManager`.
21
+ //
5
22
  // Created by Shivam Shashank on 09/11/22.
6
23
  //
7
24
 
8
25
  import Foundation
9
26
  import React
27
+ import React_RCTAppDelegate
28
+ import ReactAppDependencyProvider
10
29
 
11
- internal class RNViewManager: NSObject {
30
+ internal class RNViewManager: RCTDefaultReactNativeFactoryDelegate {
12
31
 
13
32
  internal var responseHandler: RNResponseHandler?
14
- internal var rootView: RCTRootView?
33
+ internal var rootView: UIView?
15
34
 
16
- internal lazy var bridge: RCTBridge = {
17
- RCTBridge.init(delegate: self, launchOptions: nil)
18
- }()
35
+ private var reactNativeFactory: RCTReactNativeFactory?
36
+
37
+ /// Captured when the embedded bridge loads its JS source (see `sourceURL(for:)`).
38
+ /// The host app is bridgeless, so this embedded bridge is the SDK's only bridge.
39
+ private weak var capturedBridge: RCTBridge?
19
40
 
20
41
  internal static let sharedInstance = RNViewManager()
21
42
 
22
- internal func viewForModule(_ moduleName: String, initialProperties: [String: Any]?) -> RCTRootView {
23
- let rootView: RCTRootView = RCTRootView(
24
- bridge: self.bridge,
25
- moduleName: moduleName,
43
+ /// Lazily create the standard RN factory (does the full runtime setup).
44
+ private func factory() -> RCTReactNativeFactory {
45
+ if let existing = reactNativeFactory {
46
+ return existing
47
+ }
48
+ self.dependencyProvider = RCTAppDependencyProvider()
49
+ let created = RCTReactNativeFactory(delegate: self)
50
+ reactNativeFactory = created
51
+ return created
52
+ }
53
+
54
+ /// The bridge backing the embedded runtime. Kept for the SDK's widget commands
55
+ /// (`bridge.enqueueJSCall` / `bridge.uiManager`). Available after the first view
56
+ /// is created.
57
+ internal var bridge: RCTBridge? {
58
+ return capturedBridge
59
+ }
60
+
61
+ internal func viewForModule(_ moduleName: String, initialProperties: [String: Any]?) -> UIView {
62
+ let view = factory().rootViewFactory.view(
63
+ withModuleName: moduleName,
26
64
  initialProperties: initialProperties
27
65
  )
28
- self.rootView = rootView
29
- return rootView
66
+ self.rootView = view
67
+ return view
30
68
  }
31
- internal func widgetViewForModule(_ moduleName: String, initialProperties: [String: Any]?) -> RCTRootView {
32
- return RCTRootView(
33
- bridge: self.bridge,
34
- moduleName: moduleName,
69
+
70
+ internal func widgetViewForModule(_ moduleName: String, initialProperties: [String: Any]?) -> UIView {
71
+ return factory().rootViewFactory.view(
72
+ withModuleName: moduleName,
35
73
  initialProperties: initialProperties
36
74
  )
37
75
  }
38
- }
39
76
 
40
- extension RNViewManager: RCTBridgeDelegate {
41
- func sourceURL(for bridge: RCTBridge) -> URL? {
42
- switch Helper.getInfoPlist("HyperswitchSource") {
43
- case "LocalHosted":
44
- return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
45
- case "LocalBundle":
46
- return Bundle.main.url(forResource: "hyperswitch", withExtension: "bundle")
47
- default:
48
- #if canImport(HyperOTA)
49
- return OTAServices.shared.getBundleURL()
50
- #else
77
+ // MARK: - Arch configuration
78
+
79
+ /// Keep the legacy bridge alive (Fabric-on-bridge) so the SDK's
80
+ /// `bridge.enqueueJSCall` / `bridge.uiManager` widget commands keep working.
81
+ public override func bridgelessEnabled() -> Bool {
82
+ return false
83
+ }
84
+
85
+ // MARK: - Bundle source
86
+ // Both are required in bridge mode: the default implementations throw.
87
+
88
+ override func sourceURL(for bridge: RCTBridge) -> URL? {
89
+ // Called while the embedded bridge loads its JS — capture it for the SDK's
90
+ // bridge-dependent widget commands.
91
+ self.capturedBridge = bridge
92
+ return bundleURL()
93
+ }
94
+
95
+ public override func bundleURL() -> URL? {
96
+ // switch Helper.getInfoPlist("HyperswitchSource") {
97
+ // case "LocalHosted":
98
+ // return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
99
+ // case "LocalBundle":
100
+ // return Bundle.main.url(forResource: "hyperswitch", withExtension: "bundle")
101
+ // default:
102
+ // #if canImport(HyperOTA)
103
+ // return OTAServices.shared.getBundleURL()
104
+ // #else
51
105
  return Bundle(for: RNViewManager.self).url(forResource: "hyperswitch", withExtension: "bundle")
52
- #endif
53
- }
106
+ // #endif
107
+ // }
54
108
  }
55
109
  }
110
+