@onramper/onramper-react-native 1.0.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.
Files changed (163) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/OnramperReactNative.podspec +36 -0
  5. package/README.md +114 -0
  6. package/app.plugin.js +51 -0
  7. package/ios/Frameworks/LICENSE +202 -0
  8. package/ios/Frameworks/NOTICE +5 -0
  9. package/ios/Frameworks/OnramperSDK.xcframework/Info.plist +48 -0
  10. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64/OnramperSDK.framework/Info.plist +0 -0
  11. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64/OnramperSDK.framework/Modules/OnramperSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  12. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64/OnramperSDK.framework/Modules/OnramperSDK.swiftmodule/arm64-apple-ios.swiftinterface +548 -0
  13. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64/OnramperSDK.framework/OnramperSDK +0 -0
  14. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64/OnramperSDK.framework/OnramperSDK_OnramperSDK.bundle/Info.plist +0 -0
  15. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64/OnramperSDK.framework/OnramperSDK_OnramperSDK.bundle/PrivacyInfo.xcprivacy +27 -0
  16. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64/OnramperSDK.framework/PrivacyInfo.xcprivacy +27 -0
  17. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64/dSYMs/OnramperSDK.framework.dSYM/Contents/Info.plist +20 -0
  18. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64/dSYMs/OnramperSDK.framework.dSYM/Contents/Resources/DWARF/OnramperSDK +0 -0
  19. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64/dSYMs/OnramperSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/OnramperSDK.yml +2518 -0
  20. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/OnramperSDK.framework/Info.plist +0 -0
  21. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/OnramperSDK.framework/Modules/OnramperSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  22. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/OnramperSDK.framework/Modules/OnramperSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +548 -0
  23. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/OnramperSDK.framework/Modules/OnramperSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  24. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/OnramperSDK.framework/Modules/OnramperSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +548 -0
  25. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/OnramperSDK.framework/OnramperSDK +0 -0
  26. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/OnramperSDK.framework/OnramperSDK_OnramperSDK.bundle/Info.plist +0 -0
  27. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/OnramperSDK.framework/OnramperSDK_OnramperSDK.bundle/PrivacyInfo.xcprivacy +27 -0
  28. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/OnramperSDK.framework/PrivacyInfo.xcprivacy +27 -0
  29. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/OnramperSDK.framework.dSYM/Contents/Info.plist +20 -0
  30. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/OnramperSDK.framework.dSYM/Contents/Resources/DWARF/OnramperSDK +0 -0
  31. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/OnramperSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/OnramperSDK.yml +2518 -0
  32. package/ios/Frameworks/OnramperSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/OnramperSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/OnramperSDK.yml +2487 -0
  33. package/ios/HybridOnramperCheckoutButton.swift +112 -0
  34. package/ios/HybridOnramperNitro.swift +194 -0
  35. package/ios/OnramperCheckoutBridge.swift +88 -0
  36. package/ios/OnramperSDKBridge.swift +185 -0
  37. package/ios/PreparedIntentRegistry.swift +78 -0
  38. package/lib/commonjs/OnramperCheckoutButtonView.js +16 -0
  39. package/lib/commonjs/OnramperCheckoutButtonView.js.map +1 -0
  40. package/lib/commonjs/OnramperClient.js +161 -0
  41. package/lib/commonjs/OnramperClient.js.map +1 -0
  42. package/lib/commonjs/OnramperNative.js +22 -0
  43. package/lib/commonjs/OnramperNative.js.map +1 -0
  44. package/lib/commonjs/errors.js +35 -0
  45. package/lib/commonjs/errors.js.map +1 -0
  46. package/lib/commonjs/events.js +6 -0
  47. package/lib/commonjs/events.js.map +1 -0
  48. package/lib/commonjs/generated/OnramperCheckoutButtonConfig.json +10 -0
  49. package/lib/commonjs/index.js +27 -0
  50. package/lib/commonjs/index.js.map +1 -0
  51. package/lib/commonjs/package.json +1 -0
  52. package/lib/commonjs/specs/OnramperCheckoutButton.nitro.js +6 -0
  53. package/lib/commonjs/specs/OnramperCheckoutButton.nitro.js.map +1 -0
  54. package/lib/commonjs/specs/OnramperNitro.nitro.js +6 -0
  55. package/lib/commonjs/specs/OnramperNitro.nitro.js.map +1 -0
  56. package/lib/commonjs/types.js +6 -0
  57. package/lib/commonjs/types.js.map +1 -0
  58. package/lib/module/OnramperCheckoutButtonView.js +11 -0
  59. package/lib/module/OnramperCheckoutButtonView.js.map +1 -0
  60. package/lib/module/OnramperClient.js +156 -0
  61. package/lib/module/OnramperClient.js.map +1 -0
  62. package/lib/module/OnramperNative.js +18 -0
  63. package/lib/module/OnramperNative.js.map +1 -0
  64. package/lib/module/errors.js +30 -0
  65. package/lib/module/errors.js.map +1 -0
  66. package/lib/module/events.js +4 -0
  67. package/lib/module/events.js.map +1 -0
  68. package/lib/module/generated/OnramperCheckoutButtonConfig.json +10 -0
  69. package/lib/module/index.js +8 -0
  70. package/lib/module/index.js.map +1 -0
  71. package/lib/module/package.json +1 -0
  72. package/lib/module/specs/OnramperCheckoutButton.nitro.js +4 -0
  73. package/lib/module/specs/OnramperCheckoutButton.nitro.js.map +1 -0
  74. package/lib/module/specs/OnramperNitro.nitro.js +4 -0
  75. package/lib/module/specs/OnramperNitro.nitro.js.map +1 -0
  76. package/lib/module/types.js +4 -0
  77. package/lib/module/types.js.map +1 -0
  78. package/lib/typescript/commonjs/package.json +1 -0
  79. package/lib/typescript/commonjs/src/OnramperCheckoutButtonView.d.ts +9 -0
  80. package/lib/typescript/commonjs/src/OnramperCheckoutButtonView.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/src/OnramperClient.d.ts +49 -0
  82. package/lib/typescript/commonjs/src/OnramperClient.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/src/OnramperNative.d.ts +9 -0
  84. package/lib/typescript/commonjs/src/OnramperNative.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/src/errors.d.ts +13 -0
  86. package/lib/typescript/commonjs/src/errors.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/src/events.d.ts +48 -0
  88. package/lib/typescript/commonjs/src/events.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/src/index.d.ts +9 -0
  90. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/src/specs/OnramperCheckoutButton.nitro.d.ts +10 -0
  92. package/lib/typescript/commonjs/src/specs/OnramperCheckoutButton.nitro.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/src/specs/OnramperNitro.nitro.d.ts +39 -0
  94. package/lib/typescript/commonjs/src/specs/OnramperNitro.nitro.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/src/types.d.ts +127 -0
  96. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  97. package/lib/typescript/module/package.json +1 -0
  98. package/lib/typescript/module/src/OnramperCheckoutButtonView.d.ts +9 -0
  99. package/lib/typescript/module/src/OnramperCheckoutButtonView.d.ts.map +1 -0
  100. package/lib/typescript/module/src/OnramperClient.d.ts +49 -0
  101. package/lib/typescript/module/src/OnramperClient.d.ts.map +1 -0
  102. package/lib/typescript/module/src/OnramperNative.d.ts +9 -0
  103. package/lib/typescript/module/src/OnramperNative.d.ts.map +1 -0
  104. package/lib/typescript/module/src/errors.d.ts +13 -0
  105. package/lib/typescript/module/src/errors.d.ts.map +1 -0
  106. package/lib/typescript/module/src/events.d.ts +48 -0
  107. package/lib/typescript/module/src/events.d.ts.map +1 -0
  108. package/lib/typescript/module/src/index.d.ts +9 -0
  109. package/lib/typescript/module/src/index.d.ts.map +1 -0
  110. package/lib/typescript/module/src/specs/OnramperCheckoutButton.nitro.d.ts +10 -0
  111. package/lib/typescript/module/src/specs/OnramperCheckoutButton.nitro.d.ts.map +1 -0
  112. package/lib/typescript/module/src/specs/OnramperNitro.nitro.d.ts +39 -0
  113. package/lib/typescript/module/src/specs/OnramperNitro.nitro.d.ts.map +1 -0
  114. package/lib/typescript/module/src/types.d.ts +127 -0
  115. package/lib/typescript/module/src/types.d.ts.map +1 -0
  116. package/nitro.json +19 -0
  117. package/nitrogen/generated/.gitattributes +1 -0
  118. package/nitrogen/generated/ios/OnramperReactNative+autolinking.rb +62 -0
  119. package/nitrogen/generated/ios/OnramperReactNative-Swift-Cxx-Bridge.cpp +107 -0
  120. package/nitrogen/generated/ios/OnramperReactNative-Swift-Cxx-Bridge.hpp +299 -0
  121. package/nitrogen/generated/ios/OnramperReactNative-Swift-Cxx-Umbrella.hpp +60 -0
  122. package/nitrogen/generated/ios/OnramperReactNativeAutolinking.mm +41 -0
  123. package/nitrogen/generated/ios/OnramperReactNativeAutolinking.swift +38 -0
  124. package/nitrogen/generated/ios/c++/HybridOnramperCheckoutButtonSpecSwift.cpp +11 -0
  125. package/nitrogen/generated/ios/c++/HybridOnramperCheckoutButtonSpecSwift.hpp +81 -0
  126. package/nitrogen/generated/ios/c++/HybridOnramperNitroSpecSwift.cpp +11 -0
  127. package/nitrogen/generated/ios/c++/HybridOnramperNitroSpecSwift.hpp +150 -0
  128. package/nitrogen/generated/ios/c++/views/HybridOnramperCheckoutButtonComponent.mm +122 -0
  129. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_NitroSessionCredentials____.swift +61 -0
  130. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  131. package/nitrogen/generated/ios/swift/Func_void_NitroSessionCredentials.swift +46 -0
  132. package/nitrogen/generated/ios/swift/Func_void_PreparedIntentResult.swift +46 -0
  133. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  134. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_Promise_NitroSessionCredentials__.swift +66 -0
  135. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  136. package/nitrogen/generated/ios/swift/HybridOnramperCheckoutButtonSpec.swift +55 -0
  137. package/nitrogen/generated/ios/swift/HybridOnramperCheckoutButtonSpec_cxx.swift +156 -0
  138. package/nitrogen/generated/ios/swift/HybridOnramperNitroSpec.swift +63 -0
  139. package/nitrogen/generated/ios/swift/HybridOnramperNitroSpec_cxx.swift +309 -0
  140. package/nitrogen/generated/ios/swift/NitroSessionCredentials.swift +34 -0
  141. package/nitrogen/generated/ios/swift/OnramperNitroConfig.swift +49 -0
  142. package/nitrogen/generated/ios/swift/PreparedIntentResult.swift +34 -0
  143. package/nitrogen/generated/shared/c++/HybridOnramperCheckoutButtonSpec.cpp +22 -0
  144. package/nitrogen/generated/shared/c++/HybridOnramperCheckoutButtonSpec.hpp +63 -0
  145. package/nitrogen/generated/shared/c++/HybridOnramperNitroSpec.cpp +29 -0
  146. package/nitrogen/generated/shared/c++/HybridOnramperNitroSpec.hpp +80 -0
  147. package/nitrogen/generated/shared/c++/NitroSessionCredentials.hpp +87 -0
  148. package/nitrogen/generated/shared/c++/OnramperNitroConfig.hpp +99 -0
  149. package/nitrogen/generated/shared/c++/PreparedIntentResult.hpp +87 -0
  150. package/nitrogen/generated/shared/c++/views/HybridOnramperCheckoutButtonComponent.cpp +83 -0
  151. package/nitrogen/generated/shared/c++/views/HybridOnramperCheckoutButtonComponent.hpp +111 -0
  152. package/nitrogen/generated/shared/json/OnramperCheckoutButtonConfig.json +10 -0
  153. package/package.json +107 -0
  154. package/src/OnramperCheckoutButtonView.tsx +15 -0
  155. package/src/OnramperClient.ts +170 -0
  156. package/src/OnramperNative.ts +19 -0
  157. package/src/errors.ts +69 -0
  158. package/src/events.ts +23 -0
  159. package/src/generated/OnramperCheckoutButtonConfig.json +10 -0
  160. package/src/index.ts +30 -0
  161. package/src/specs/OnramperCheckoutButton.nitro.ts +16 -0
  162. package/src/specs/OnramperNitro.nitro.ts +60 -0
  163. package/src/types.ts +133 -0
