@@ -21,7 +21,7 @@ import OSLog
21
21
///
22
22
/// The NFCConnection is short lived and should be closed as soon as the commands sent to the YubiKey have finished processing. It is up to the user of
23
23
/// the connection to close it when it no longer is needed. As long as the connection is open the NFC modal will cover the lower part of the iPhone screen.
24
- /// In addition to the ``close(error:)`` method defined in the Connection protocol the NFCConnection has an additional ``close(message :)``
24
+ /// In addition to the ``close(error:)`` method defined in the Connection protocol the NFCConnection has an additional ``close(success :)``
25
25
/// method that will close the connection and set the alertMessage of the NFC alert to the provided message.
26
26
///
27
27
/// > Note: NFC is only supported on iPhones from iPhone 6 and forward. It will not work on iPads since there's no NFC chip in these devices.
@@ -77,7 +77,9 @@ public struct NFCConnection: Connection, Sendable {
77
77
78
78
// @TraceScope
79
79
public func close( message: String ? = nil ) async {
80
- trace ( message: " NFCConnection.close(message:) – closing with error msg: \( String ( describing: message) ) " )
80
+ trace (
81
+ message: " NFCConnection.close(message:) – closing with success msg: \( String ( describing: message) ) "
82
+ )
81
83
await NFCConnectionManager . shared. stop ( with: . success( message) )
82
84
}
83
85
@@ -164,8 +166,6 @@ private final actor NFCConnectionManager: NSObject {
164
166
private override init ( ) { super. init ( ) }
165
167
166
168
private var isEstablishing : Bool = false
167
- private var connection : Promise < NFCConnection > ? = nil
168
- private var didCloseConnection : Promise < Error ? > ? = nil
169
169
170
170
private var currentState = State . inactive
171
171
private func set( state: State ) {
@@ -183,13 +183,14 @@ private final actor NFCConnectionManager: NSObject {
183
183
func didClose( for connection: NFCConnection ) async throws {
184
184
trace ( message: " Manager.didClose(for:) – tracking closure for tag \( connection. tag) " )
185
185
186
- guard let tag = currentState. tag,
187
- connection. tag == . init( tag. identifier) ,
188
- let didCloseConnection
189
- else { return }
190
-
191
- if let error = try await didCloseConnection. value ( ) {
192
- throw error
186
+ switch currentState {
187
+ case . inactive, . scanning:
188
+ return
189
+ case let . connected( state) :
190
+ guard let tag = currentState. tag, connection. tag == . init( tag. identifier) else { return }
191
+ if let error = try await state. didCloseConnection. value ( ) {
192
+ throw error
193
+ }
193
194
}
194
195
}
195
196
@@ -229,55 +230,79 @@ private final actor NFCConnectionManager: NSObject {
229
230
// Close the previous connection before establishing a new one
230
231
switch currentState {
231
232
case . inactive:
233
+ // lets continue
232
234
break
233
- case . connected, . scanning:
234
- await stop ( with: . success( nil ) )
235
+ case . scanning, . connected:
236
+ // invalidate and continue
237
+ await invalidate ( )
235
238
}
236
239
237
240
// Start polling
238
241
guard let session = NFCTagReaderSession ( pollingOption: [ . iso14443] , delegate: self , queue: nil ) else {
239
242
throw NFCConnectionError . failedToPoll
240
243
}
244
+
245
+ let connection : Promise < NFCConnection > = . init( )
246
+ currentState = . scanning( . init( session: session, connection: connection) )
247
+
241
248
if let alertMessage { session. alertMessage = alertMessage }
242
- currentState = . scanning( session)
243
- connection = . init( )
244
249
session. begin ( )
245
250
246
- return try await connection! . value ( )
251
+ return try await connection. value ( )
247
252
}
248
253
249
254
// @TraceScope
250
255
func stop( with result: Result < String ? , Error > ) async {
251
256
trace ( message: " Manager.stop(with:) - result: \( String ( describing: result) ) " )
252
257
253
258
switch result {
254
- case . success( nil ) , . failure:
255
- currentState. session? . invalidate ( )
256
- case let . success( errorMessage) :
257
- currentState. session? . invalidate ( errorMessage: errorMessage!)
259
+ case let . failure( error) :
260
+ currentState. session? . invalidate ( errorMessage: error. localizedDescription)
261
+ case let . success( message) :
262
+ if let message = message {
263
+ currentState. session? . alertMessage = message
264
+ }
258
265
}
259
266
260
- currentState. session? . invalidate ( )
261
- // Workaround for the NFC session being active for an additional 4 seconds after
262
- // invalidate() has been called on the session.
263
- try ? await Task . sleep ( nanoseconds: 5_000_000_000 )
264
267
switch result {
265
268
case . success:
266
- await didCloseConnection ? . fulfill ( nil )
269
+ await invalidate ( )
267
270
case let . failure( error) :
268
- await didCloseConnection ? . fulfill ( error)
271
+ await invalidate ( error : error)
269
272
}
270
- currentState = . inactive
271
- connection = nil
272
- didCloseConnection = nil
273
273
}
274
274
275
275
// @TraceScope
276
276
func connected( session: NFCTagReaderSession , tag: NFCISO7816Tag ) async {
277
277
trace ( message: " Manager.connected(session:tag:) - tag: \( String ( describing: tag. identifier) ) " )
278
- didCloseConnection = . init( )
279
- currentState = . connected( session, tag)
280
- await connection? . fulfill ( . init( tag: . init( tag. identifier) ) )
278
+
279
+ guard let promise = currentState. connectionPromise else {
280
+ await invalidate ( )
281
+ return
282
+ }
283
+
284
+ let connection : NFCConnection = . init( tag: . init( tag. identifier) )
285
+ currentState = . connected(
286
+ . init(
287
+ session: session,
288
+ tag: tag,
289
+ connection: connection,
290
+ didCloseConnection: . init( )
291
+ )
292
+ )
293
+
294
+ await promise. fulfill ( connection)
295
+ }
296
+
297
+ private func invalidate( error: Error ? = nil ) async {
298
+ currentState. session? . invalidate ( )
299
+
300
+ // Workaround for the NFC session being active for an additional 4 seconds after
301
+ // invalidate() has been called on the session.
302
+ try ? await Task . sleep ( for: . seconds( 5 ) )
303
+ await currentState. didCloseConnection? . fulfill ( error)
304
+ await currentState. connectionPromise? . cancel ( with: error ?? ConnectionError . cancelled)
305
+ currentState = . inactive
281
306
}
282
307
}
283
308
@@ -291,18 +316,24 @@ extension NFCConnectionManager: NFCTagReaderSessionDelegate {
291
316
}
292
317
293
318
// @TraceScope
294
- nonisolated public func tagReaderSession(
295
- _ session: NFCTagReaderSession ,
296
- didInvalidateWithError error: Error
297
- ) {
319
+ nonisolated public func tagReaderSession( _ session: NFCTagReaderSession , didInvalidateWithError error: Error ) {
298
320
trace ( message: " NFCTagReaderSessionDelegate: Session invalidated – \( error. localizedDescription) " )
299
321
300
- Task { await stop ( with: . failure( error) ) }
322
+ let nfcError = error as? NFCReaderError
323
+ switch nfcError? . code {
324
+ case . some( . readerSessionInvalidationErrorUserCanceled) :
325
+ return
326
+ default :
327
+ Task {
328
+ if await currentState. session === session {
329
+ await stop ( with: . failure( error) )
330
+ }
331
+ }
332
+ }
301
333
}
302
334
303
335
// @TraceScope
304
336
nonisolated public func tagReaderSession( _ session: NFCTagReaderSession , didDetect tags: [ NFCTag ] ) {
305
-
306
337
trace ( message: " NFCTagReaderSessionDelegate: Session didDetectTags – \( tags. count) tags " )
307
338
let iso7816Tags = tags. compactMap { tag -> NFCISO7816Tag ? in
308
339
if case . iso7816( let iso7816Tag) = tag { return iso7816Tag }
@@ -314,27 +345,79 @@ extension NFCConnectionManager: NFCTagReaderSessionDelegate {
314
345
return
315
346
}
316
347
317
- Task { await connected ( session: session, tag: firstTag) }
348
+ Task {
349
+ if await session === currentState. session {
350
+ await connected ( session: session, tag: firstTag)
351
+ } else {
352
+ await invalidate ( error: ConnectionError . cancelled)
353
+ }
354
+ }
318
355
}
319
356
}
320
357
321
358
// MARK: - State enum
322
359
private enum State : Sendable {
360
+ struct ScanningState {
361
+ let session : NFCTagReaderSession
362
+ let connection : Promise < NFCConnection >
363
+ }
364
+
365
+ struct ConnectedState {
366
+ let session : NFCTagReaderSession
367
+ let tag : NFCISO7816Tag
368
+ let connection : NFCConnection
369
+ let didCloseConnection : Promise < Error ? >
370
+ }
371
+
323
372
case inactive
324
- case scanning( NFCTagReaderSession )
325
- case connected( NFCTagReaderSession , NFCISO7816Tag )
373
+ case scanning( ScanningState )
374
+ case connected( ConnectedState )
326
375
327
376
var session : NFCTagReaderSession ? {
328
377
switch self {
329
378
case . inactive: return nil
330
- case let . scanning( session) , let . connected( session, _) : return session
379
+ case let . scanning( state) :
380
+ return state. session
381
+ case let . connected( state) :
382
+ return state. session
331
383
}
332
384
}
333
385
334
386
var tag : NFCISO7816Tag ? {
335
387
switch self {
336
- case . inactive, . scanning: return nil
337
- case let . connected( _, tag) : return tag
388
+ case . inactive, . scanning:
389
+ return nil
390
+ case let . connected( state) :
391
+ return state. tag
392
+ }
393
+ }
394
+
395
+ var didCloseConnection : Promise < Error ? > ? {
396
+ switch self {
397
+ case . inactive, . scanning:
398
+ return nil
399
+ case let . connected( state) :
400
+ return state. didCloseConnection
401
+ }
402
+ }
403
+
404
+ var connectionPromise : Promise < NFCConnection > ? {
405
+ switch self {
406
+ case . inactive:
407
+ return nil
408
+ case let . scanning( state) :
409
+ return state. connection
410
+ case . connected:
411
+ return nil
412
+ }
413
+ }
414
+
415
+ var connection : NFCConnection ? {
416
+ switch self {
417
+ case . inactive, . scanning:
418
+ return nil
419
+ case let . connected( state) :
420
+ return state. connection
338
421
}
339
422
}
340
423
}
0 commit comments