diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml index 298b859..61b720e 100644 --- a/Android/app/src/main/AndroidManifest.xml +++ b/Android/app/src/main/AndroidManifest.xml @@ -20,7 +20,7 @@ - + @@ -34,6 +34,9 @@ + + + diff --git a/Sources/Showcase/BiometricAuthenticationPlayground.swift b/Sources/Showcase/BiometricAuthenticationPlayground.swift new file mode 100644 index 0000000..5971161 --- /dev/null +++ b/Sources/Showcase/BiometricAuthenticationPlayground.swift @@ -0,0 +1,87 @@ +// Copyright 2023–2026 Skip +import SwiftUI +import SkipKit + +/// This component uses the `SkipKit` module from https://source.skip.tools/skip-kit +struct BiometricAuthenticationPlayground: View { + @State var authenticationType = BiometricAuthentication.authenticationType + @State var result = "Not authenticated" + @State var authenticationAttempt = 0 + + private var canAuthenticate: Bool { BiometricAuthentication.canAuthenticate } + + private var resultColor: Color { + switch self.result { + case "Authenticated": .green + case "Cancelled": .orange + case "Failed", "Unavailable": .red + default: + .secondary + } + } + + var body: some View { + List { + Section("Availability") { + LabeledContent("Type", + value: { + switch self.authenticationType { + case .fingerprint: "Fingerprint" + case .facialRecognition: "Facial Recognition" + case .unspecified: "Unspecified Biometric" + case .none: "None" + } + }() + ) + LabeledContent("Can Authenticate") { + Text(self.canAuthenticate ? "Yes" : "No") + .foregroundStyle(self.canAuthenticate ? Color.green : Color.red) + } + } + + if self.canAuthenticate { + Section("Authentication") { + Button("Authenticate") { self.authenticate() } + + Text(self.result) + .foregroundStyle(self.resultColor) + } + } + } + .onAppear { self.refresh() } + .onDisappear { self.cancelAuthentication() } + .toolbar { + PlaygroundSourceLink(file: "BiometricAuthenticationPlayground.swift") + } + } + + private func authenticate() { + self.authenticationAttempt += 1 + let authenticationAttempt = self.authenticationAttempt + self.result = "Authenticating..." + + BiometricAuthentication.authenticate( + localizedReason: "Authenticate with biometrics" + ) { authenticationResult in + guard authenticationAttempt == self.authenticationAttempt else { + return + } + + self.result = switch authenticationResult { + case .success: "Authenticated" + case .cancelled: "Cancelled" + case .failed: "Failed" + case .unavailable: "Unavailable" + } + self.refresh() + } + } + + private func refresh() { + self.authenticationType = BiometricAuthentication.authenticationType + } + + private func cancelAuthentication() { + self.authenticationAttempt += 1 + } +} diff --git a/Sources/Showcase/PlaygroundListView.swift b/Sources/Showcase/PlaygroundListView.swift index b3d10b7..89e6c15 100644 --- a/Sources/Showcase/PlaygroundListView.swift +++ b/Sources/Showcase/PlaygroundListView.swift @@ -8,6 +8,7 @@ enum PlaygroundType: CaseIterable, View { case animation case audio case background + case biometricAuthentication case blendMode case blur case border @@ -113,6 +114,8 @@ enum PlaygroundType: CaseIterable, View { return LocalizedStringResource("Audio", comment: "Title of Audio playground") case .background: return LocalizedStringResource("Background", comment: "Title of Background playground") + case .biometricAuthentication: + return LocalizedStringResource("Biometric Authentication", comment: "Title of Biometric Authentication playground") case .blendMode: return LocalizedStringResource("BlendMode", comment: "Title of BlendMode playground") case .blur: @@ -316,6 +319,8 @@ enum PlaygroundType: CaseIterable, View { #endif case .background: BackgroundPlayground() + case .biometricAuthentication: + BiometricAuthenticationPlayground() case .blendMode: BlendModePlayground() case .blur: diff --git a/Sources/Showcase/Resources/Localizable.xcstrings b/Sources/Showcase/Resources/Localizable.xcstrings index f9cf7c8..0224a8a 100644 --- a/Sources/Showcase/Resources/Localizable.xcstrings +++ b/Sources/Showcase/Resources/Localizable.xcstrings @@ -1002,12 +1002,30 @@ }, "AsyncImage" : { + }, + "Authenticate" : { + + }, + "Authenticate with biometrics" : { + + }, + "Authenticated" : { + + }, + "Authenticating..." : { + + }, + "Authentication" : { + }, "Audio" : { "comment" : "Title of Audio playground" }, "AudioPlayground is Lite-only until ported to SkipAV" : { + }, + "Availability" : { + }, "B" : { @@ -1077,6 +1095,9 @@ }, "Binding View" : { + }, + "Biometric Authentication" : { + "comment" : "Title of Biometric Authentication playground" }, "BipBop" : { @@ -1182,6 +1203,12 @@ }, "Cancel" : { + }, + "Cancelled" : { + + }, + "Can Authenticate" : { + }, "Capsule" : { @@ -1673,6 +1700,12 @@ }, "F2" : { + }, + "Facial Recognition" : { + + }, + "Failed" : { + }, "Favorite" : { @@ -1700,6 +1733,9 @@ }, "fill(.red)\n .stroke(.green, lineWidth: 10)" : { + }, + "Fingerprint" : { + }, "First" : { @@ -2647,6 +2683,9 @@ }, "NO" : { + }, + "No" : { + }, "No badge (count 0)" : { @@ -2665,6 +2704,12 @@ }, "Normal" : { + }, + "None" : { + + }, + "Not authenticated" : { + }, "Not supported on macOS" : { @@ -4225,6 +4270,9 @@ }, "Title Only" : { + }, + "Type" : { + }, "To:" : { @@ -4306,6 +4354,9 @@ }, "Unavailable" : { + }, + "Unspecified Biometric" : { + }, "Underline" : { @@ -4719,6 +4770,9 @@ }, "Yellow" : { + }, + "Yes" : { + }, "YES" : { @@ -4734,4 +4788,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +}