From 4f1e0e61ece222cd0e505c852a32259b4f3bd768 Mon Sep 17 00:00:00 2001 From: brianna Date: Fri, 19 Jul 2024 16:55:31 -0700 Subject: [PATCH 1/4] 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/4] 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 0b4ba81b39ad9280788e539f49b7692456d852ab Mon Sep 17 00:00:00 2001 From: brianna Date: Tue, 23 Jul 2024 11:02:58 -0700 Subject: [PATCH 3/4] Update noVerificationSignal constant. --- .../Swift/DaysUntilBirthday/Shared/Models/Verification.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Models/Verification.swift b/Samples/Swift/DaysUntilBirthday/Shared/Models/Verification.swift index 08b6dcb6..5f9c69e4 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Models/Verification.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Models/Verification.swift @@ -28,9 +28,7 @@ struct Verification: Decodable { self.signal = signal } - static var noVerificationSignal: Verification? { - return Verification(signal: "No signal found") - } + static let noVerificationSignal: Verification = Verification(signal: "No signal found") } struct VerificationResponse: Decodable { From 16de7d46d328fae48c5fb233ce45ea3347f1197a Mon Sep 17 00:00:00 2001 From: brianna Date: Tue, 23 Jul 2024 11:24:02 -0700 Subject: [PATCH 4/4] Update path to fetching access token string. --- .../DaysUntilBirthday/Shared/Services/VerificationLoader.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift index 0fe7722c..b75fdec2 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift @@ -49,7 +49,7 @@ final class VerificationLoader: ObservableObject { private func createSession(verifyResult: GIDVerifiedAccountDetailResult, completion: @escaping (Result) -> Void) { - guard let token = verifyResult.accessTokenString else { + guard let token = verifyResult.accessToken?.tokenString else { completion(.failure(.couldNotCreateURLSession)) return }