@icedevml/react-native-host-card-emulation 0.9.1 → 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.
@@ -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
- extension StringProtocol {
5
- func asHexArray() -> [UInt8] {
6
- var startIndex = self.startIndex
7
- return stride(from: 0, to: count, by: 2).compactMap { _ in
8
- let endIndex = index(startIndex, offsetBy: 2, limitedBy: self.endIndex) ?? self.endIndex
9
- defer { startIndex = endIndex }
10
- return UInt8(self[startIndex..<endIndex], radix: 16)
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
- extension Data {
16
- func hexEncodedString() -> String {
17
- let format = "%02hhX"
18
- return map { String(format: format, $0) }.joined()
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
- var cardSession: CardSession? = nil
25
- var cardSessionInvalidated: Bool = true
26
- var presentmentIntent: NFCPresentmentIntentAssertion? = nil
27
- var receivedCardAPDU: CardSession.APDU? = nil
28
- var deviceLockStateReported: Bool = false
58
+ private struct PendingAPDU {
59
+ let id: Int64
60
+ let apdu: CardSession.APDU
61
+ }
29
62
 
30
- var emitOnEvent: ((NSString, NSString?) -> ())? = nil
63
+ private struct EmulationStart {
64
+ let sessionId: String
65
+ let task: Task<Void, Error>
66
+ }
31
67
 
32
- @objc public func setEmitOnEvent(emitOnEvent: @escaping (NSString, NSString?) -> ()) {
33
- self.emitOnEvent = emitOnEvent
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
- if self.presentmentIntent != nil && self.presentmentIntent!.isValid {
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
- self.presentmentIntent = try await NFCPresentmentIntentAssertion.acquire()
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
- reject("err_create_presentment_\(error)", "Failed to acquire NFCPresentmentIntentAssertion: \(error)", nil)
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
- self.presentmentIntent = nil
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 != nil && self.presentmentIntent!.isValid
219
+ return synchronized { self.presentmentIntent?.isValid ?? false }
77
220
  }
78
221
 
79
- @objc public func beginSession(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
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
- guard self.cardSessionInvalidated else {
97
- reject("err_card_session_exists", "CardSession already exists.", nil)
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
- self.cardSession = try await CardSession()
269
+ session = try await CardSession()
103
270
  } catch (let error) {
104
- reject("err_create_card_session_\(error)", "Failed to create CardSession: \(error)", nil)
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.cardSessionInvalidated = false
109
- self.deviceLockStateReported = false
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
- do {
113
- for try await event in cardSession!.eventStream {
114
- switch event {
115
- case .sessionStarted:
116
- cardSession!.alertMessage = String(localized: "Communicating with card reader.")
117
- self.emitOnEvent!("sessionStarted", "")
118
-
119
- case .readerDetected:
120
- self.emitOnEvent!("readerDetected", "")
121
-
122
- case .readerDeselected:
123
- self.emitOnEvent!("readerDeselected", "")
124
-
125
- case .received(let cardAPDU):
126
- if !self.deviceLockStateReported {
127
- // it's impossible to communicate over HCE while the device is locked
128
- self.deviceLockStateReported = true
129
- self.emitOnEvent!("deviceUnlocked", "")
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
- self.receivedCardAPDU = cardAPDU
133
- self.emitOnEvent!("received", NSString(string:cardAPDU.payload.hexEncodedString()))
326
+ case .received(let cardAPDU):
327
+ let apduId = synchronized { () -> Int64? in
328
+ guard self.currentSessionId == sessionId else {
329
+ return nil
330
+ }
134
331
 
135
- case .sessionInvalidated(let reason):
136
- self.cardSessionInvalidated = true
137
- self.emitOnEvent!("sessionInvalidated", "\(reason)" as NSString)
138
- self.cardSession = nil
332
+ self.lastAPDUId += 1
333
+ self.pendingAPDU = PendingAPDU(id: self.lastAPDUId, apdu: cardAPDU)
334
+ return self.lastAPDUId
335
+ }
139
336
 
140
- @unknown default:
141
- fatalError("Received unknown CardSession event.")
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 setSessionAlertMessage(message: NSString) {
151
- self.cardSession?.alertMessage = String(message)
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
- @objc public func invalidateSession() {
155
- self.cardSession?.invalidate()
156
- self.cardSession = nil
157
- self.cardSessionInvalidated = true
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 isSessionRunning() -> Bool {
161
- return self.cardSession != nil && !self.cardSessionInvalidated
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 isHCERunning(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
427
+ @objc public func startEmulation(sessionId: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
428
+ let sessionId = String(sessionId)
429
+
165
430
  Task.init {
166
- guard self.cardSession != nil else {
167
- return resolve(false)
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
- let isRunning = await self.cardSession!.isEmulationInProgress
171
- resolve(isRunning)
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 respondAPDU(handle: NSString?, rapdu: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
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
- guard handle == nil else {
178
- reject("err_illegal_state", "The provided handle is invalid.", nil)
179
- return
180
- }
480
+ let session = self.synchronized { () -> CardSession? in
481
+ let session = self.sessionIfCurrent(sessionId)
181
482
 
182
- guard let capdu = self.receivedCardAPDU else {
183
- reject("err_no_apdu_received", "There is no received APDU to respond to.", nil)
184
- return
483
+ if session != nil {
484
+ self.pendingAPDU = nil
485
+ }
486
+
487
+ return session
185
488
  }
186
489
 
187
- do {
188
- try await capdu.respond(response: Data.init(String(rapdu).asHexArray()))
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 startHCE(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
500
+ @objc public func isEmulationRunning(sessionId: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
501
+ let sessionId = String(sessionId)
502
+
199
503
  Task.init {
200
- do {
201
- try await self.cardSession?.startEmulation()
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(nil)
509
+ resolve(await session.isEmulationInProgress)
208
510
  }
209
511
  }
210
512
 
211
- @objc public func stopHCE(status: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
212
- Task.init {
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
- if (sStatus == "success") {
216
- await self.cardSession?.stopEmulation(status: .success)
217
- } else {
218
- await self.cardSession?.stopEmulation(status: .failure)
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)