From 4f1e0e61ece222cd0e505c852a32259b4f3bd768 Mon Sep 17 00:00:00 2001 From: brianna Date: Fri, 19 Jul 2024 16:55:31 -0700 Subject: [PATCH 1/9] Create `Verification` that will hold the age verification signal. --- .../project.pbxproj | 4 ++ .../Shared/Models/Verification.swift | 70 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 Samples/Swift/DaysUntilBirthday/Shared/Models/Verification.swift diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj index abc5605b..365e583d 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 641495132C405D0200C9A613 /* VerifiedAgeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495122C405D0200C9A613 /* VerifiedAgeViewModel.swift */; }; 641495152C405E1400C9A613 /* VerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495142C405E1400C9A613 /* VerificationView.swift */; }; 641495172C405E3600C9A613 /* VerificationLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495162C405E3600C9A613 /* VerificationLoader.swift */; }; + 6499D22A2C4B2F4200825B30 /* Verification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6499D2292C4B2F4200825B30 /* Verification.swift */; }; 7345AD032703D9470020AFB1 /* DaysUntilBirthday.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7345AD022703D9470020AFB1 /* DaysUntilBirthday.swift */; }; 7345AD052703D9470020AFB1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7345AD042703D9470020AFB1 /* ContentView.swift */; }; 7345AD072703D9480020AFB1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7345AD062703D9480020AFB1 /* Assets.xcassets */; }; @@ -59,6 +60,7 @@ 641495122C405D0200C9A613 /* VerifiedAgeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerifiedAgeViewModel.swift; sourceTree = ""; }; 641495142C405E1400C9A613 /* VerificationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerificationView.swift; sourceTree = ""; }; 641495162C405E3600C9A613 /* VerificationLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerificationLoader.swift; sourceTree = ""; }; + 6499D2292C4B2F4200825B30 /* Verification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Verification.swift; sourceTree = ""; }; 7345ACFF2703D9470020AFB1 /* DaysUntilBirthday (iOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DaysUntilBirthday (iOS).app"; sourceTree = BUILT_PRODUCTS_DIR; }; 7345AD022703D9470020AFB1 /* DaysUntilBirthday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaysUntilBirthday.swift; sourceTree = ""; }; 7345AD042703D9470020AFB1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -220,6 +222,7 @@ isa = PBXGroup; children = ( 739FCC47270E659A00C92042 /* Birthday.swift */, + 6499D2292C4B2F4200825B30 /* Verification.swift */, ); path = Models; sourceTree = ""; @@ -383,6 +386,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6499D22A2C4B2F4200825B30 /* Verification.swift in Sources */, 739FCC48270E659A00C92042 /* Birthday.swift in Sources */, 739FCC46270E467600C92042 /* BirthdayView.swift in Sources */, 7345AD1B2703D9C30020AFB1 /* SignInView.swift in Sources */, diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Models/Verification.swift b/Samples/Swift/DaysUntilBirthday/Shared/Models/Verification.swift new file mode 100644 index 00000000..08b6dcb6 --- /dev/null +++ b/Samples/Swift/DaysUntilBirthday/Shared/Models/Verification.swift @@ -0,0 +1,70 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +struct Verification: Decodable { + let signal: String + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.signal = try container.decode(String.self) + } + + init(signal: String) { + self.signal = signal + } + + static var noVerificationSignal: Verification? { + return Verification(signal: "No signal found") + } +} + +struct VerificationResponse: Decodable { + let verifications: [Verification] + let firstVerification: Verification + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.verifications = try container.decode([Verification].self, forKey:.ageVerificationResults) + guard let first = verifications.first else { + throw Error.noVerificationInResult + } + self.firstVerification = first + } +} + +extension VerificationResponse { + enum CodingKeys: String, CodingKey { + case ageVerificationResults + } +} + +extension VerificationResponse { + enum Error: Swift.Error { + case noVerificationInResult + } +} + +/* + { + "name": "ageVerification", + "verificationId": "A verification id string", + "ageVerificationResults": [ + "AGE_PENDING" + ] + } + */ From 14c88aefdcdc734ae06cc71c9bc1e8e722c388b0 Mon Sep 17 00:00:00 2001 From: brianna Date: Fri, 19 Jul 2024 16:56:35 -0700 Subject: [PATCH 2/9] Create `fetchAgeVerificationSignal` to create a url session, decode the response, and publish the verification signal. --- .../Shared/Services/VerificationLoader.swift | 104 +++++++++++++++++- 1 file changed, 99 insertions(+), 5 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift index 410330b8..0fe7722c 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +import Combine import Foundation import GoogleSignIn @@ -21,27 +22,89 @@ import GoogleSignIn /// An observable class for verifying via Google. final class VerificationLoader: ObservableObject { + /// The `Verification` object of the current user holding the age verification signal. + /// - note: Changes to this property will be published to observers. + @Published private(set) var verification: Verification? + private var verifiedAgeViewModel: VerifiedAgeViewModel + private let baseUrlString = "https://verifywithgoogle.googleapis.com/v1/ageVerification" + + private lazy var components: URLComponents? = { + var comps = URLComponents(string: baseUrlString) + return comps + }() + + private lazy var request: URLRequest? = { + guard let components = components, let url = components.url else { + return nil + } + return URLRequest(url: url) + }() /// Creates an instance of this loader. - /// - parameter verifiedAgeViewModel: The view model to use to set verification status on. + /// - parameter verifiedAgeViewModel: The view model to use to set age verification signal on. init(verifiedViewAgeModel: VerifiedAgeViewModel) { self.verifiedAgeViewModel = verifiedViewAgeModel } + private func createSession(verifyResult: GIDVerifiedAccountDetailResult, + completion: @escaping (Result) -> Void) { + guard let token = verifyResult.accessTokenString else { + completion(.failure(.couldNotCreateURLSession)) + return + } + let configuration = URLSessionConfiguration.default + configuration.httpAdditionalHeaders = [ + "Authorization": "Bearer \(token)" + ] + let session = URLSession(configuration: configuration) + completion(.success(session)) + } + + /// Creates a `Publisher` to fetch a user's `Verification` holding their age verification signal. + /// - parameter completion: A closure passing back the `AnyPublisher` + /// upon success. + func verificationPublisher(verifyResult: GIDVerifiedAccountDetailResult, + completion: @escaping (AnyPublisher) -> Void) { + createSession(verifyResult: verifyResult) { [weak self] result in + switch result { + case .success(let urlSession): + guard let request = self?.request else { + return completion(Fail(error:.couldNotCreateURLRequest).eraseToAnyPublisher()) + } + let verificationPublisher = urlSession.dataTaskPublisher(for: request) + .tryMap { data, error -> Verification in + let decoder = JSONDecoder() + let verificationResponse = try decoder.decode(VerificationResponse.self, from: data) + return verificationResponse.firstVerification + } + .mapError { error -> Error in + guard let loaderError = error as? Error else { + return Error.couldNotFetchVerificationSignal(underlying: error) + } + return loaderError + } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + completion(verificationPublisher) + case .failure(let error): + completion(Fail(error: error).eraseToAnyPublisher()) + } + } + } + /// Verifies the user's age is over 18 based upon the selected account. /// - note: Successful calls to this method will set the `verificationState` property of the /// `verifiedAgeViewModel` instance passed to the initializer. func verifyUserAgeOver18() { - let accountDetails: [GIDVerifiableAccountDetail] = [ - GIDVerifiableAccountDetail(accountDetailType: .ageOver18) - ] - guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else { print("There is no root view controller!") return } + let accountDetails: [GIDVerifiableAccountDetail] = [ + GIDVerifiableAccountDetail(accountDetailType: .ageOver18) + ] let verifyAccountDetail = GIDVerifyAccountDetail() verifyAccountDetail.verifyAccountDetails(accountDetails, presenting: rootViewController) { verifyResult, error in @@ -53,6 +116,37 @@ final class VerificationLoader: ObservableObject { self.verifiedAgeViewModel.verificationState = .verified(verifyResult) } } + + private var cancellable: AnyCancellable? + + /// Fetches the age verification signal of the current user. + func fetchAgeVerificationSignal(verifyResult: GIDVerifiedAccountDetailResult, + completion: @escaping () -> Void) { + self.verificationPublisher(verifyResult: verifyResult) { publisher in + self.cancellable = publisher.sink { sinkCompletion in + switch sinkCompletion { + case .finished: + break + case .failure(let error): + self.verification = Verification.noVerificationSignal + print("Error retrieving age verification: \(error)") + completion() + } + } receiveValue: { verification in + self.verification = verification + completion() + } + } + } +} + +extension VerificationLoader { + /// An error representing what went wrong in fetching a user's number of day until their birthday. + enum Error: Swift.Error { + case couldNotFetchVerificationSignal(underlying: Swift.Error) + case couldNotCreateURLRequest + case couldNotCreateURLSession + } } #endif From d38f0b2f279a8aa3926cd2d96a7034ebd241b2e7 Mon Sep 17 00:00:00 2001 From: brianna Date: Fri, 19 Jul 2024 17:32:17 -0700 Subject: [PATCH 3/9] Create a button on VerificationView to fetch age verification signal. --- .../project.pbxproj | 4 ++ .../ViewModels/VerifiedAgeViewModel.swift | 28 +++++++++++++ .../Shared/Views/AgeVerificationView.swift | 42 +++++++++++++++++++ .../Shared/Views/VerificationView.swift | 20 +++++++++ 4 files changed, 94 insertions(+) create mode 100644 Samples/Swift/DaysUntilBirthday/Shared/Views/AgeVerificationView.swift diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj index 365e583d..f1f0664b 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 641495152C405E1400C9A613 /* VerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495142C405E1400C9A613 /* VerificationView.swift */; }; 641495172C405E3600C9A613 /* VerificationLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495162C405E3600C9A613 /* VerificationLoader.swift */; }; 6499D22A2C4B2F4200825B30 /* Verification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6499D2292C4B2F4200825B30 /* Verification.swift */; }; + 6499D22C2C4B3B1500825B30 /* AgeVerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6499D22B2C4B3B1500825B30 /* AgeVerificationView.swift */; }; 7345AD032703D9470020AFB1 /* DaysUntilBirthday.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7345AD022703D9470020AFB1 /* DaysUntilBirthday.swift */; }; 7345AD052703D9470020AFB1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7345AD042703D9470020AFB1 /* ContentView.swift */; }; 7345AD072703D9480020AFB1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7345AD062703D9480020AFB1 /* Assets.xcassets */; }; @@ -61,6 +62,7 @@ 641495142C405E1400C9A613 /* VerificationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerificationView.swift; sourceTree = ""; }; 641495162C405E3600C9A613 /* VerificationLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerificationLoader.swift; sourceTree = ""; }; 6499D2292C4B2F4200825B30 /* Verification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Verification.swift; sourceTree = ""; }; + 6499D22B2C4B3B1500825B30 /* AgeVerificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgeVerificationView.swift; sourceTree = ""; }; 7345ACFF2703D9470020AFB1 /* DaysUntilBirthday (iOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DaysUntilBirthday (iOS).app"; sourceTree = BUILT_PRODUCTS_DIR; }; 7345AD022703D9470020AFB1 /* DaysUntilBirthday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaysUntilBirthday.swift; sourceTree = ""; }; 7345AD042703D9470020AFB1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -211,6 +213,7 @@ children = ( 7345AD042703D9470020AFB1 /* ContentView.swift */, 739FCC45270E467600C92042 /* BirthdayView.swift */, + 6499D22B2C4B3B1500825B30 /* AgeVerificationView.swift */, 7345AD112703D9C30020AFB1 /* SignInView.swift */, 641495142C405E1400C9A613 /* VerificationView.swift */, 7345AD142703D9C30020AFB1 /* UserProfileImageView.swift */, @@ -388,6 +391,7 @@ files = ( 6499D22A2C4B2F4200825B30 /* Verification.swift in Sources */, 739FCC48270E659A00C92042 /* Birthday.swift in Sources */, + 6499D22C2C4B3B1500825B30 /* AgeVerificationView.swift in Sources */, 739FCC46270E467600C92042 /* BirthdayView.swift in Sources */, 7345AD1B2703D9C30020AFB1 /* SignInView.swift in Sources */, 7345AD212703D9C30020AFB1 /* GoogleSignInAuthenticator.swift in Sources */, diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift index 44315446..4f7a22fc 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift @@ -24,6 +24,15 @@ final class VerifiedAgeViewModel: ObservableObject { /// The user's account verification status. /// - note: This will publish updates when its value changes. @Published var verificationState: VerificationState + /// The age verification signal telling whether the user's age is over 18 or pending. + /// - note: This will publish updates when its value changes. + @Published var ageVerificationSignal: String + /// Indicates whether the view to display the user's age is over 18 should be shown. + /// - note: This will publish updates when its value changes. + @Published var isShowingAgeVerificationSignal = false + /// Indicates whether an alert should be displayed to inform the user that they are not verified as over 18. + /// - note: This will publish updates when its value changes. + @Published var isShowingAgeVerificationAlert = false private lazy var loader: VerificationLoader = { return VerificationLoader(verifiedViewAgeModel: self) @@ -32,12 +41,31 @@ final class VerifiedAgeViewModel: ObservableObject { /// Creates an instance of this view model. init() { self.verificationState = .unverified + self.ageVerificationSignal = "Signal Unavailable" + self.isShowingAgeVerificationSignal = false + self.isShowingAgeVerificationAlert = false } /// Verifies the user's age is over 18. func verifyUserAgeOver18() { loader.verifyUserAgeOver18() } + + /// Fetches the age verification signal representing whether the user's age is over 18 or pending. + func fetchAgeVerificationSignal(verifyResult: GIDVerifiedAccountDetailResult) { + loader.fetchAgeVerificationSignal(verifyResult: verifyResult) { + self.ageVerificationSignal = + self.loader.verification?.signal ?? "Error: No verification details found" + + if (self.ageVerificationSignal == "AGE_OVER_18_STANDARD"){ + self.isShowingAgeVerificationSignal = true + self.isShowingAgeVerificationAlert = false + } else { + self.isShowingAgeVerificationSignal = false + self.isShowingAgeVerificationAlert = true + } + } + } } extension VerifiedAgeViewModel { diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Views/AgeVerificationView.swift b/Samples/Swift/DaysUntilBirthday/Shared/Views/AgeVerificationView.swift new file mode 100644 index 00000000..2b3c0be8 --- /dev/null +++ b/Samples/Swift/DaysUntilBirthday/Shared/Views/AgeVerificationView.swift @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI +import GoogleSignIn + +struct AgeVerificationResultView: View { + /// The age verification signal telling whether the user's age is over 18 or pending. + let ageVerificationSignal: String + + @Environment(\.presentationMode) var presentationMode + + var body: some View { + NavigationView { + VStack { + Text("Age Verification Signal: \(ageVerificationSignal)") + Spacer() + } + .navigationTitle("User is verified over 18!") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Close") { + presentationMode.wrappedValue.dismiss() + } + } + } + } + } +} diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift b/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift index 7c309ed3..6fe18a37 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift @@ -23,6 +23,16 @@ struct VerificationView: View { var body: some View { switch verifiedAgeViewModel.verificationState { case .verified(let result): + Button("Fetch Age Verification Signal") { + verifiedAgeViewModel.fetchAgeVerificationSignal(verifyResult: result) + } + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 30) + .stroke(Color.gray) + ) + .padding(.bottom) + VStack(alignment:.leading) { Text("List of result object properties:") .font(.headline) @@ -52,6 +62,16 @@ struct VerificationView: View { action:{refresh(results: result)}) } } + .sheet(isPresented: $verifiedAgeViewModel.isShowingAgeVerificationSignal) { + AgeVerificationResultView(ageVerificationSignal: verifiedAgeViewModel.ageVerificationSignal) + } + .alert(isPresented: $verifiedAgeViewModel.isShowingAgeVerificationAlert, content: { + Alert( + title: Text("Oh no! User is not verified over 18."), + message: Text("Age Verification Signal: \(verifiedAgeViewModel.ageVerificationSignal)"), + dismissButton: .cancel() + ) + }) case .unverified: ProgressView() .navigationTitle(NSLocalizedString("Unverified account", From 9fa09d3f216e77225b67677c46c56d047462fe7a Mon Sep 17 00:00:00 2001 From: brianna Date: Tue, 23 Jul 2024 11:11:53 -0700 Subject: [PATCH 4/9] Remove setting booleans in initializer. --- .../Shared/ViewModels/VerifiedAgeViewModel.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift index 4f7a22fc..00e85869 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift @@ -42,8 +42,6 @@ final class VerifiedAgeViewModel: ObservableObject { init() { self.verificationState = .unverified self.ageVerificationSignal = "Signal Unavailable" - self.isShowingAgeVerificationSignal = false - self.isShowingAgeVerificationAlert = false } /// Verifies the user's age is over 18. From fe63129ab39d93b88411f706ef0f863fda4f7953 Mon Sep 17 00:00:00 2001 From: brianna Date: Tue, 23 Jul 2024 13:34:02 -0700 Subject: [PATCH 5/9] Create enum to handle possible states for age verification signal. --- .../ViewModels/VerifiedAgeViewModel.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift index 00e85869..d07f9a5a 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift @@ -38,10 +38,20 @@ final class VerifiedAgeViewModel: ObservableObject { return VerificationLoader(verifiedViewAgeModel: self) }() + /// An enumeration representing possible states of an age verification signal. + enum AgeVerificationSignal: String { + /// The user's age has been verified to be over 18. + case ageOver18Standard = "AGE_OVER_18_STANDARD" + /// The user's age verification is pending. + case agePending = "AGE_PENDING" + /// Indicates there was no age verification signal found. + case noAgeVerificationSignal = "Signal Unavailable" + } + /// Creates an instance of this view model. init() { self.verificationState = .unverified - self.ageVerificationSignal = "Signal Unavailable" + self.ageVerificationSignal = AgeVerificationSignal.noAgeVerificationSignal.rawValue } /// Verifies the user's age is over 18. @@ -53,9 +63,10 @@ final class VerifiedAgeViewModel: ObservableObject { func fetchAgeVerificationSignal(verifyResult: GIDVerifiedAccountDetailResult) { loader.fetchAgeVerificationSignal(verifyResult: verifyResult) { self.ageVerificationSignal = - self.loader.verification?.signal ?? "Error: No verification details found" + self.loader.verification?.signal ?? AgeVerificationSignal.noAgeVerificationSignal.rawValue - if (self.ageVerificationSignal == "AGE_OVER_18_STANDARD"){ + let signal = AgeVerificationSignal(rawValue: self.ageVerificationSignal) + if (signal == .ageOver18Standard) { self.isShowingAgeVerificationSignal = true self.isShowingAgeVerificationAlert = false } else { From f524f15621712bd22aa3a3f49db47d596c093a51 Mon Sep 17 00:00:00 2001 From: brianna Date: Tue, 23 Jul 2024 16:34:34 -0700 Subject: [PATCH 6/9] Replace deprecated .alert and add check for iOS 15+. --- .../Shared/Views/VerificationView.swift | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift b/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift index 3e9cebbf..722058bf 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift @@ -28,50 +28,50 @@ struct VerificationView: View { } .padding(10) .overlay( - RoundedRectangle(cornerRadius: 30) - .stroke(Color.gray) + RoundedRectangle(cornerRadius: 30) + .stroke(Color.gray) ) .padding(.bottom) + if #available(iOS 15, *) { + VStack(alignment:.leading) { + Text("List of result object properties:") + .font(.headline) - VStack(alignment:.leading) { - Text("List of result object properties:") - .font(.headline) - - HStack(alignment: .top) { - Text("Access Token:") - Text(result.accessToken?.tokenString ?? "Not available") - } - HStack(alignment: .top) { - Text("Refresh Token:") - Text(result.refreshToken?.tokenString ?? "Not available") - } - HStack { - Text("Expiration:") - if let expirationDate = result.accessToken?.expirationDate { - Text(formatDateWithDateFormatter(expirationDate)) - } else { - Text("Not available") + HStack(alignment: .top) { + Text("Access Token:") + Text(result.accessToken?.tokenString ?? "Not available") + } + HStack(alignment: .top) { + Text("Refresh Token:") + Text(result.refreshToken?.tokenString ?? "Not available") + } + HStack { + Text("Expiration:") + if let expirationDate = result.accessToken?.expirationDate { + Text(formatDateWithDateFormatter(expirationDate)) + } else { + Text("Not available") + } } + Spacer() } - Spacer() - } - .navigationTitle("Verified Account!") - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button(NSLocalizedString("Refresh", comment: "Refresh button"), - action:{refresh(results: result)}) + .navigationTitle("Verified Account!") + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button(NSLocalizedString("Refresh", comment: "Refresh button"), + action:{refresh(results: result)}) + } } - } - .sheet(isPresented: $verifiedAgeViewModel.isShowingAgeVerificationSignal) { + .sheet(isPresented: $verifiedAgeViewModel.isShowingAgeVerificationSignal) { AgeVerificationResultView(ageVerificationSignal: verifiedAgeViewModel.ageVerificationSignal) + } + .alert("Oh no! User is not verified over 18.", + isPresented: $verifiedAgeViewModel.isShowingAgeVerificationAlert) { + Button("OK", role: .cancel) { } + } message: { + Text("Age Verification Signal: \(verifiedAgeViewModel.ageVerificationSignal)") + } } - .alert(isPresented: $verifiedAgeViewModel.isShowingAgeVerificationAlert, content: { - Alert( - title: Text("Oh no! User is not verified over 18."), - message: Text("Age Verification Signal: \(verifiedAgeViewModel.ageVerificationSignal)"), - dismissButton: .cancel() - ) - }) case .unverified: ProgressView() .navigationTitle(NSLocalizedString("Unverified account", From b2aefb119cb49d4a03d139bbf98cffe35ed5e4f7 Mon Sep 17 00:00:00 2001 From: brianna Date: Fri, 2 Aug 2024 17:47:19 -0700 Subject: [PATCH 7/9] Change var `ageVerificationSignal` to be of type `AgeVerificationSignal` so consumers get the raw value when needed. --- .../Shared/ViewModels/VerifiedAgeViewModel.swift | 12 ++++++------ .../Shared/Views/VerificationView.swift | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift index d07f9a5a..79099e3c 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift @@ -26,7 +26,7 @@ final class VerifiedAgeViewModel: ObservableObject { @Published var verificationState: VerificationState /// The age verification signal telling whether the user's age is over 18 or pending. /// - note: This will publish updates when its value changes. - @Published var ageVerificationSignal: String + @Published var ageVerificationSignal: AgeVerificationSignal /// Indicates whether the view to display the user's age is over 18 should be shown. /// - note: This will publish updates when its value changes. @Published var isShowingAgeVerificationSignal = false @@ -51,7 +51,7 @@ final class VerifiedAgeViewModel: ObservableObject { /// Creates an instance of this view model. init() { self.verificationState = .unverified - self.ageVerificationSignal = AgeVerificationSignal.noAgeVerificationSignal.rawValue + self.ageVerificationSignal = .noAgeVerificationSignal } /// Verifies the user's age is over 18. @@ -62,11 +62,11 @@ final class VerifiedAgeViewModel: ObservableObject { /// Fetches the age verification signal representing whether the user's age is over 18 or pending. func fetchAgeVerificationSignal(verifyResult: GIDVerifiedAccountDetailResult) { loader.fetchAgeVerificationSignal(verifyResult: verifyResult) { - self.ageVerificationSignal = - self.loader.verification?.signal ?? AgeVerificationSignal.noAgeVerificationSignal.rawValue + let signal = self.loader.verification?.signal ?? "" + self.ageVerificationSignal = AgeVerificationSignal(rawValue: signal) ?? + .noAgeVerificationSignal - let signal = AgeVerificationSignal(rawValue: self.ageVerificationSignal) - if (signal == .ageOver18Standard) { + if (self.ageVerificationSignal == .ageOver18Standard) { self.isShowingAgeVerificationSignal = true self.isShowingAgeVerificationAlert = false } else { diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift b/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift index 722058bf..fe072b11 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift @@ -63,13 +63,13 @@ struct VerificationView: View { } } .sheet(isPresented: $verifiedAgeViewModel.isShowingAgeVerificationSignal) { - AgeVerificationResultView(ageVerificationSignal: verifiedAgeViewModel.ageVerificationSignal) + AgeVerificationResultView(ageVerificationSignal: verifiedAgeViewModel.ageVerificationSignal.rawValue) } .alert("Oh no! User is not verified over 18.", isPresented: $verifiedAgeViewModel.isShowingAgeVerificationAlert) { Button("OK", role: .cancel) { } } message: { - Text("Age Verification Signal: \(verifiedAgeViewModel.ageVerificationSignal)") + Text("Age Verification Signal: \(verifiedAgeViewModel.ageVerificationSignal.rawValue)") } } case .unverified: From cae8350c0553a8fac725f39e0dd59428a3490775 Mon Sep 17 00:00:00 2001 From: brianna Date: Mon, 5 Aug 2024 14:26:52 -0700 Subject: [PATCH 8/9] Remove switch so method only focuses on the failure case. --- .../Shared/Services/VerificationLoader.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift index b75fdec2..913cd13b 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift @@ -124,14 +124,11 @@ final class VerificationLoader: ObservableObject { completion: @escaping () -> Void) { self.verificationPublisher(verifyResult: verifyResult) { publisher in self.cancellable = publisher.sink { sinkCompletion in - switch sinkCompletion { - case .finished: - break - case .failure(let error): + if case .failure(let error) = sinkCompletion { self.verification = Verification.noVerificationSignal print("Error retrieving age verification: \(error)") - completion() } + completion() } receiveValue: { verification in self.verification = verification completion() From cd2ba7bd6931660f53fddd880afa6486708a61fb Mon Sep 17 00:00:00 2001 From: brianna Date: Tue, 6 Aug 2024 11:20:07 -0700 Subject: [PATCH 9/9] Ensure completion() is called in `fetchAgeVerificationSignal:` once per failure or succession. --- .../Shared/Services/VerificationLoader.swift | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift index 913cd13b..380dce36 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift @@ -123,16 +123,19 @@ final class VerificationLoader: ObservableObject { func fetchAgeVerificationSignal(verifyResult: GIDVerifiedAccountDetailResult, completion: @escaping () -> Void) { self.verificationPublisher(verifyResult: verifyResult) { publisher in - self.cancellable = publisher.sink { sinkCompletion in - if case .failure(let error) = sinkCompletion { - self.verification = Verification.noVerificationSignal - print("Error retrieving age verification: \(error)") + self.cancellable = publisher.sink( + receiveCompletion: { sinkCompletion in + if case .failure(let error) = sinkCompletion { + self.verification = Verification.noVerificationSignal + print("Error retrieving age verification: \(error)") + completion() + } + }, + receiveValue: { verification in + self.verification = verification + completion() } - completion() - } receiveValue: { verification in - self.verification = verification - completion() - } + ) } } }