Skip to content

Commit dd372fb

Browse files
committed
Improvements to the NFC connection
1 parent f86cd91 commit dd372fb

File tree

5 files changed

+90
-25
lines changed

5 files changed

+90
-25
lines changed

Samples/OATHSample/OATHSample/ConnectionManager.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,12 @@ final class ConnectionManager: ObservableObject {
8383
extension SmartCardConnection {
8484
var connectionType: String {
8585
switch self {
86+
#if os(iOS)
8687
case _ as NFCSmartCardConnection:
8788
return "NFC"
8889
case _ as LightningSmartCardConnection:
8990
return "Lightning"
91+
#endif
9092
case _ as SmartCardConnection:
9193
return "USB"
9294
default:

YubiKit/YubiKit/Connection.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ public enum ConnectionError: Error, Sendable {
7878
case missingResult
7979
/// Awaiting call to connect() was cancelled.
8080
case cancelled
81-
/// SmartCardConnection was closed.
82-
case closed
81+
/// Awaiting call to connect() was dismissed by the user.
82+
case cancelledByUser
8383
}
8484

8585
/// A ResponseError containing the status code.

YubiKit/YubiKit/NFCSmartCardConnection.swift

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,7 @@ private final actor NFCConnectionManager: NSObject {
267267
if let message = message {
268268
currentState.session?.alertMessage = message
269269
}
270-
}
271-
272-
switch result {
273-
case .success:
274-
await invalidate()
275-
case let .failure(error):
276-
await invalidate(error: error)
270+
currentState.session?.invalidate()
277271
}
278272
}
279273

@@ -282,7 +276,7 @@ private final actor NFCConnectionManager: NSObject {
282276
trace(message: "Manager.connected(session:tag:) - tag: \(String(describing: tag.identifier))")
283277

284278
guard let promise = currentState.connectionPromise else {
285-
await invalidate()
279+
await cleanup(session: session)
286280
return
287281
}
288282

@@ -299,15 +293,21 @@ private final actor NFCConnectionManager: NSObject {
299293
await promise.fulfill(connection)
300294
}
301295

302-
private func invalidate(error: Error? = nil) async {
303-
currentState.session?.invalidate()
296+
private func cleanup(session: NFCTagReaderSession, error: Error? = nil) async {
297+
guard currentState.session === session else {
298+
return
299+
}
300+
301+
switch error {
302+
case .none:
303+
await currentState.didCloseConnection?.fulfill(nil)
304+
await currentState.connectionPromise?.cancel(with: ConnectionError.cancelledByUser)
305+
case let .some(error):
306+
await currentState.didCloseConnection?.fulfill(nil)
307+
await currentState.connectionPromise?.cancel(with: error)
308+
}
304309

305-
// Workaround for the NFC session being active for an additional 4 seconds after
306-
// invalidate() has been called on the session.
307-
try? await Task.sleep(for: .seconds(5))
308-
await currentState.didCloseConnection?.fulfill(error)
309-
await currentState.connectionPromise?.cancel(with: error ?? ConnectionError.cancelled)
310-
currentState = .inactive
310+
self.currentState = .inactive
311311
}
312312
}
313313

@@ -325,16 +325,16 @@ extension NFCConnectionManager: NFCTagReaderSessionDelegate {
325325
trace(message: "NFCTagReaderSessionDelegate: Session invalidated – \(error.localizedDescription)")
326326

327327
let nfcError = error as? NFCReaderError
328+
329+
let mappedError: Error?
328330
switch nfcError?.code {
329331
case .some(.readerSessionInvalidationErrorUserCanceled):
330-
return
332+
mappedError = nil // user cancelled, no error
331333
default:
332-
Task {
333-
if await currentState.session === session {
334-
await stop(with: .failure(error))
335-
}
336-
}
334+
mappedError = error
337335
}
336+
337+
Task { await cleanup(session: session, error: mappedError) }
338338
}
339339

340340
// @TraceScope
@@ -354,7 +354,7 @@ extension NFCConnectionManager: NFCTagReaderSessionDelegate {
354354
if await session === currentState.session {
355355
await connected(session: session, tag: firstTag)
356356
} else {
357-
await invalidate(error: ConnectionError.cancelled)
357+
await cleanup(session: session, error: ConnectionError.cancelled)
358358
}
359359
}
360360
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright Yubico AB
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
protocol ShareableConnection: Sendable {
18+
func hold(timeout: TimeInterval) async throws
19+
20+
func release()
21+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright Yubico AB
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
enum TimeoutError: Error {
18+
case exceeded
19+
}
20+
21+
func withTimeout<T>(
22+
_ seconds: TimeInterval,
23+
operation: @escaping @Sendable () async throws -> T
24+
) async throws -> T where T: Sendable {
25+
try await withThrowingTaskGroup(of: T.self) { group in
26+
// Main operation
27+
group.addTask {
28+
try await operation()
29+
}
30+
31+
// Timeout enforcer
32+
group.addTask {
33+
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
34+
throw TimeoutError.exceeded
35+
}
36+
37+
// Return the first finished task
38+
let result = try await group.next()!
39+
group.cancelAll()
40+
return result
41+
}
42+
}

0 commit comments

Comments
 (0)