Kaynağa Gözat

Create method to fetch the age verification signal from API. (#460)

Brianna Morales 1 yıl önce
ebeveyn
işleme
25e1a72e33

+ 4 - 0
Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj

@@ -10,6 +10,7 @@
 		641495132C405D0200C9A613 /* VerifiedAgeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495122C405D0200C9A613 /* VerifiedAgeViewModel.swift */; };
 		641495132C405D0200C9A613 /* VerifiedAgeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495122C405D0200C9A613 /* VerifiedAgeViewModel.swift */; };
 		641495152C405E1400C9A613 /* VerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495142C405E1400C9A613 /* VerificationView.swift */; };
 		641495152C405E1400C9A613 /* VerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495142C405E1400C9A613 /* VerificationView.swift */; };
 		641495172C405E3600C9A613 /* VerificationLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495162C405E3600C9A613 /* VerificationLoader.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 */; };
 		7345AD032703D9470020AFB1 /* DaysUntilBirthday.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7345AD022703D9470020AFB1 /* DaysUntilBirthday.swift */; };
 		7345AD052703D9470020AFB1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7345AD042703D9470020AFB1 /* ContentView.swift */; };
 		7345AD052703D9470020AFB1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7345AD042703D9470020AFB1 /* ContentView.swift */; };
 		7345AD072703D9480020AFB1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7345AD062703D9480020AFB1 /* Assets.xcassets */; };
 		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 = "<group>"; };
 		641495122C405D0200C9A613 /* VerifiedAgeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerifiedAgeViewModel.swift; sourceTree = "<group>"; };
 		641495142C405E1400C9A613 /* VerificationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerificationView.swift; sourceTree = "<group>"; };
 		641495142C405E1400C9A613 /* VerificationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerificationView.swift; sourceTree = "<group>"; };
 		641495162C405E3600C9A613 /* VerificationLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerificationLoader.swift; sourceTree = "<group>"; };
 		641495162C405E3600C9A613 /* VerificationLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerificationLoader.swift; sourceTree = "<group>"; };
+		6499D2292C4B2F4200825B30 /* Verification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Verification.swift; sourceTree = "<group>"; };
 		7345ACFF2703D9470020AFB1 /* DaysUntilBirthday (iOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DaysUntilBirthday (iOS).app"; sourceTree = BUILT_PRODUCTS_DIR; };
 		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 = "<group>"; };
 		7345AD022703D9470020AFB1 /* DaysUntilBirthday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaysUntilBirthday.swift; sourceTree = "<group>"; };
 		7345AD042703D9470020AFB1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
 		7345AD042703D9470020AFB1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -220,6 +222,7 @@
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
 				739FCC47270E659A00C92042 /* Birthday.swift */,
 				739FCC47270E659A00C92042 /* Birthday.swift */,
+				6499D2292C4B2F4200825B30 /* Verification.swift */,
 			);
 			);
 			path = Models;
 			path = Models;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -383,6 +386,7 @@
 			isa = PBXSourcesBuildPhase;
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
 			files = (
 			files = (
+				6499D22A2C4B2F4200825B30 /* Verification.swift in Sources */,
 				739FCC48270E659A00C92042 /* Birthday.swift in Sources */,
 				739FCC48270E659A00C92042 /* Birthday.swift in Sources */,
 				739FCC46270E467600C92042 /* BirthdayView.swift in Sources */,
 				739FCC46270E467600C92042 /* BirthdayView.swift in Sources */,
 				7345AD1B2703D9C30020AFB1 /* SignInView.swift in Sources */,
 				7345AD1B2703D9C30020AFB1 /* SignInView.swift in Sources */,

+ 68 - 0
Samples/Swift/DaysUntilBirthday/Shared/Models/Verification.swift

@@ -0,0 +1,68 @@
+/*
+ * 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 let noVerificationSignal: Verification = 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"
+   ]
+ }
+ */

+ 99 - 5
Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift

@@ -14,6 +14,7 @@
  * limitations under the License.
  * limitations under the License.
  */
  */
 
 
+import Combine
 import Foundation
 import Foundation
 import GoogleSignIn
 import GoogleSignIn
 
 
@@ -21,27 +22,89 @@ import GoogleSignIn
 
 
 /// An observable class for verifying via Google.
 /// An observable class for verifying via Google.
 final class VerificationLoader: ObservableObject {
 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 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.
   /// 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) {
   init(verifiedViewAgeModel: VerifiedAgeViewModel) {
     self.verifiedAgeViewModel = verifiedViewAgeModel
     self.verifiedAgeViewModel = verifiedViewAgeModel
   }
   }
 
 
+  private func createSession(verifyResult: GIDVerifiedAccountDetailResult,
+                             completion: @escaping (Result<URLSession, Error>) -> Void) {
+    guard let token = verifyResult.accessToken?.tokenString 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<Verification, Error>`
+  /// upon success.
+  func verificationPublisher(verifyResult: GIDVerifiedAccountDetailResult,
+                             completion: @escaping (AnyPublisher<Verification, Error>) -> 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.
   /// 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
   /// - note: Successful calls to this method will set the `verificationState` property of the
   /// `verifiedAgeViewModel` instance passed to the initializer.
   /// `verifiedAgeViewModel` instance passed to the initializer.
   func verifyUserAgeOver18() {
   func verifyUserAgeOver18() {
-    let accountDetails: [GIDVerifiableAccountDetail] = [
-      GIDVerifiableAccountDetail(accountDetailType: .ageOver18)
-    ]
-
     guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
     guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
       print("There is no root view controller!")
       print("There is no root view controller!")
       return
       return
     }
     }
 
 
+    let accountDetails: [GIDVerifiableAccountDetail] = [
+      GIDVerifiableAccountDetail(accountDetailType: .ageOver18)
+    ]
     let verifyAccountDetail = GIDVerifyAccountDetail()
     let verifyAccountDetail = GIDVerifyAccountDetail()
     verifyAccountDetail.verifyAccountDetails(accountDetails, presenting: rootViewController) {
     verifyAccountDetail.verifyAccountDetails(accountDetails, presenting: rootViewController) {
       verifyResult, error in
       verifyResult, error in
@@ -53,6 +116,37 @@ final class VerificationLoader: ObservableObject {
       self.verifiedAgeViewModel.verificationState = .verified(verifyResult)
       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
 #endif