@@ -0,0 +1,112 @@
1
+ #if canImport(UIKit)
2
+ import NitroModules
3
+ import SwiftUI
4
+ import UIKit
5
+
6
+ /// Nitro view that renders the SwiftUI `OnramperCheckoutButton` produced by
7
+ /// `getCheckoutRequirements` and stashed in `PreparedIntentRegistry.shared`
8
+ /// under `intentHandle`. The button is hosted in a `UIHostingController` whose
9
+ /// view is added to our container; the controller is attached as a child of the
10
+ /// nearest React view controller so the SDK button can present its login /
11
+ /// webview sheets.
12
+ final class HybridOnramperCheckoutButton: HybridOnramperCheckoutButtonSpec {
13
+ private let container = CheckoutContainerView()
14
+ private var mountedHandle: String?
15
+
16
+ var intentHandle: String = "" {
17
+ didSet {
18
+ guard !intentHandle.isEmpty, intentHandle != mountedHandle else { return }
19
+ mount(handle: intentHandle)
20
+ }
21
+ }
22
+
23
+ var view: UIView { container }
24
+
25
+ private func mount(handle: String) {
26
+ mountedHandle = handle
27
+ Task { @MainActor [weak self] in
28
+ guard let self else { return }
29
+ guard let entry = await PreparedIntentRegistry.shared.consume(handle) else {
30
+ // Handle was invalidated or already consumed — leave the view empty.
31
+ return
32
+ }
33
+ // Pin the SwiftUI content to the container's width so long ToS text wraps
34
+ // instead of laying out at its intrinsic (single-line) width and spilling
35
+ // past the right edge. `.fixedSize(vertical:)` lets it grow downward.
36
+ self.container.host(AnyView(
37
+ entry.button
38
+ .frame(maxWidth: .infinity, alignment: .leading)
39
+ .fixedSize(horizontal: false, vertical: true)
40
+ ))
41
+ }
42
+ }
43
+ }
44
+
45
+ /// Container that hosts the SwiftUI button and (re)attaches the hosting
46
+ /// controller to the React view-controller hierarchy when it enters a window.
47
+ final class CheckoutContainerView: UIView {
48
+ private var hostingController: UIHostingController<AnyView>?
49
+
50
+ @MainActor
51
+ func host(_ rootView: AnyView) {
52
+ // Re-hosting (e.g. when `intentHandle` changes) must tear the previous
53
+ // controller down first, otherwise its view/constraints accumulate and the
54
+ // old controller stays childed to the parent VC.
55
+ detachHosted()
56
+ let controller = UIHostingController(rootView: rootView)
57
+ controller.view.backgroundColor = .clear
58
+ controller.view.translatesAutoresizingMaskIntoConstraints = false
59
+ hostingController = controller
60
+ addSubview(controller.view)
61
+ NSLayoutConstraint.activate([
62
+ controller.view.leadingAnchor.constraint(equalTo: leadingAnchor),
63
+ controller.view.trailingAnchor.constraint(equalTo: trailingAnchor),
64
+ controller.view.topAnchor.constraint(equalTo: topAnchor),
65
+ controller.view.bottomAnchor.constraint(equalTo: bottomAnchor),
66
+ ])
67
+ attachChildIfPossible()
68
+ }
69
+
70
+ override func didMoveToWindow() {
71
+ super.didMoveToWindow()
72
+ attachChildIfPossible()
73
+ }
74
+
75
+ private func attachChildIfPossible() {
76
+ guard let controller = hostingController, controller.parent == nil,
77
+ let parent = nearestViewController() else { return }
78
+ parent.addChild(controller)
79
+ controller.didMove(toParent: parent)
80
+ }
81
+
82
+ private func nearestViewController() -> UIViewController? {
83
+ var responder: UIResponder? = self
84
+ while let current = responder {
85
+ if let vc = current as? UIViewController { return vc }
86
+ responder = current.next
87
+ }
88
+ return nil
89
+ }
90
+
91
+ /// Detaches the currently hosted controller from the VC hierarchy and removes
92
+ /// its view/constraints. Safe to call when nothing is hosted.
93
+ @MainActor
94
+ private func detachHosted() {
95
+ guard let controller = hostingController else { return }
96
+ controller.willMove(toParent: nil)
97
+ controller.view.removeFromSuperview()
98
+ controller.removeFromParent()
99
+ hostingController = nil
100
+ }
101
+
102
+ deinit {
103
+ // deinit can't hop to @MainActor; the container is torn down on the main
104
+ // thread by UIKit, so touching the controller here is safe.
105
+ if let controller = hostingController {
106
+ controller.willMove(toParent: nil)
107
+ controller.view.removeFromSuperview()
108
+ controller.removeFromParent()
109
+ }
110
+ }
111
+ }
112
+ #endif
@@ -0,0 +1,194 @@
1
+ import Combine
2
+ import Foundation
3
+ import NitroModules
4
+ import OnramperSDK
5
+ import SwiftUI
6
+
7
+ /// Nitro bridge to OnramperSDK's `OnramperClient`. Wraps the @MainActor SDK
8
+ /// client, mirrors its `@Published state` and `events` stream to stored JS
9
+ /// callbacks, and feeds the SDK's `sessionExpirationHandler` from a stored
10
+ /// async JS callback. The public typed JS API lives in `OnramperClient.ts`.
11
+ final class HybridOnramperNitro: HybridOnramperNitroSpec {
12
+ private var client: OnramperClient?
13
+ private var stateObservation: AnyCancellable?
14
+ private var eventTask: Task<Void, Never>?
15
+
16
+ private var onState: ((String) -> Void)?
17
+ private var onEvent: ((String) -> Void)?
18
+ // Nitro wraps a callback's return value in a Promise; our callback already
19
+ // returns Promise<NitroSessionCredentials>, hence the doubly-nested Promise.
20
+ private var sessionHandler: (() -> Promise<Promise<NitroSessionCredentials>>)?
21
+
22
+ // Handle of the most recent prepared intent owned by *this* client, used to
23
+ // enforce single-flight per client without touching other clients' intents.
24
+ private var lastPreparedHandle: String?
25
+
26
+ // MARK: - Listener registration (synchronous)
27
+
28
+ func setStateListener(onState: @escaping (String) -> Void) {
29
+ self.onState = onState
30
+ }
31
+
32
+ func setEventListener(onEvent: @escaping (String) -> Void) {
33
+ self.onEvent = onEvent
34
+ }
35
+
36
+ func setSessionExpirationHandler(handler: @escaping () -> Promise<Promise<NitroSessionCredentials>>) {
37
+ self.sessionHandler = handler
38
+ }
39
+
40
+ // MARK: - Lifecycle
41
+
42
+ func configure(config: OnramperNitroConfig) throws -> Promise<Void> {
43
+ return Promise.async { [weak self] in
44
+ guard let self else { return }
45
+ try await self.performConfigure(config)
46
+ }
47
+ }
48
+
49
+ @MainActor
50
+ private func performConfigure(_ config: OnramperNitroConfig) async throws {
51
+ // Idempotent: a second configure() with the SDK already built is a no-op.
52
+ if client != nil { return }
53
+
54
+ let environment: OnramperConfiguration.Environment =
55
+ config.environment == "development" ? .development : .production
56
+ let theme: OnramperConfiguration.Theme = {
57
+ switch config.theme {
58
+ case "light": return .light
59
+ case "dark": return .dark
60
+ default: return .system
61
+ }
62
+ }()
63
+ let logLevel: LogLevel = {
64
+ switch config.logLevel {
65
+ case "error": return .error
66
+ case "info": return .info
67
+ case "debug": return .debug
68
+ default: return .off
69
+ }
70
+ }()
71
+
72
+ let swiftConfig = OnramperConfiguration(
73
+ apiKey: config.apiKey,
74
+ clientId: config.clientId,
75
+ environment: environment,
76
+ theme: theme,
77
+ logLevel: logLevel,
78
+ sessionExpirationHandler: { [weak self] in
79
+ guard let self, let handler = self.sessionHandler else {
80
+ throw OnramperError.userTokenRefreshFailed("no session expiration handler registered")
81
+ }
82
+ // Await twice: outer Promise = the callback invocation, inner Promise =
83
+ // the JS async function's own returned Promise. Then map to the SDK type.
84
+ let creds = try await handler().await().await()
85
+ return SessionCredentials(sessionId: creds.sessionId, sessionToken: creds.sessionToken)
86
+ }
87
+ )
88
+
89
+ let newClient = OnramperClient(configuration: swiftConfig)
90
+ client = newClient
91
+ attachObservers(to: newClient)
92
+ }
93
+
94
+ @MainActor
95
+ private func attachObservers(to client: OnramperClient) {
96
+ stateObservation = client.$state.sink { [weak self] state in
97
+ self?.onState?(jsonString(state.toJSDict()))
98
+ }
99
+ // `events` is a computed AsyncStream that captures a single continuation, so
100
+ // access it exactly once here. CheckoutEvent is Sendable → safe to iterate
101
+ // off the main actor.
102
+ let stream = client.events
103
+ eventTask = Task { [weak self] in
104
+ for await event in stream {
105
+ self?.onEvent?(jsonString(event.toJSDict()))
106
+ }
107
+ }
108
+ }
109
+
110
+ @MainActor
111
+ private func requireClient() throws -> OnramperClient {
112
+ guard let client else { throw OnramperError.notInitialized }
113
+ return client
114
+ }
115
+
116
+ func initialize(sessionId: String, sessionToken: String) throws -> Promise<Void> {
117
+ return Promise.async { [weak self] in
118
+ guard let self else { return }
119
+ let client = try await self.requireClient()
120
+ try await client.initialize(sessionId: sessionId, sessionToken: sessionToken)
121
+ }
122
+ }
123
+
124
+ func reset() throws -> Promise<Void> {
125
+ return Promise.async { [weak self] in
126
+ guard let self else { return }
127
+ let client = try await self.requireClient()
128
+ await client.reset()
129
+ }
130
+ }
131
+
132
+ func signOut() throws -> Promise<Void> {
133
+ return Promise.async { [weak self] in
134
+ guard let self else { return }
135
+ let client = try await self.requireClient()
136
+ await client.signOut()
137
+ }
138
+ }
139
+
140
+ // MARK: - Checkout
141
+
142
+ func getCheckoutRequirements(requestJson: String, styleJson: String) throws -> Promise<PreparedIntentResult> {
143
+ return Promise.async { [weak self] in
144
+ guard let self else { throw OnramperError.notInitialized }
145
+ return try await self.performGetCheckoutRequirements(requestJson: requestJson, styleJson: styleJson)
146
+ }
147
+ }
148
+
149
+ @MainActor
150
+ private func performGetCheckoutRequirements(requestJson: String, styleJson: String) async throws -> PreparedIntentResult {
151
+ let client = try requireClient()
152
+ let request = try decodeCheckoutRequest(requestJson)
153
+ let style = decodeCheckoutButtonStyle(styleJson)
154
+
155
+ // Single-flight, scoped to this client: a new prepared intent supersedes
156
+ // this instance's prior unconsumed one, but leaves other clients' intents
157
+ // in the shared registry untouched.
158
+ if let prev = lastPreparedHandle {
159
+ await PreparedIntentRegistry.shared.drop(prev)
160
+ }
161
+
162
+ let result = try await client.getCheckoutRequirements(request, buttonStyle: style)
163
+ let entry = PreparedIntentRegistry.PreparedIntent(button: AnyView(result.button), createdAt: Date())
164
+ let handle = await PreparedIntentRegistry.shared.store(entry)
165
+ lastPreparedHandle = handle
166
+ let quoteDict = (codableToJSValue(result.quote) as? [String: Any]) ?? [:]
167
+ return PreparedIntentResult(intentHandle: handle, quoteJson: jsonString(quoteDict))
168
+ }
169
+
170
+ func cancelPreparedIntent(intentHandle: String) throws -> Promise<Void> {
171
+ if intentHandle == lastPreparedHandle {
172
+ lastPreparedHandle = nil
173
+ }
174
+ return Promise.async {
175
+ await PreparedIntentRegistry.shared.drop(intentHandle)
176
+ }
177
+ }
178
+
179
+ // MARK: - Teardown
180
+
181
+ /// Releases the stored JS callbacks (Nitro holds strong refs to them) and the
182
+ /// SDK client. Called from `OnramperClient.destroy()` in JS.
183
+ func dispose() {
184
+ stateObservation?.cancel()
185
+ stateObservation = nil
186
+ eventTask?.cancel()
187
+ eventTask = nil
188
+ onState = nil
189
+ onEvent = nil
190
+ sessionHandler = nil
191
+ lastPreparedHandle = nil
192
+ client = nil
193
+ }
194
+ }
@@ -0,0 +1,88 @@
1
+ #if canImport(UIKit)
2
+ import Foundation
3
+ import OnramperSDK
4
+ import SwiftUI
5
+
6
+ // Decodes the flat JS CheckoutRequest / CheckoutButtonStyle JSON into the SDK's
7
+ // nested request + styled button config. (The SDK's OnramperTransactionData.init
8
+ // lowercases ISO codes, so we construct via inits rather than Codable-decoding
9
+ // straight into CheckoutIntentRequest.)
10
+
11
+ private struct CheckoutRequestJSON: Decodable {
12
+ let source: String
13
+ let destination: String?
14
+ let amount: Double
15
+ let type: String
16
+ let country: String?
17
+ let subdivision: String?
18
+ let paymentMethod: String
19
+ let wallet: WalletJSON
20
+ let onlyOnramps: [String]?
21
+
22
+ struct WalletJSON: Decodable {
23
+ let network: String
24
+ let address: String
25
+ let memo: String?
26
+ }
27
+ }
28
+
29
+ func decodeCheckoutRequest(_ json: String) throws -> CheckoutIntentRequest {
30
+ guard let data = json.data(using: .utf8) else {
31
+ throw OnramperError.invalidRequest(field: nil, debugInfo: "request JSON not UTF-8")
32
+ }
33
+ do {
34
+ let r = try JSONDecoder().decode(CheckoutRequestJSON.self, from: data)
35
+ let txData = OnramperTransactionData(
36
+ source: r.source,
37
+ destination: r.destination,
38
+ amount: r.amount,
39
+ type: r.type == "sell" ? .sell : .buy,
40
+ country: r.country,
41
+ subdivision: r.subdivision,
42
+ paymentMethod: r.paymentMethod,
43
+ wallet: WalletInfo(network: r.wallet.network, address: r.wallet.address, memo: r.wallet.memo)
44
+ )
45
+ return CheckoutIntentRequest(onramperTransactionData: txData, onlyOnramps: r.onlyOnramps)
46
+ } catch {
47
+ throw OnramperError.invalidRequest(field: nil, debugInfo: "request decode failed: \(error)")
48
+ }
49
+ }
50
+
51
+ private struct CheckoutButtonStyleJSON: Decodable {
52
+ let backgroundColor: String?
53
+ let foregroundColor: String?
54
+ let borderRadius: Double?
55
+ }
56
+
57
+ func decodeCheckoutButtonStyle(_ json: String) -> CheckoutButtonStyle {
58
+ var style = CheckoutButtonStyle()
59
+ guard let data = json.data(using: .utf8),
60
+ let s = try? JSONDecoder().decode(CheckoutButtonStyleJSON.self, from: data) else {
61
+ return style
62
+ }
63
+ if let hex = s.backgroundColor, let color = Color(hex: hex) { style.backgroundColor = color }
64
+ if let hex = s.foregroundColor, let color = Color(hex: hex) { style.foregroundColor = color }
65
+ if let radius = s.borderRadius { style.borderRadius = CGFloat(radius) }
66
+ return style
67
+ }
68
+
69
+ extension Color {
70
+ init?(hex: String) {
71
+ let s = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#"))
72
+ guard s.count == 6 || s.count == 8, let value = UInt64(s, radix: 16) else { return nil }
73
+ let r, g, b, a: Double
74
+ if s.count == 6 {
75
+ r = Double((value >> 16) & 0xFF) / 255
76
+ g = Double((value >> 8) & 0xFF) / 255
77
+ b = Double(value & 0xFF) / 255
78
+ a = 1
79
+ } else {
80
+ r = Double((value >> 24) & 0xFF) / 255
81
+ g = Double((value >> 16) & 0xFF) / 255
82
+ b = Double((value >> 8) & 0xFF) / 255
83
+ a = Double(value & 0xFF) / 255
84
+ }
85
+ self = Color(.sRGB, red: r, green: g, blue: b, opacity: a)
86
+ }
87
+ }
88
+ #endif
@@ -0,0 +1,185 @@
1
+ import Foundation
2
+ import OnramperSDK
3
+
4
+ // Maps the SDK's non-Codable Swift enums (OnramperState, CheckoutEvent) and
5
+ // OnramperError into JS-friendly dictionaries, then to JSON strings for the
6
+ // Nitro bridge. This is SDK-shape mapping, not Expo-specific — it has no
7
+ // dependency on any bridging framework.
8
+
9
+ // MARK: - Error → code / structured info
10
+
11
+ extension OnramperError {
12
+ var jsCode: String {
13
+ switch self {
14
+ case .notInitialized: return "notInitialized"
15
+ case .initializationFailed: return "initializationFailed"
16
+ case .attestationFailed: return "attestationFailed"
17
+ case .invalidStateTransition: return "invalidStateTransition"
18
+ case .invalidState: return "invalidState"
19
+ case .networkError: return "networkError"
20
+ case .decodingError: return "decodingError"
21
+ case .timeout: return "timeout"
22
+ case .requirementNotSatisfied: return "requirementNotSatisfied"
23
+ case .amountOutOfRange: return "amountOutOfRange"
24
+ case .oidcFlowCancelled: return "oidcFlowCancelled"
25
+ case .oidcTokenExchangeFailed: return "oidcTokenExchangeFailed"
26
+ case .oidcFlowFailed: return "oidcFlowFailed"
27
+ case .userTokenInvalid: return "userTokenInvalid"
28
+ case .userTokenRefreshFailed: return "userTokenRefreshFailed"
29
+ case .webviewLoadFailed: return "webviewLoadFailed"
30
+ case .deepLinkFailed: return "deepLinkFailed"
31
+ case .invalidRequest: return "invalidRequest"
32
+ case .quoteUnavailable: return "quoteUnavailable"
33
+ case .checkoutForbidden: return "checkoutForbidden"
34
+ case .temporaryFailure: return "temporaryFailure"
35
+ case .unrecoverable: return "unrecoverable"
36
+ case .configurationError: return "configurationError"
37
+ case .deviceBlocked: return "deviceBlocked"
38
+ case .securityStorageFailed: return "securityStorageFailed"
39
+ case .securityTrustFailed: return "securityTrustFailed"
40
+ }
41
+ }
42
+
43
+ var jsInfo: [String: Any]? {
44
+ switch self {
45
+ case .amountOutOfRange(let min, let max):
46
+ return ["min": min, "max": max]
47
+ case .networkError(let code, _):
48
+ return ["status": code]
49
+ case .securityStorageFailed(let status):
50
+ return ["status": Int(status)]
51
+ case .securityTrustFailed(let host):
52
+ return ["host": host]
53
+ case .invalidStateTransition(let from, let to):
54
+ return ["from": from, "to": to]
55
+ case .invalidState(let expected, let actual):
56
+ return ["expected": expected, "actual": actual]
57
+ case .invalidRequest(let field, let debugInfo):
58
+ var info: [String: Any] = [:]
59
+ if let field { info["field"] = field }
60
+ if let debugInfo { info["debugInfo"] = debugInfo }
61
+ return info.isEmpty ? nil : info
62
+ case .temporaryFailure(let retryAfter, let debugInfo):
63
+ var info: [String: Any] = [:]
64
+ if let retryAfter { info["retryAfter"] = retryAfter }
65
+ if let debugInfo { info["debugInfo"] = debugInfo }
66
+ return info.isEmpty ? nil : info
67
+ case .quoteUnavailable(let debugInfo),
68
+ .checkoutForbidden(let debugInfo),
69
+ .unrecoverable(let debugInfo):
70
+ return debugInfo.map { ["debugInfo": $0] }
71
+ default:
72
+ return nil
73
+ }
74
+ }
75
+
76
+ /// A JS-friendly error dict: `{ code, message, info? }`.
77
+ func toJSDict() -> [String: Any] {
78
+ var dict: [String: Any] = ["code": jsCode, "message": errorDescription ?? "Unknown error"]
79
+ if let info = jsInfo { dict["info"] = info }
80
+ return dict
81
+ }
82
+ }
83
+
84
+ // MARK: - Enum raw values
85
+
86
+ extension RenderType {
87
+ var jsValue: String { rawValue } // "webview" | "deeplink"
88
+ }
89
+
90
+ extension CheckoutPaymentType {
91
+ var jsValue: String { rawValue } // "applepay" | "revolutpay"
92
+ }
93
+
94
+ // MARK: - State → dict
95
+
96
+ extension OnramperState {
97
+ func toJSDict() -> [String: Any] {
98
+ switch self {
99
+ case .idle: return ["kind": "idle"]
100
+ case .initializing: return ["kind": "initializing"]
101
+ case .ready: return ["kind": "ready"]
102
+ case .checkoutPreparing: return ["kind": "checkoutPreparing"]
103
+ case .requireLogin(let requirements):
104
+ return ["kind": "requireLogin", "requirements": codableToJSArray(requirements)]
105
+ case .authenticating: return ["kind": "authenticating"]
106
+ case .readyToCheckout: return ["kind": "readyToCheckout"]
107
+ case .finalizing: return ["kind": "finalizing"]
108
+ case .rendering(let ctx):
109
+ return [
110
+ "kind": "rendering",
111
+ "url": ctx.url.absoluteString,
112
+ "renderType": ctx.renderType.jsValue,
113
+ "paymentType": ctx.paymentType.jsValue,
114
+ ]
115
+ case .completed: return ["kind": "completed"]
116
+ case .failed(let error):
117
+ return ["kind": "failed", "error": error.toJSDict()]
118
+ }
119
+ }
120
+ }
121
+
122
+ // MARK: - Event → dict
123
+
124
+ extension CheckoutEvent {
125
+ func toJSDict() -> [String: Any] {
126
+ switch self {
127
+ case .stateChanged(let state):
128
+ return ["type": "stateChanged", "state": state.toJSDict()]
129
+ case .checkoutStarted(let intentId):
130
+ return ["type": "checkoutStarted", "intentId": intentId]
131
+ case .loginRequired(let requirements):
132
+ return ["type": "loginRequired", "requirements": codableToJSArray(requirements)]
133
+ case .readyToCheckout:
134
+ return ["type": "readyToCheckout"]
135
+ case .requirementSatisfied(let kind):
136
+ return ["type": "requirementSatisfied", "requirementType": kind.rawValue]
137
+ case .checkoutFinalized(let response):
138
+ return ["type": "checkoutFinalized", "response": codableToJSValue(response)]
139
+ case .renderingStarted(let url, let renderType):
140
+ return ["type": "renderingStarted", "url": url.absoluteString, "renderType": renderType.jsValue]
141
+ case .completed(let checkoutId):
142
+ return ["type": "completed", "checkoutId": checkoutId]
143
+ case .failed(let err):
144
+ return ["type": "failed", "error": err.toJSDict()]
145
+ case .checkoutCancelled:
146
+ return ["type": "cancelled"]
147
+ // Provider-lifecycle events (third-party checkout webviews).
148
+ case .providerReady:
149
+ return ["type": "providerReady"]
150
+ case .paymentAuthorized:
151
+ return ["type": "paymentAuthorized"]
152
+ case .paymentProcessing:
153
+ return ["type": "paymentProcessing"]
154
+ case .paymentCancelled:
155
+ return ["type": "paymentCancelled"]
156
+ case .providerError(let reason):
157
+ return ["type": "providerError", "reason": reason]
158
+ }
159
+ }
160
+ }
161
+
162
+ // MARK: - Codable → JS value / JSON string
163
+
164
+ func codableToJSValue<T: Codable>(_ value: T) -> Any {
165
+ guard let data = try? JSONEncoder().encode(value),
166
+ let obj = try? JSONSerialization.jsonObject(with: data) else {
167
+ return [:]
168
+ }
169
+ return obj
170
+ }
171
+
172
+ func codableToJSArray<T: Codable>(_ values: [T]) -> [Any] {
173
+ (codableToJSValue(values) as? [Any]) ?? []
174
+ }
175
+
176
+ /// Serializes a JS-friendly dict to a JSON string for transport across the
177
+ /// Nitro callback boundary. Returns "{}" on failure (should not happen for the
178
+ /// fixed-shape dicts produced above).
179
+ func jsonString(_ dict: [String: Any]) -> String {
180
+ guard let data = try? JSONSerialization.data(withJSONObject: dict),
181
+ let str = String(data: data, encoding: .utf8) else {
182
+ return "{}"
183
+ }
184
+ return str
185
+ }
@@ -0,0 +1,78 @@
1
+ import Foundation
2
+ import Security
3
+ import SwiftUI
4
+
5
+ actor PreparedIntentRegistry {
6
+ /// Process-wide registry. `getCheckoutRequirements` (on HybridOnramperNitro)
7
+ /// stores the SwiftUI button under a ULID handle; the separate
8
+ /// HybridOnramperCheckoutButton view consumes it by handle. Handles are
9
+ /// unique ULIDs, so a single shared registry is safe across clients.
10
+ static let shared = PreparedIntentRegistry()
11
+
12
+ struct PreparedIntent {
13
+ let button: AnyView
14
+ let createdAt: Date
15
+ var consumed: Bool = false
16
+ }
17
+
18
+ private var entries: [String: PreparedIntent] = [:]
19
+ private let ttl: TimeInterval
20
+
21
+ init(ttl: TimeInterval = 300) {
22
+ self.ttl = ttl
23
+ }
24
+
25
+ func store(_ entry: PreparedIntent) -> String {
26
+ sweepExpired()
27
+ let handle = Self.makeULID()
28
+ entries[handle] = entry
29
+ return handle
30
+ }
31
+
32
+ func consume(_ handle: String) -> PreparedIntent? {
33
+ guard var entry = entries[handle], !entry.consumed else { return nil }
34
+ entry.consumed = true
35
+ entries[handle] = entry
36
+ return entry
37
+ }
38
+
39
+ func drop(_ handle: String) {
40
+ entries.removeValue(forKey: handle)
41
+ }
42
+
43
+ func invalidateAll() {
44
+ entries.removeAll()
45
+ }
46
+
47
+ private func sweepExpired() {
48
+ let now = Date()
49
+ entries = entries.filter { now.timeIntervalSince($0.value.createdAt) < ttl }
50
+ }
51
+
52
+ private static func makeULID() -> String {
53
+ var bytes = [UInt8](repeating: 0, count: 16)
54
+ _ = SecRandomCopyBytes(kSecRandomDefault, 10, &bytes[6])
55
+ let ts = UInt64(Date().timeIntervalSince1970 * 1000)
56
+ for i in 0..<6 {
57
+ bytes[i] = UInt8((ts >> ((5 - i) * 8)) & 0xFF)
58
+ }
59
+ let alphabet = Array("0123456789ABCDEFGHJKMNPQRSTVWXYZ")
60
+ var out = ""
61
+ var bits: UInt64 = 0
62
+ var bitCount = 0
63
+ for byte in bytes {
64
+ bits = (bits << 8) | UInt64(byte)
65
+ bitCount += 8
66
+ while bitCount >= 5 {
67
+ bitCount -= 5
68
+ let idx = Int((bits >> bitCount) & 0x1F)
69
+ out.append(alphabet[idx])
70
+ }
71
+ }
72
+ if bitCount > 0 {
73
+ let idx = Int((bits << (5 - bitCount)) & 0x1F)
74
+ out.append(alphabet[idx])
75
+ }
76
+ return String(out.prefix(26))
77
+ }
78
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.OnramperCheckoutButtonView = void 0;
7
+ var _reactNativeNitroModules = require("react-native-nitro-modules");
8
+ var _OnramperCheckoutButtonConfig = _interopRequireDefault(require("./generated/OnramperCheckoutButtonConfig.json"));
9
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
+ /**
11
+ * Native checkout button. Renders the SwiftUI `OnramperCheckoutButton` that
12
+ * `OnramperClient.getCheckoutRequirements()` prepared, addressed by `intentHandle`.
13
+ * Standard view props (e.g. `style`) are supported via `HybridViewProps`.
14
+ */
15
+ const OnramperCheckoutButtonView = exports.OnramperCheckoutButtonView = (0, _reactNativeNitroModules.getHostComponent)('OnramperCheckoutButton', () => _OnramperCheckoutButtonConfig.default);
16
+ //# sourceMappingURL=OnramperCheckoutButtonView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_reactNativeNitroModules","require","_OnramperCheckoutButtonConfig","_interopRequireDefault","e","__esModule","default","OnramperCheckoutButtonView","exports","getHostComponent","OnramperCheckoutButtonConfig"],"sourceRoot":"../../src","sources":["OnramperCheckoutButtonView.tsx"],"mappings":";;;;;;AAAA,IAAAA,wBAAA,GAAAC,OAAA;AACA,IAAAC,6BAAA,GAAAC,sBAAA,CAAAF,OAAA;AAAyF,SAAAE,uBAAAC,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAKzF;AACA;AACA;AACA;AACA;AACO,MAAMG,0BAA0B,GAAAC,OAAA,CAAAD,0BAAA,GAAG,IAAAE,yCAAgB,EACxD,wBAAwB,EACxB,MAAMC,qCACR,CAAC","ignoreList":[]}