@icedevml/react-native-host-card-emulation 0.9.2 → 1.0.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.
- package/Package.swift +87 -0
- package/README.md +216 -152
- package/RTNHCEModuleIOS.podspec +1 -1
- package/android/build.gradle +5 -17
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/BinaryUtils.java +11 -5
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEManager.java +558 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEService.java +22 -299
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +232 -166
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidPackage.java +5 -4
- package/ios/RTNHCEModuleIOS.mm +69 -48
- package/ios/RTNHCEModuleIOSImpl.swift +429 -100
- package/js/NativeHCEModule.ts +48 -130
- package/js/background.ts +168 -0
- package/js/index.ts +49 -0
- package/js/internal.ts +218 -0
- package/js/session.ts +192 -0
- package/js/types.ts +179 -0
- package/package.json +19 -3
- package/react-native-spm-prefix.h +10 -0
- package/android/src/main/AndroidMainfest.xml +0 -3
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEServiceCallback.java +0 -7
- package/js/hceBackground.ts +0 -68
|
@@ -1,38 +1,158 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import CoreNFC
|
|
3
|
+
#if canImport(React)
|
|
4
|
+
// SwiftPM has no bridging header, RCTPromiseResolveBlock/RCTPromiseRejectBlock come from the React module
|
|
5
|
+
import React
|
|
6
|
+
#endif
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
enum HexCoding {
|
|
9
|
+
private static let hexDigits = Array("0123456789ABCDEF".utf8)
|
|
10
|
+
|
|
11
|
+
static func encode(_ data: Data) -> String {
|
|
12
|
+
var out = [UInt8]()
|
|
13
|
+
out.reserveCapacity(data.count * 2)
|
|
14
|
+
|
|
15
|
+
for byte in data {
|
|
16
|
+
out.append(hexDigits[Int(byte >> 4)])
|
|
17
|
+
out.append(hexDigits[Int(byte & 0x0F)])
|
|
11
18
|
}
|
|
19
|
+
|
|
20
|
+
return String(decoding: out, as: UTF8.self)
|
|
12
21
|
}
|
|
13
|
-
}
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
func
|
|
17
|
-
let
|
|
18
|
-
|
|
23
|
+
/// Strict decoding, returns nil for odd length or non-hex characters.
|
|
24
|
+
static func decode(_ string: String) -> Data? {
|
|
25
|
+
let chars = Array(string.utf8)
|
|
26
|
+
|
|
27
|
+
guard chars.count % 2 == 0 else {
|
|
28
|
+
return nil
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func nibble(_ c: UInt8) -> UInt8? {
|
|
32
|
+
switch c {
|
|
33
|
+
case 0x30...0x39: return c - 0x30
|
|
34
|
+
case 0x41...0x46: return c - 0x41 + 10
|
|
35
|
+
case 0x61...0x66: return c - 0x61 + 10
|
|
36
|
+
default: return nil
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var data = Data(capacity: chars.count / 2)
|
|
41
|
+
var i = 0
|
|
42
|
+
|
|
43
|
+
while i < chars.count {
|
|
44
|
+
guard let hi = nibble(chars[i]), let lo = nibble(chars[i + 1]) else {
|
|
45
|
+
return nil
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
data.append((hi << 4) | lo)
|
|
49
|
+
i += 2
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return data
|
|
19
53
|
}
|
|
20
54
|
}
|
|
21
55
|
|
|
22
56
|
@available(iOS 17.4, *)
|
|
23
57
|
@objc public class RTNHCEModuleIOSImpl: NSObject {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
var deviceLockStateReported: Bool = false
|
|
58
|
+
private struct PendingAPDU {
|
|
59
|
+
let id: Int64
|
|
60
|
+
let apdu: CardSession.APDU
|
|
61
|
+
}
|
|
29
62
|
|
|
30
|
-
|
|
63
|
+
private struct EmulationStart {
|
|
64
|
+
let sessionId: String
|
|
65
|
+
let task: Task<Void, Error>
|
|
66
|
+
}
|
|
31
67
|
|
|
32
|
-
|
|
33
|
-
|
|
68
|
+
// how long beginSession() waits for the previous CardSession to end
|
|
69
|
+
private static let previousSessionEndTimeout: UInt64 = 5_000_000_000
|
|
70
|
+
private static let previousSessionPollInterval: UInt64 = 50_000_000
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
All the mutable state below is guarded by `lock`. It is accessed concurrently from
|
|
74
|
+
the TurboModule method queue, the JS thread (synchronous methods) and the Swift
|
|
75
|
+
concurrency thread pool (Tasks). Never hold the lock across an `await`.
|
|
76
|
+
*/
|
|
77
|
+
private let lock = NSLock()
|
|
78
|
+
private var currentSessionId: String? = nil
|
|
79
|
+
private var currentSession: CardSession? = nil
|
|
80
|
+
private var startingSessionId: String? = nil
|
|
81
|
+
private var startingSessionInvalidateRequested: Bool = false
|
|
82
|
+
private var pendingAPDU: PendingAPDU? = nil
|
|
83
|
+
private var lastAPDUId: Int64 = 0
|
|
84
|
+
// the number of event loops that are still running, including the ones of invalidated sessions
|
|
85
|
+
private var runningEventLoops: Int = 0
|
|
86
|
+
private var emulationStart: EmulationStart? = nil
|
|
87
|
+
private var presentmentIntent: NFCPresentmentIntentAssertion? = nil
|
|
88
|
+
private var presentmentIntentAcquiring: Bool = false
|
|
89
|
+
private var emitOnEvent: ((NSDictionary) -> ())? = nil
|
|
90
|
+
|
|
91
|
+
private func synchronized<T>(_ body: () throws -> T) rethrows -> T {
|
|
92
|
+
lock.lock()
|
|
93
|
+
defer { lock.unlock() }
|
|
94
|
+
return try body()
|
|
34
95
|
}
|
|
35
96
|
|
|
97
|
+
private func emit(_ sessionId: String, _ type: String, apduId: Int64 = 0, command: String = "", reason: String = "") {
|
|
98
|
+
let callback = synchronized { self.emitOnEvent }
|
|
99
|
+
let payload: [String: Any] = [
|
|
100
|
+
"sessionId": sessionId,
|
|
101
|
+
"type": type,
|
|
102
|
+
"apduId": NSNumber(value: Double(apduId)),
|
|
103
|
+
"command": command,
|
|
104
|
+
// it's impossible to communicate over HCE while the device is locked
|
|
105
|
+
"deviceLocked": NSNumber(value: false),
|
|
106
|
+
"reason": reason,
|
|
107
|
+
]
|
|
108
|
+
callback?(payload as NSDictionary)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Must be called while holding the lock.
|
|
112
|
+
private func sessionIfCurrent(_ sessionId: String) -> CardSession? {
|
|
113
|
+
return currentSessionId == sessionId ? currentSession : nil
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Only a single CardSession may exist at a time, so wait until the event loops of the previous ones have ended.
|
|
117
|
+
private func waitForPreviousSessionsToEnd() async -> Bool {
|
|
118
|
+
var waited: UInt64 = 0
|
|
119
|
+
|
|
120
|
+
while synchronized({ self.runningEventLoops > 0 }) {
|
|
121
|
+
if waited >= Self.previousSessionEndTimeout {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try? await Task.sleep(nanoseconds: Self.previousSessionPollInterval)
|
|
126
|
+
waited += Self.previousSessionPollInterval
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@objc public func setEmitOnEvent(emitOnEvent: @escaping (NSDictionary) -> ()) {
|
|
133
|
+
synchronized { self.emitOnEvent = emitOnEvent }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@objc public func getCapabilities(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
137
|
+
Task.init {
|
|
138
|
+
let hceSupported = CardSession.isSupported
|
|
139
|
+
let nfcEnabled = NFCReaderSession.readingAvailable
|
|
140
|
+
let eligible = hceSupported ? await CardSession.isEligible : false
|
|
141
|
+
|
|
142
|
+
let capabilities: [String: Any] = [
|
|
143
|
+
"platform": "ios",
|
|
144
|
+
"hceSupported": hceSupported,
|
|
145
|
+
"nfcEnabled": nfcEnabled,
|
|
146
|
+
"eligible": eligible,
|
|
147
|
+
"backgroundSupported": false,
|
|
148
|
+
"presentmentSupported": hceSupported,
|
|
149
|
+
]
|
|
150
|
+
resolve(capabilities as NSDictionary)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// MARK: - Exclusive NFC access
|
|
155
|
+
|
|
36
156
|
@objc public func acquireExclusiveNFC(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
37
157
|
Task.init {
|
|
38
158
|
guard NFCReaderSession.readingAvailable else {
|
|
@@ -50,17 +170,34 @@ extension Data {
|
|
|
50
170
|
return
|
|
51
171
|
}
|
|
52
172
|
|
|
53
|
-
|
|
173
|
+
// reserve the acquisition atomically, so that concurrent calls can't both pass the check
|
|
174
|
+
let (canAcquire, previousIntent) = self.synchronized { () -> (Bool, NFCPresentmentIntentAssertion?) in
|
|
175
|
+
if self.presentmentIntentAcquiring || (self.presentmentIntent?.isValid ?? false) {
|
|
176
|
+
return (false, nil)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let previous = self.presentmentIntent
|
|
180
|
+
self.presentmentIntent = nil
|
|
181
|
+
self.presentmentIntentAcquiring = true
|
|
182
|
+
return (true, previous)
|
|
183
|
+
}
|
|
184
|
+
// the expired assertion (if any) is released here, outside of the lock
|
|
185
|
+
_ = previousIntent
|
|
186
|
+
|
|
187
|
+
guard canAcquire else {
|
|
54
188
|
reject("err_already_exists", "NFCPresentmentIntentAssertion already exists and is valid.", nil)
|
|
55
189
|
return
|
|
56
190
|
}
|
|
57
191
|
|
|
58
|
-
self.presentmentIntent = nil
|
|
59
|
-
|
|
60
192
|
do {
|
|
61
|
-
|
|
193
|
+
let intent = try await NFCPresentmentIntentAssertion.acquire()
|
|
194
|
+
self.synchronized {
|
|
195
|
+
self.presentmentIntent = intent
|
|
196
|
+
self.presentmentIntentAcquiring = false
|
|
197
|
+
}
|
|
62
198
|
} catch (let error) {
|
|
63
|
-
|
|
199
|
+
self.synchronized { self.presentmentIntentAcquiring = false }
|
|
200
|
+
reject("err_acquire_presentment", "Failed to acquire NFCPresentmentIntentAssertion: \(error)", nil)
|
|
64
201
|
return
|
|
65
202
|
}
|
|
66
203
|
|
|
@@ -69,14 +206,24 @@ extension Data {
|
|
|
69
206
|
}
|
|
70
207
|
|
|
71
208
|
@objc public func releaseExclusiveNFC() {
|
|
72
|
-
|
|
209
|
+
let previousIntent = synchronized { () -> NFCPresentmentIntentAssertion? in
|
|
210
|
+
let previous = self.presentmentIntent
|
|
211
|
+
self.presentmentIntent = nil
|
|
212
|
+
return previous
|
|
213
|
+
}
|
|
214
|
+
// the assertion is released here, outside of the lock
|
|
215
|
+
_ = previousIntent
|
|
73
216
|
}
|
|
74
217
|
|
|
75
218
|
@objc public func isExclusiveNFC() -> Bool {
|
|
76
|
-
return self.presentmentIntent
|
|
219
|
+
return synchronized { self.presentmentIntent?.isValid ?? false }
|
|
77
220
|
}
|
|
78
221
|
|
|
79
|
-
|
|
222
|
+
// MARK: - Session
|
|
223
|
+
|
|
224
|
+
@objc public func beginSession(sessionId: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
225
|
+
let sessionId = String(sessionId)
|
|
226
|
+
|
|
80
227
|
Task.init {
|
|
81
228
|
guard NFCReaderSession.readingAvailable else {
|
|
82
229
|
reject("err_nfc_reader_not_available", "Preflight check failed: NFCReaderSession.readingAvailable is false", nil)
|
|
@@ -93,129 +240,311 @@ extension Data {
|
|
|
93
240
|
return
|
|
94
241
|
}
|
|
95
242
|
|
|
96
|
-
|
|
97
|
-
|
|
243
|
+
// reserve the session slot atomically, so that concurrent calls can't both pass the check
|
|
244
|
+
let canStart = self.synchronized { () -> Bool in
|
|
245
|
+
if self.currentSession != nil || self.startingSessionId != nil {
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
self.startingSessionId = sessionId
|
|
250
|
+
self.startingSessionInvalidateRequested = false
|
|
251
|
+
return true
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
guard canStart else {
|
|
255
|
+
reject("err_session_exists", "A session already exists.", nil)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// the previous session might still be ending, e.g. if it was invalidated right before this call
|
|
260
|
+
guard await self.waitForPreviousSessionsToEnd() else {
|
|
261
|
+
self.synchronized { self.startingSessionId = nil }
|
|
262
|
+
reject("err_session_exists", "The previous session is still being closed.", nil)
|
|
98
263
|
return
|
|
99
264
|
}
|
|
100
265
|
|
|
266
|
+
let session: CardSession
|
|
267
|
+
|
|
101
268
|
do {
|
|
102
|
-
|
|
269
|
+
session = try await CardSession()
|
|
103
270
|
} catch (let error) {
|
|
104
|
-
|
|
271
|
+
self.synchronized { self.startingSessionId = nil }
|
|
272
|
+
reject("err_create_card_session", "Failed to create CardSession: \(error)", nil)
|
|
105
273
|
return
|
|
106
274
|
}
|
|
107
275
|
|
|
108
|
-
self.
|
|
109
|
-
|
|
276
|
+
let invalidateRequested = self.synchronized { () -> Bool in
|
|
277
|
+
self.startingSessionId = nil
|
|
278
|
+
// the event loop below runs in both cases
|
|
279
|
+
self.runningEventLoops += 1
|
|
280
|
+
|
|
281
|
+
if self.startingSessionInvalidateRequested {
|
|
282
|
+
// invalidateSession() was called while the session was being created
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
self.currentSessionId = sessionId
|
|
287
|
+
self.currentSession = session
|
|
288
|
+
self.pendingAPDU = nil
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if invalidateRequested {
|
|
293
|
+
session.invalidate()
|
|
294
|
+
}
|
|
295
|
+
|
|
110
296
|
resolve(nil)
|
|
297
|
+
await self.runEventLoop(sessionId: sessionId, session: session)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
111
300
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
301
|
+
/*
|
|
302
|
+
Process events of the given session. The loop always operates on its own session object,
|
|
303
|
+
as the current session might be cleared or replaced by a newer one at any time.
|
|
304
|
+
*/
|
|
305
|
+
private func runEventLoop(sessionId: String, session: CardSession) async {
|
|
306
|
+
var invalidationReason: String? = nil
|
|
307
|
+
|
|
308
|
+
do {
|
|
309
|
+
for try await event in session.eventStream {
|
|
310
|
+
switch event {
|
|
311
|
+
case .sessionStarted:
|
|
312
|
+
session.alertMessage = String(localized: "Communicating with card reader.")
|
|
313
|
+
emit(sessionId, "sessionStarted")
|
|
314
|
+
|
|
315
|
+
case .readerDetected:
|
|
316
|
+
emit(sessionId, "readerDetected")
|
|
317
|
+
|
|
318
|
+
case .readerDeselected:
|
|
319
|
+
synchronized {
|
|
320
|
+
if self.currentSessionId == sessionId {
|
|
321
|
+
self.pendingAPDU = nil
|
|
130
322
|
}
|
|
323
|
+
}
|
|
324
|
+
emit(sessionId, "readerLost")
|
|
131
325
|
|
|
132
|
-
|
|
133
|
-
|
|
326
|
+
case .received(let cardAPDU):
|
|
327
|
+
let apduId = synchronized { () -> Int64? in
|
|
328
|
+
guard self.currentSessionId == sessionId else {
|
|
329
|
+
return nil
|
|
330
|
+
}
|
|
134
331
|
|
|
135
|
-
|
|
136
|
-
self.
|
|
137
|
-
self.
|
|
138
|
-
|
|
332
|
+
self.lastAPDUId += 1
|
|
333
|
+
self.pendingAPDU = PendingAPDU(id: self.lastAPDUId, apdu: cardAPDU)
|
|
334
|
+
return self.lastAPDUId
|
|
335
|
+
}
|
|
139
336
|
|
|
140
|
-
|
|
141
|
-
|
|
337
|
+
guard let apduId = apduId else {
|
|
338
|
+
// C-APDU of a session that was already invalidated, nobody can respond to it
|
|
339
|
+
continue
|
|
142
340
|
}
|
|
341
|
+
|
|
342
|
+
emit(sessionId, "apdu", apduId: apduId, command: HexCoding.encode(cardAPDU.payload))
|
|
343
|
+
|
|
344
|
+
case .sessionInvalidated(let reason):
|
|
345
|
+
invalidationReason = "\(reason)"
|
|
346
|
+
|
|
347
|
+
@unknown default:
|
|
348
|
+
// don't crash on event types introduced by future iOS versions
|
|
349
|
+
NSLog("RTNHCEModuleIOS: ignoring unknown CardSession event: %@", String(describing: event))
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if invalidationReason != nil {
|
|
353
|
+
break
|
|
143
354
|
}
|
|
144
|
-
} catch (let err) {
|
|
145
|
-
self.emitOnEvent!("internalError", "\(err)" as NSString)
|
|
146
355
|
}
|
|
356
|
+
} catch (let error) {
|
|
357
|
+
invalidationReason = "internalError: \(error)"
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// the event stream has ended, make sure that the module doesn't stay stuck with a dead session
|
|
361
|
+
let wasCurrent = synchronized { () -> Bool in
|
|
362
|
+
guard self.currentSessionId == sessionId else {
|
|
363
|
+
return false
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
self.currentSessionId = nil
|
|
367
|
+
self.currentSession = nil
|
|
368
|
+
self.pendingAPDU = nil
|
|
369
|
+
return true
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if wasCurrent {
|
|
373
|
+
session.invalidate()
|
|
147
374
|
}
|
|
375
|
+
|
|
376
|
+
// only now a new CardSession can be created
|
|
377
|
+
synchronized { self.runningEventLoops -= 1 }
|
|
378
|
+
emit(sessionId, "sessionInvalidated", reason: invalidationReason ?? "unknown")
|
|
148
379
|
}
|
|
149
380
|
|
|
150
|
-
@objc public func
|
|
151
|
-
|
|
381
|
+
@objc public func invalidateSession(sessionId: NSString) {
|
|
382
|
+
let sessionId = String(sessionId)
|
|
383
|
+
|
|
384
|
+
let session = synchronized { () -> CardSession? in
|
|
385
|
+
if self.startingSessionId == sessionId {
|
|
386
|
+
self.startingSessionInvalidateRequested = true
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
guard self.currentSessionId == sessionId else {
|
|
390
|
+
return nil
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let current = self.currentSession
|
|
394
|
+
self.currentSessionId = nil
|
|
395
|
+
self.currentSession = nil
|
|
396
|
+
self.pendingAPDU = nil
|
|
397
|
+
return current
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
session?.invalidate()
|
|
152
401
|
}
|
|
153
402
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
403
|
+
/// Called when the native module is being torn down (e.g. JS reload).
|
|
404
|
+
@objc public func invalidateAll() {
|
|
405
|
+
let session = synchronized { () -> CardSession? in
|
|
406
|
+
if self.startingSessionId != nil {
|
|
407
|
+
self.startingSessionInvalidateRequested = true
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let current = self.currentSession
|
|
411
|
+
self.currentSessionId = nil
|
|
412
|
+
self.currentSession = nil
|
|
413
|
+
self.pendingAPDU = nil
|
|
414
|
+
self.emitOnEvent = nil
|
|
415
|
+
return current
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
session?.invalidate()
|
|
419
|
+
releaseExclusiveNFC()
|
|
158
420
|
}
|
|
159
421
|
|
|
160
|
-
@objc public func
|
|
161
|
-
|
|
422
|
+
@objc public func setSessionAlertMessage(sessionId: NSString, message: NSString) {
|
|
423
|
+
let session = synchronized { self.sessionIfCurrent(String(sessionId)) }
|
|
424
|
+
session?.alertMessage = String(message)
|
|
162
425
|
}
|
|
163
426
|
|
|
164
|
-
@objc public func
|
|
427
|
+
@objc public func startEmulation(sessionId: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
428
|
+
let sessionId = String(sessionId)
|
|
429
|
+
|
|
165
430
|
Task.init {
|
|
166
|
-
|
|
167
|
-
|
|
431
|
+
// concurrent calls share a single attempt, so that the emulation is never started twice
|
|
432
|
+
let task = self.synchronized { () -> Task<Void, Error>? in
|
|
433
|
+
guard let session = self.sessionIfCurrent(sessionId) else {
|
|
434
|
+
return nil
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if let start = self.emulationStart, start.sessionId == sessionId {
|
|
438
|
+
return start.task
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let task = Task<Void, Error> {
|
|
442
|
+
if await session.isEmulationInProgress {
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try await session.startEmulation()
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
self.emulationStart = EmulationStart(sessionId: sessionId, task: task)
|
|
450
|
+
return task
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
guard let task = task else {
|
|
454
|
+
reject("err_no_session", "The session is not active.", nil)
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let result = await task.result
|
|
459
|
+
|
|
460
|
+
self.synchronized {
|
|
461
|
+
if self.emulationStart?.task == task {
|
|
462
|
+
self.emulationStart = nil
|
|
463
|
+
}
|
|
168
464
|
}
|
|
169
465
|
|
|
170
|
-
|
|
171
|
-
|
|
466
|
+
switch result {
|
|
467
|
+
case .success:
|
|
468
|
+
resolve(nil)
|
|
469
|
+
case .failure(let error):
|
|
470
|
+
reject("err_start_emulation", "Error trying to start emulation. \(error)", nil)
|
|
471
|
+
}
|
|
172
472
|
}
|
|
173
473
|
}
|
|
174
474
|
|
|
175
|
-
@objc public func
|
|
475
|
+
@objc public func stopEmulation(sessionId: NSString, status: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
476
|
+
let sessionId = String(sessionId)
|
|
477
|
+
let success = String(status) == "success"
|
|
478
|
+
|
|
176
479
|
Task.init {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return
|
|
180
|
-
}
|
|
480
|
+
let session = self.synchronized { () -> CardSession? in
|
|
481
|
+
let session = self.sessionIfCurrent(sessionId)
|
|
181
482
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
483
|
+
if session != nil {
|
|
484
|
+
self.pendingAPDU = nil
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return session
|
|
185
488
|
}
|
|
186
489
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
} catch (let error) {
|
|
190
|
-
reject("err_apdu_respond", "Error trying to respond to APDU. \(error)", nil)
|
|
490
|
+
guard let session = session else {
|
|
491
|
+
reject("err_no_session", "The session is not active.", nil)
|
|
191
492
|
return
|
|
192
493
|
}
|
|
193
494
|
|
|
495
|
+
await session.stopEmulation(status: success ? .success : .failure)
|
|
194
496
|
resolve(nil)
|
|
195
497
|
}
|
|
196
498
|
}
|
|
197
499
|
|
|
198
|
-
@objc public func
|
|
500
|
+
@objc public func isEmulationRunning(sessionId: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
501
|
+
let sessionId = String(sessionId)
|
|
502
|
+
|
|
199
503
|
Task.init {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
} catch(let error) {
|
|
203
|
-
reject("err_start_emulation", "Error trying to start emulation. \(error)", nil)
|
|
504
|
+
guard let session = self.synchronized({ self.sessionIfCurrent(sessionId) }) else {
|
|
505
|
+
resolve(false)
|
|
204
506
|
return
|
|
205
507
|
}
|
|
206
508
|
|
|
207
|
-
resolve(
|
|
509
|
+
resolve(await session.isEmulationInProgress)
|
|
208
510
|
}
|
|
209
511
|
}
|
|
210
512
|
|
|
211
|
-
@objc public func
|
|
212
|
-
|
|
213
|
-
let sStatus = String(status)
|
|
513
|
+
@objc public func respondApdu(sessionId: NSString, apduId: Double, rapdu: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
514
|
+
let sessionId = String(sessionId)
|
|
214
515
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
516
|
+
// Int64(_:) would crash on NaN, infinity or an out of range value
|
|
517
|
+
guard let apduId = Int64(exactly: apduId) else {
|
|
518
|
+
reject("err_invalid_argument", "The APDU identifier must be an integer.", nil)
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
guard let response = HexCoding.decode(String(rapdu)), response.count >= 2 else {
|
|
523
|
+
reject("err_invalid_rapdu", "The R-APDU must be a hex-encoded string of at least 2 bytes.", nil)
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// take the C-APDU atomically, so it's responded to at most once
|
|
528
|
+
let pending = synchronized { () -> PendingAPDU? in
|
|
529
|
+
guard self.currentSessionId == sessionId, let pending = self.pendingAPDU, pending.id == apduId else {
|
|
530
|
+
return nil
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
self.pendingAPDU = nil
|
|
534
|
+
return pending
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
guard let pending = pending else {
|
|
538
|
+
reject("err_stale_apdu", "The C-APDU is not awaiting a response (reader lost or session closed).", nil)
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
Task.init {
|
|
543
|
+
do {
|
|
544
|
+
try await pending.apdu.respond(response: response)
|
|
545
|
+
} catch (let error) {
|
|
546
|
+
reject("err_apdu_respond", "Error trying to respond to APDU. \(error)", nil)
|
|
547
|
+
return
|
|
219
548
|
}
|
|
220
549
|
|
|
221
550
|
resolve(nil)
|