Complete Challenge 3
> Our app silently fails when errors occur during biometric authentication. Add code to show those errors in an alert. But be careful: you can only add one alert() modifier to each view.
This commit is contained in:
parent
53dc10ab4a
commit
74ac5d349b
|
@ -14,7 +14,7 @@ import LocalAuthentication
|
||||||
|
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
var authenticationService: AuthenticatingService
|
var authenticationService: AuthenticationService
|
||||||
var locationCollectionsState: LocationCollectionsState
|
var locationCollectionsState: LocationCollectionsState
|
||||||
var wikiPagesState: WikiPagesState
|
var wikiPagesState: WikiPagesState
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ struct AppState {
|
||||||
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
authenticationService: AuthenticatingService = AuthenticationService(laContextType: LAContext.self),
|
authenticationService: AuthenticationService = AuthenticationService(laContextType: LAContext.self),
|
||||||
locationCollectionsState: LocationCollectionsState = .init(),
|
locationCollectionsState: LocationCollectionsState = .init(),
|
||||||
wikiPagesState: WikiPagesState = .init()
|
wikiPagesState: WikiPagesState = .init()
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -24,19 +24,12 @@ extension LAContext: LAContextType {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protocol AuthenticatingService {
|
class AuthenticationService {
|
||||||
|
|
||||||
/// - Parameter reason: The app-provided reason for requesting authentication,
|
|
||||||
/// which displays in the authentication dialog presented to the user.
|
|
||||||
func authenticate(reason: String) -> AnyPublisher<Void, Error>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
final class AuthenticationService: AuthenticatingService {
|
|
||||||
static let authReason = "Please authenticate to unlock this app and access saved locations."
|
static let authReason = "Please authenticate to unlock this app and access saved locations."
|
||||||
|
|
||||||
enum Error: Swift.Error {
|
enum Error: Swift.Error, Identifiable {
|
||||||
|
var id: String { self.localizedDescription }
|
||||||
|
|
||||||
case noBiometricsEnabled(Swift.Error?)
|
case noBiometricsEnabled(Swift.Error?)
|
||||||
case evaluationFailed(Swift.Error?)
|
case evaluationFailed(Swift.Error?)
|
||||||
}
|
}
|
||||||
|
@ -46,12 +39,14 @@ final class AuthenticationService: AuthenticatingService {
|
||||||
private var context: LAContextType?
|
private var context: LAContextType?
|
||||||
|
|
||||||
|
|
||||||
init(laContextType: LAContextType.Type) {
|
init(laContextType: LAContextType.Type = LAContext.self) {
|
||||||
self.laContextType = laContextType
|
self.laContextType = laContextType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func authenticate(reason: String) -> AnyPublisher<Void, Swift.Error> {
|
/// - Parameter reason: The app-provided reason for requesting authentication,
|
||||||
|
/// which displays in the authentication dialog presented to the user.
|
||||||
|
func authenticate(reason: String) -> AnyPublisher<Void, Error> {
|
||||||
let context = laContextType.init()
|
let context = laContextType.init()
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
|
@ -94,8 +89,8 @@ final class AuthenticationService: AuthenticatingService {
|
||||||
|
|
||||||
extension SampleData {
|
extension SampleData {
|
||||||
|
|
||||||
class AuthService: AuthenticatingService {
|
class AuthService: AuthenticationService {
|
||||||
func authenticate(reason: String) -> AnyPublisher<Void, Error> {
|
override func authenticate(reason: String) -> AnyPublisher<Void, Error> {
|
||||||
Empty().eraseToAnyPublisher()
|
Empty().eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ extension LocationCollectionsContainerView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitle("PlaceCase")
|
.navigationBarTitle("PlaceCase")
|
||||||
|
.alert(item: $viewModel.authenticationError) { _ in self.authenticationErrorAlert }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +71,15 @@ extension LocationCollectionsContainerView {
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private var authenticationErrorAlert: Alert {
|
||||||
|
.init(
|
||||||
|
title: Text(viewModel.authenticationErrorAlertTitle),
|
||||||
|
message: Text(viewModel.authenticationErrorAlertBody),
|
||||||
|
dismissButton: .default(Text("OK"))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,20 +14,19 @@ import Combine
|
||||||
final class LocationCollectionsContainerViewModel: ObservableObject {
|
final class LocationCollectionsContainerViewModel: ObservableObject {
|
||||||
private var subscriptions = Set<AnyCancellable>()
|
private var subscriptions = Set<AnyCancellable>()
|
||||||
|
|
||||||
private let authService: AuthenticatingService
|
private let authService: AuthenticationService
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Published Properties
|
// MARK: - Published Properties
|
||||||
@Published var isAuthenticated: Bool = false
|
@Published var isAuthenticated: Bool = false
|
||||||
|
@Published var authenticationError: AuthenticationService.Error? = nil
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Init
|
// MARK: - Init
|
||||||
init(
|
init(
|
||||||
authService: AuthenticatingService
|
authService: AuthenticationService
|
||||||
) {
|
) {
|
||||||
self.authService = authService
|
self.authService = authService
|
||||||
|
|
||||||
setupSubscribers()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +48,7 @@ extension LocationCollectionsContainerViewModel {
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
print("Authentication failed: \(error)")
|
self?.authenticationError = error
|
||||||
self?.isAuthenticated = false
|
self?.isAuthenticated = false
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -67,11 +66,25 @@ extension LocationCollectionsContainerViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Computed
|
||||||
|
extension LocationCollectionsContainerViewModel {
|
||||||
|
|
||||||
|
var authenticationErrorAlertTitle: String { "Authentication Failed" }
|
||||||
|
|
||||||
|
var authenticationErrorAlertBody: String {
|
||||||
|
guard let error = authenticationError else { return "" }
|
||||||
|
|
||||||
|
switch error {
|
||||||
|
case .noBiometricsEnabled:
|
||||||
|
return "No Biomentric authentication is enabled on this device."
|
||||||
|
case .evaluationFailed(_):
|
||||||
|
return "An error occured while attempting evaluation."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Private Helpers
|
// MARK: - Private Helpers
|
||||||
private extension LocationCollectionsContainerViewModel {
|
private extension LocationCollectionsContainerViewModel {
|
||||||
|
|
||||||
func setupSubscribers() {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue