@atomicfi/transact-capacitor 0.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.
@@ -0,0 +1,380 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import AtomicTransact
4
+
5
+ @objc(TransactPluginPlugin)
6
+ public class TransactPluginPlugin: CAPPlugin, CAPBridgedPlugin {
7
+ public let identifier = "TransactPluginPlugin"
8
+ public let jsName = "TransactPlugin"
9
+ public let pluginMethods: [CAPPluginMethod] = [
10
+ CAPPluginMethod(name: "presentTransact", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "presentAction", returnType: CAPPluginReturnPromise),
12
+ CAPPluginMethod(name: "hideTransact", returnType: CAPPluginReturnPromise),
13
+ CAPPluginMethod(name: "resolveDataRequest", returnType: CAPPluginReturnPromise)
14
+ ]
15
+
16
+ private var dataResponseHandler: (([String: Any]?) -> Void)?
17
+
18
+ // MARK: - Environment Parsing
19
+
20
+ private func parseEnvironment(_ environmentData: [String: Any]?) -> TransactEnvironment {
21
+ guard let env = environmentData,
22
+ let environment = env["environment"] as? String else {
23
+ return .production
24
+ }
25
+
26
+ switch environment {
27
+ case "sandbox":
28
+ return .sandbox
29
+ case "custom":
30
+ let transactPath = env["transactPath"] as? String ?? "https://transact.atomicfi.com"
31
+ let apiPath = env["apiPath"] as? String ?? "https://api.atomicfi.com"
32
+ return .custom(transactPath: transactPath, apiPath: apiPath)
33
+ default:
34
+ return .production
35
+ }
36
+ }
37
+
38
+ private func parsePresentationStyle(_ style: String?) -> UIModalPresentationStyle {
39
+ switch style {
40
+ case "fullScreen":
41
+ return .fullScreen
42
+ default:
43
+ return .formSheet
44
+ }
45
+ }
46
+
47
+ // MARK: - presentTransact
48
+
49
+ @objc func presentTransact(_ call: CAPPluginCall) {
50
+ guard let configData = call.getObject("config") else {
51
+ call.reject("Config is required")
52
+ return
53
+ }
54
+
55
+ let environmentData = call.getObject("environment")
56
+ let presentationStyle = call.getString("presentationStyle")
57
+
58
+ DispatchQueue.main.async { [weak self] in
59
+ guard let self = self,
60
+ let source = self.bridge?.viewController else {
61
+ call.reject("Unable to get view controller")
62
+ return
63
+ }
64
+
65
+ let parsedEnvironment = self.parseEnvironment(environmentData)
66
+ let parsedPresentationStyle = self.parsePresentationStyle(presentationStyle)
67
+
68
+ do {
69
+ var json = configData as [String: Any]
70
+
71
+ // Default language to "en" if not provided
72
+ if json["language"] == nil {
73
+ json["language"] = "en"
74
+ }
75
+
76
+ // Add platform info using SDK defaults with -capacitor suffix
77
+ json["platform"] = AtomicConfig.Platform(suffixed: "capacitor").encode()
78
+
79
+ guard let data = try? JSONSerialization.data(withJSONObject: json, options: []) else {
80
+ call.reject("Failed to serialize config")
81
+ return
82
+ }
83
+
84
+ let decoder = JSONDecoder()
85
+ let config = try decoder.decode(AtomicConfig.self, from: data)
86
+
87
+ Atomic.presentTransact(
88
+ from: source,
89
+ config: config,
90
+ environment: parsedEnvironment,
91
+ presentationStyle: parsedPresentationStyle,
92
+ onInteraction: { [weak self] interaction in
93
+ self?.notifyListeners("onInteraction", data: [
94
+ "name": interaction.name,
95
+ "value": interaction.value
96
+ ])
97
+ },
98
+ onDataRequest: { [weak self] request async -> TransactDataResponse? in
99
+ return await withCheckedContinuation { continuation in
100
+ self?.dataResponseHandler = { responseData in
101
+ guard let responseDict = responseData else {
102
+ continuation.resume(returning: nil)
103
+ return
104
+ }
105
+
106
+ do {
107
+ let jsonData = try JSONSerialization.data(withJSONObject: responseDict, options: [])
108
+ let response = try JSONDecoder().decode(TransactDataResponse.self, from: jsonData)
109
+ continuation.resume(returning: response)
110
+ } catch {
111
+ continuation.resume(returning: nil)
112
+ }
113
+ }
114
+
115
+ // Build event data from the request
116
+ var eventData: [String: Any] = [:]
117
+ for (key, value) in request.data {
118
+ eventData[key] = value
119
+ }
120
+ eventData["userId"] = request.userId
121
+ eventData["identifier"] = request.identifier
122
+ eventData["fields"] = request.fields
123
+ if let taskId = request.taskId {
124
+ eventData["taskId"] = taskId
125
+ }
126
+
127
+ self?.notifyListeners("onDataRequest", data: eventData)
128
+ }
129
+ },
130
+ onAuthStatusUpdate: { [weak self] status in
131
+ self?.notifyListeners("onAuthStatusUpdate", data: status.toDictionary())
132
+ },
133
+ onTaskStatusUpdate: { [weak self] status in
134
+ self?.notifyListeners("onTaskStatusUpdate", data: status.toDictionary())
135
+ },
136
+ onLaunch: { [weak self] in
137
+ self?.notifyListeners("onLaunch", data: [:])
138
+ },
139
+ onCompletion: { [weak self] result in
140
+ switch result {
141
+ case .finished(let response):
142
+ let data = self?.sanitizeDictionary(response.data) ?? [:]
143
+ self?.notifyListeners("onFinish", data: data)
144
+ call.resolve(["finished": data])
145
+ case .closed(let response):
146
+ let data = self?.sanitizeDictionary(response.data) ?? [:]
147
+ self?.notifyListeners("onClose", data: data)
148
+ call.resolve(["closed": data])
149
+ case .error:
150
+ call.resolve(["error": "Unknown error"])
151
+ case .transactDismissed:
152
+ call.resolve(["closed": ["reason": "dismissed"]])
153
+ @unknown default:
154
+ call.resolve(["error": "Unknown error"])
155
+ }
156
+ }
157
+ )
158
+ } catch let DecodingError.keyNotFound(key, context) {
159
+ call.reject("Config error: Missing key '\(key.stringValue)' at path: \(context.codingPath.map(\.stringValue).joined(separator: ".")). Debug: \(context.debugDescription)")
160
+ } catch let DecodingError.typeMismatch(type, context) {
161
+ call.reject("Config error: Type mismatch for \(type) at path: \(context.codingPath.map(\.stringValue).joined(separator: ".")). Debug: \(context.debugDescription)")
162
+ } catch let DecodingError.valueNotFound(type, context) {
163
+ call.reject("Config error: Value not found for \(type) at path: \(context.codingPath.map(\.stringValue).joined(separator: ".")). Debug: \(context.debugDescription)")
164
+ } catch {
165
+ call.reject("Config error: \(String(describing: error))")
166
+ }
167
+ }
168
+ }
169
+
170
+ // MARK: - presentAction
171
+
172
+ @objc func presentAction(_ call: CAPPluginCall) {
173
+ guard let id = call.getString("id") else {
174
+ call.reject("id is required")
175
+ return
176
+ }
177
+
178
+ let environmentData = call.getObject("environment")
179
+ let presentationStyle = call.getString("presentationStyle")
180
+
181
+ DispatchQueue.main.async { [weak self] in
182
+ guard let self = self,
183
+ let source = self.bridge?.viewController else {
184
+ call.reject("Unable to get view controller")
185
+ return
186
+ }
187
+
188
+ let parsedEnvironment = self.parseEnvironment(environmentData)
189
+ let parsedPresentationStyle = self.parsePresentationStyle(presentationStyle)
190
+
191
+ // Parse optional theme
192
+ var theme = AtomicConfig.Theme()
193
+ if let themeData = call.getObject("theme") {
194
+ if let jsonData = try? JSONSerialization.data(withJSONObject: themeData, options: []),
195
+ let parsedTheme = try? JSONDecoder().decode(AtomicConfig.Theme.self, from: jsonData) {
196
+ theme = parsedTheme
197
+ }
198
+ }
199
+
200
+ // Parse optional metadata
201
+ var metadata: [String: String]? = nil
202
+ if let metadataData = call.getObject("metadata") {
203
+ metadata = metadataData.compactMapValues { $0 as? String }
204
+ }
205
+
206
+ Atomic.presentAction(
207
+ from: source,
208
+ id: id,
209
+ environment: parsedEnvironment,
210
+ presentationStyle: parsedPresentationStyle,
211
+ theme: theme,
212
+ metadata: metadata,
213
+ onLaunch: { [weak self] in
214
+ self?.notifyListeners("onLaunch", data: [:])
215
+ },
216
+ onAuthStatusUpdate: { [weak self] status in
217
+ self?.notifyListeners("onAuthStatusUpdate", data: status.toDictionary())
218
+ },
219
+ onTaskStatusUpdate: { [weak self] status in
220
+ self?.notifyListeners("onTaskStatusUpdate", data: status.toDictionary())
221
+ },
222
+ onCompletion: { [weak self] result in
223
+ switch result {
224
+ case .finished(let response):
225
+ let data = self?.sanitizeDictionary(response.data) ?? [:]
226
+ self?.notifyListeners("onFinish", data: data)
227
+ call.resolve(["finished": data])
228
+ case .closed(let response):
229
+ let data = self?.sanitizeDictionary(response.data) ?? [:]
230
+ self?.notifyListeners("onClose", data: data)
231
+ call.resolve(["closed": data])
232
+ case .error:
233
+ call.resolve(["error": "Unknown error"])
234
+ case .transactDismissed:
235
+ call.resolve(["closed": ["reason": "dismissed"]])
236
+ @unknown default:
237
+ call.resolve(["error": "Unknown error"])
238
+ }
239
+ }
240
+ )
241
+ }
242
+ }
243
+
244
+ // MARK: - hideTransact
245
+
246
+ @objc func hideTransact(_ call: CAPPluginCall) {
247
+ DispatchQueue.main.async {
248
+ Atomic.hideTransact()
249
+ call.resolve()
250
+ }
251
+ }
252
+
253
+ // MARK: - resolveDataRequest
254
+
255
+ @objc func resolveDataRequest(_ call: CAPPluginCall) {
256
+ guard let handler = dataResponseHandler else {
257
+ call.reject("No active data request")
258
+ return
259
+ }
260
+
261
+ var responseData: [String: Any] = [:]
262
+ if let card = call.getObject("card") {
263
+ responseData["card"] = card
264
+ }
265
+ if let identity = call.getObject("identity") {
266
+ responseData["identity"] = identity
267
+ }
268
+
269
+ handler(responseData.isEmpty ? nil : responseData)
270
+ dataResponseHandler = nil
271
+ call.resolve()
272
+ }
273
+
274
+ // MARK: - Helpers
275
+
276
+ /// Ensures all values in the dictionary are JSON-serializable for Capacitor
277
+ private func sanitizeDictionary(_ dict: [String: Any]) -> [String: Any] {
278
+ var result: [String: Any] = [:]
279
+ for (key, value) in dict {
280
+ if let nested = value as? [String: Any] {
281
+ result[key] = sanitizeDictionary(nested)
282
+ } else if let array = value as? [Any] {
283
+ result[key] = array
284
+ } else if value is String || value is Int || value is Double || value is Bool {
285
+ result[key] = value
286
+ } else if let number = value as? NSNumber {
287
+ result[key] = number
288
+ } else {
289
+ result[key] = "\(value)"
290
+ }
291
+ }
292
+ return result
293
+ }
294
+ }
295
+
296
+ // MARK: - Serialization Extensions
297
+
298
+ extension TransactCompany {
299
+ func toDictionary() -> [String: Any] {
300
+ var dict: [String: Any] = [
301
+ "_id": id,
302
+ "name": name
303
+ ]
304
+ if let branding = branding {
305
+ var brandingDict: [String: Any] = [
306
+ "color": branding.color
307
+ ]
308
+ var logoDict: [String: Any] = [
309
+ "url": branding.logo.url
310
+ ]
311
+ if let bgColor = branding.logo.backgroundColor {
312
+ logoDict["backgroundColor"] = bgColor
313
+ }
314
+ brandingDict["logo"] = logoDict
315
+ dict["branding"] = brandingDict
316
+ }
317
+ return dict
318
+ }
319
+ }
320
+
321
+ extension TransactAuthStatusUpdate {
322
+ func toDictionary() -> [String: Any] {
323
+ return [
324
+ "status": status.rawValue,
325
+ "company": company.toDictionary()
326
+ ]
327
+ }
328
+ }
329
+
330
+ extension TransactTaskStatusUpdate {
331
+ func toDictionary() -> [String: Any] {
332
+ var result: [String: Any] = [
333
+ "taskId": taskId,
334
+ "product": product.rawValue,
335
+ "status": status.rawValue,
336
+ "company": company.toDictionary()
337
+ ]
338
+
339
+ if let failReason = failReason {
340
+ result["failReason"] = failReason
341
+ }
342
+
343
+ if let switchData = switchData {
344
+ var switchMap: [String: Any] = [:]
345
+ let payment = switchData.paymentMethod
346
+ var paymentMap: [String: Any] = [
347
+ "id": payment.id,
348
+ "title": payment.title,
349
+ "type": payment.type.rawValue
350
+ ]
351
+
352
+ if let expiry = payment.expiry { paymentMap["expiry"] = expiry }
353
+ if let brand = payment.brand { paymentMap["brand"] = brand }
354
+ if let lastFour = payment.lastFour { paymentMap["lastFour"] = lastFour }
355
+ if let routingNumber = payment.routingNumber { paymentMap["routingNumber"] = routingNumber }
356
+ if let accountType = payment.accountType { paymentMap["accountType"] = accountType }
357
+ if let lastFourAccount = payment.lastFourAccountNumber { paymentMap["lastFourAccountNumber"] = lastFourAccount }
358
+
359
+ switchMap["paymentMethod"] = paymentMap
360
+ result["switchData"] = switchMap
361
+ }
362
+
363
+ if let depositData = depositData {
364
+ var depositMap: [String: Any] = [:]
365
+ if let accountType = depositData.accountType { depositMap["accountType"] = accountType }
366
+ if let distributionAmount = depositData.distributionAmount { depositMap["distributionAmount"] = distributionAmount }
367
+ if let distributionType = depositData.distributionType { depositMap["distributionType"] = "\(distributionType)" }
368
+ if let lastFour = depositData.lastFour { depositMap["lastFour"] = lastFour }
369
+ if let routingNumber = depositData.routingNumber { depositMap["routingNumber"] = routingNumber }
370
+ if let title = depositData.title { depositMap["title"] = title }
371
+ result["depositData"] = depositMap
372
+ }
373
+
374
+ if let managedBy = managedBy {
375
+ result["managedBy"] = ["company": managedBy.company.toDictionary()]
376
+ }
377
+
378
+ return result
379
+ }
380
+ }
@@ -0,0 +1,14 @@
1
+ import XCTest
2
+ @testable import TransactPluginPlugin
3
+
4
+ class TransactPluginTests: XCTestCase {
5
+ func testPluginMethodsRegistered() {
6
+ let plugin = TransactPluginPlugin()
7
+ let methodNames = plugin.pluginMethods.map { $0.name }
8
+
9
+ XCTAssertTrue(methodNames.contains("presentTransact"))
10
+ XCTAssertTrue(methodNames.contains("presentAction"))
11
+ XCTAssertTrue(methodNames.contains("hideTransact"))
12
+ XCTAssertTrue(methodNames.contains("resolveDataRequest"))
13
+ }
14
+ }
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@atomicfi/transact-capacitor",
3
+ "version": "0.0.1",
4
+ "description": "Capactior plugin to use native sdks from Atomic",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Sources",
14
+ "ios/Tests",
15
+ "Package.swift",
16
+ "AtomicfiTransactCapacitor.podspec"
17
+ ],
18
+ "author": "atomicfi",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/atomicfi/atomic-transact-capacitor.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/atomicfi/atomic-transact-capacitor/issues"
26
+ },
27
+ "keywords": [
28
+ "capacitor",
29
+ "plugin",
30
+ "native"
31
+ ],
32
+ "scripts": {
33
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
34
+ "verify:ios": "xcodebuild -scheme AtomicfiTransactCapacitor -destination generic/platform=iOS",
35
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
36
+ "verify:web": "npm run build",
37
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
38
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
39
+ "eslint": "eslint . --ext ts",
40
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
41
+ "swiftlint": "node-swiftlint",
42
+ "docgen": "docgen --api TransactPluginPlugin --output-readme README.md --output-json dist/docs.json",
43
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
44
+ "clean": "rimraf ./dist",
45
+ "watch": "tsc --watch",
46
+ "prepublishOnly": "npm run build"
47
+ },
48
+ "devDependencies": {
49
+ "@capacitor/android": "^8.0.0",
50
+ "@capacitor/core": "^8.0.0",
51
+ "@capacitor/docgen": "^0.3.1",
52
+ "@capacitor/ios": "^8.0.0",
53
+ "@ionic/eslint-config": "^0.4.0",
54
+ "@ionic/prettier-config": "^4.0.0",
55
+ "@ionic/swiftlint-config": "^2.0.0",
56
+ "eslint": "^8.57.1",
57
+ "prettier": "^3.6.2",
58
+ "prettier-plugin-java": "^2.7.7",
59
+ "rimraf": "^6.1.0",
60
+ "rollup": "^4.53.2",
61
+ "swiftlint": "^2.0.0",
62
+ "typescript": "^5.9.3"
63
+ },
64
+ "peerDependencies": {
65
+ "@capacitor/core": ">=8.0.0"
66
+ },
67
+ "prettier": "@ionic/prettier-config",
68
+ "swiftlint": "@ionic/swiftlint-config",
69
+ "eslintConfig": {
70
+ "extends": "@ionic/eslint-config/recommended"
71
+ },
72
+ "capacitor": {
73
+ "ios": {
74
+ "src": "ios"
75
+ },
76
+ "android": {
77
+ "src": "android"
78
+ }
79
+ }
80
+ }