Skip to content

Commit 4bfa427

Browse files
committed
Fixes to NFCConnection
1 parent 0d2d29c commit 4bfa427

File tree

1 file changed

+127
-44
lines changed

1 file changed

+127
-44
lines changed

YubiKit/YubiKit/NFCConnection.swift

Lines changed: 127 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import OSLog
2121
///
2222
/// 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
2323
/// 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:)``
2525
/// method that will close the connection and set the alertMessage of the NFC alert to the provided message.
2626
///
2727
/// > 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 {
7777

7878
// @TraceScope
7979
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+
)
8183
await NFCConnectionManager.shared.stop(with: .success(message))
8284
}
8385

@@ -164,8 +166,6 @@ private final actor NFCConnectionManager: NSObject {
164166
private override init() { super.init() }
165167

166168
private var isEstablishing: Bool = false
167-
private var connection: Promise<NFCConnection>? = nil
168-
private var didCloseConnection: Promise<Error?>? = nil
169169

170170
private var currentState = State.inactive
171171
private func set(state: State) {
@@ -183,13 +183,14 @@ private final actor NFCConnectionManager: NSObject {
183183
func didClose(for connection: NFCConnection) async throws {
184184
trace(message: "Manager.didClose(for:) – tracking closure for tag \(connection.tag)")
185185

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+
}
193194
}
194195
}
195196

@@ -229,55 +230,79 @@ private final actor NFCConnectionManager: NSObject {
229230
// Close the previous connection before establishing a new one
230231
switch currentState {
231232
case .inactive:
233+
// lets continue
232234
break
233-
case .connected, .scanning:
234-
await stop(with: .success(nil))
235+
case .scanning, .connected:
236+
// invalidate and continue
237+
await invalidate()
235238
}
236239

237240
// Start polling
238241
guard let session = NFCTagReaderSession(pollingOption: [.iso14443], delegate: self, queue: nil) else {
239242
throw NFCConnectionError.failedToPoll
240243
}
244+
245+
let connection: Promise<NFCConnection> = .init()
246+
currentState = .scanning(.init(session: session, connection: connection))
247+
241248
if let alertMessage { session.alertMessage = alertMessage }
242-
currentState = .scanning(session)
243-
connection = .init()
244249
session.begin()
245250

246-
return try await connection!.value()
251+
return try await connection.value()
247252
}
248253

249254
// @TraceScope
250255
func stop(with result: Result<String?, Error>) async {
251256
trace(message: "Manager.stop(with:) - result: \(String(describing: result))")
252257

253258
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+
}
258265
}
259266

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)
264267
switch result {
265268
case .success:
266-
await didCloseConnection?.fulfill(nil)
269+
await invalidate()
267270
case let .failure(error):
268-
await didCloseConnection?.fulfill(error)
271+
await invalidate(error: error)
269272
}
270-
currentState = .inactive
271-
connection = nil
272-
didCloseConnection = nil
273273
}
274274

275275
// @TraceScope
276276
func connected(session: NFCTagReaderSession, tag: NFCISO7816Tag) async {
277277
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
281306
}
282307
}
283308

@@ -291,18 +316,24 @@ extension NFCConnectionManager: NFCTagReaderSessionDelegate {
291316
}
292317

293318
// @TraceScope
294-
nonisolated public func tagReaderSession(
295-
_ session: NFCTagReaderSession,
296-
didInvalidateWithError error: Error
297-
) {
319+
nonisolated public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
298320
trace(message: "NFCTagReaderSessionDelegate: Session invalidated – \(error.localizedDescription)")
299321

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+
}
301333
}
302334

303335
// @TraceScope
304336
nonisolated public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
305-
306337
trace(message: "NFCTagReaderSessionDelegate: Session didDetectTags – \(tags.count) tags")
307338
let iso7816Tags = tags.compactMap { tag -> NFCISO7816Tag? in
308339
if case .iso7816(let iso7816Tag) = tag { return iso7816Tag }
@@ -314,27 +345,79 @@ extension NFCConnectionManager: NFCTagReaderSessionDelegate {
314345
return
315346
}
316347

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+
}
318355
}
319356
}
320357

321358
// MARK: - State enum
322359
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+
323372
case inactive
324-
case scanning(NFCTagReaderSession)
325-
case connected(NFCTagReaderSession, NFCISO7816Tag)
373+
case scanning(ScanningState)
374+
case connected(ConnectedState)
326375

327376
var session: NFCTagReaderSession? {
328377
switch self {
329378
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
331383
}
332384
}
333385

334386
var tag: NFCISO7816Tag? {
335387
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
338421
}
339422
}
340423
}

0 commit comments

Comments
 (0)