diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj index 4c12414e..abc5605b 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj @@ -3,10 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 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 */; }; 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 */; }; @@ -53,6 +56,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 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 = ""; }; 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 = ""; }; @@ -204,6 +210,7 @@ 7345AD042703D9470020AFB1 /* ContentView.swift */, 739FCC45270E467600C92042 /* BirthdayView.swift */, 7345AD112703D9C30020AFB1 /* SignInView.swift */, + 641495142C405E1400C9A613 /* VerificationView.swift */, 7345AD142703D9C30020AFB1 /* UserProfileImageView.swift */, ); path = Views; @@ -221,6 +228,7 @@ isa = PBXGroup; children = ( 7345AD162703D9C30020AFB1 /* AuthenticationViewModel.swift */, + 641495122C405D0200C9A613 /* VerifiedAgeViewModel.swift */, 736F49BB270E102C00580053 /* BirthdayViewModel.swift */, ); path = ViewModels; @@ -231,6 +239,7 @@ children = ( 7345AD172703D9C30020AFB1 /* GoogleSignInAuthenticator.swift */, 736F49B9270E05E200580053 /* BirthdayLoader.swift */, + 641495162C405E3600C9A613 /* VerificationLoader.swift */, 7345AD192703D9C30020AFB1 /* UserProfileImageLoader.swift */, ); path = Services; @@ -383,9 +392,12 @@ 736F49BC270E102C00580053 /* BirthdayViewModel.swift in Sources */, 736F49BA270E05E200580053 /* BirthdayLoader.swift in Sources */, 7345AD242703D9C30020AFB1 /* UserProfileView.swift in Sources */, + 641495172C405E3600C9A613 /* VerificationLoader.swift in Sources */, 7345AD202703D9C30020AFB1 /* AuthenticationViewModel.swift in Sources */, + 641495132C405D0200C9A613 /* VerifiedAgeViewModel.swift in Sources */, 7345AD052703D9470020AFB1 /* ContentView.swift in Sources */, 7345AD032703D9470020AFB1 /* DaysUntilBirthday.swift in Sources */, + 641495152C405E1400C9A613 /* VerificationView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayForPod.xcodeproj/project.pbxproj b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayForPod.xcodeproj/project.pbxproj index 87f26e0c..341e870b 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayForPod.xcodeproj/project.pbxproj +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayForPod.xcodeproj/project.pbxproj @@ -3,11 +3,14 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 4C6A2BA2321434ACBD2AF201 /* Pods_DaysUntilBirthdayForPod__macOS_.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 926A15D393D684C68E09FB0F /* Pods_DaysUntilBirthdayForPod__macOS_.framework */; }; + 641495072C3C90E100C9A613 /* VerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495062C3C90E100C9A613 /* VerificationView.swift */; }; + 6414950A2C3E094B00C9A613 /* VerifiedAgeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641495092C3E094B00C9A613 /* VerifiedAgeViewModel.swift */; }; + 6414950C2C3E0C7A00C9A613 /* VerificationLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6414950B2C3E0C7A00C9A613 /* VerificationLoader.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 */; }; @@ -45,6 +48,9 @@ 1A129363EBB5DF1F41FAAB14 /* Pods-DaysUntilBirthdayForPod(macOS).release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DaysUntilBirthdayForPod(macOS).release.xcconfig"; path = "Target Support Files/Pods-DaysUntilBirthdayForPod(macOS)/Pods-DaysUntilBirthdayForPod(macOS).release.xcconfig"; sourceTree = ""; }; 29BEB027A694593FA0450863 /* Pods-DaysUntilBirthdayForPod(iOS).debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DaysUntilBirthdayForPod(iOS).debug.xcconfig"; path = "Target Support Files/Pods-DaysUntilBirthdayForPod(iOS)/Pods-DaysUntilBirthdayForPod(iOS).debug.xcconfig"; sourceTree = ""; }; 5CF615341A61D92D89389D44 /* Pods-DaysUntilBirthdayForPod (macOS).release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DaysUntilBirthdayForPod (macOS).release.xcconfig"; path = "Target Support Files/Pods-DaysUntilBirthdayForPod (macOS)/Pods-DaysUntilBirthdayForPod (macOS).release.xcconfig"; sourceTree = ""; }; + 641495062C3C90E100C9A613 /* VerificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationView.swift; sourceTree = ""; }; + 641495092C3E094B00C9A613 /* VerifiedAgeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifiedAgeViewModel.swift; sourceTree = ""; }; + 6414950B2C3E0C7A00C9A613 /* VerificationLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationLoader.swift; sourceTree = ""; }; 7345ACFF2703D9470020AFB1 /* DaysUntilBirthdayForPod (iOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DaysUntilBirthdayForPod (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 = ""; }; @@ -195,6 +201,7 @@ children = ( 7345AD042703D9470020AFB1 /* ContentView.swift */, 739FCC45270E467600C92042 /* BirthdayView.swift */, + 641495062C3C90E100C9A613 /* VerificationView.swift */, 7345AD112703D9C30020AFB1 /* SignInView.swift */, 7345AD142703D9C30020AFB1 /* UserProfileImageView.swift */, ); @@ -214,6 +221,7 @@ children = ( 7345AD162703D9C30020AFB1 /* AuthenticationViewModel.swift */, 736F49BB270E102C00580053 /* BirthdayViewModel.swift */, + 641495092C3E094B00C9A613 /* VerifiedAgeViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -222,6 +230,7 @@ isa = PBXGroup; children = ( 7345AD172703D9C30020AFB1 /* GoogleSignInAuthenticator.swift */, + 6414950B2C3E0C7A00C9A613 /* VerificationLoader.swift */, 736F49B9270E05E200580053 /* BirthdayLoader.swift */, 7345AD192703D9C30020AFB1 /* UserProfileImageLoader.swift */, ); @@ -413,12 +422,15 @@ buildActionMask = 2147483647; files = ( 739FCC48270E659A00C92042 /* Birthday.swift in Sources */, + 641495072C3C90E100C9A613 /* VerificationView.swift in Sources */, 739FCC46270E467600C92042 /* BirthdayView.swift in Sources */, 7345AD1B2703D9C30020AFB1 /* SignInView.swift in Sources */, 7345AD212703D9C30020AFB1 /* GoogleSignInAuthenticator.swift in Sources */, + 6414950A2C3E094B00C9A613 /* VerifiedAgeViewModel.swift in Sources */, 7345AD232703D9C30020AFB1 /* UserProfileImageLoader.swift in Sources */, 7345AD1E2703D9C30020AFB1 /* UserProfileImageView.swift in Sources */, 736F49BC270E102C00580053 /* BirthdayViewModel.swift in Sources */, + 6414950C2C3E0C7A00C9A613 /* VerificationLoader.swift in Sources */, 736F49BA270E05E200580053 /* BirthdayLoader.swift in Sources */, 7345AD242703D9C30020AFB1 /* UserProfileView.swift in Sources */, 7345AD202703D9C30020AFB1 /* AuthenticationViewModel.swift in Sources */, @@ -432,6 +444,7 @@ buildActionMask = 2147483647; files = ( 73DB41912805FBFD0028B8D3 /* SignInView.swift in Sources */, + 641495082C3C90E100C9A613 /* VerificationView.swift in Sources */, 73DB418B2805FBC40028B8D3 /* BirthdayLoader.swift in Sources */, 73DB418D2805FBD00028B8D3 /* AuthenticationViewModel.swift in Sources */, 73DB418F2805FBF50028B8D3 /* ContentView.swift in Sources */, diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift new file mode 100644 index 00000000..410330b8 --- /dev/null +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/VerificationLoader.swift @@ -0,0 +1,58 @@ +/* + * 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 +import GoogleSignIn + +#if os(iOS) + +/// An observable class for verifying via Google. +final class VerificationLoader: ObservableObject { + private var verifiedAgeViewModel: VerifiedAgeViewModel + + /// Creates an instance of this loader. + /// - parameter verifiedAgeViewModel: The view model to use to set verification status on. + init(verifiedViewAgeModel: VerifiedAgeViewModel) { + self.verifiedAgeViewModel = verifiedViewAgeModel + } + + /// 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 verifyAccountDetail = GIDVerifyAccountDetail() + verifyAccountDetail.verifyAccountDetails(accountDetails, presenting: rootViewController) { + verifyResult, error in + guard let verifyResult else { + self.verifiedAgeViewModel.verificationState = .unverified + print("Error! \(String(describing: error))") + return + } + self.verifiedAgeViewModel.verificationState = .verified(verifyResult) + } + } +} + +#endif diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift new file mode 100644 index 00000000..44315446 --- /dev/null +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/VerifiedAgeViewModel.swift @@ -0,0 +1,53 @@ +/* + * 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 + +#if os(iOS) + +/// A class conforming to `ObservableObject` used to represent a user's verification status. +final class VerifiedAgeViewModel: ObservableObject { + /// The user's account verification status. + /// - note: This will publish updates when its value changes. + @Published var verificationState: VerificationState + + private lazy var loader: VerificationLoader = { + return VerificationLoader(verifiedViewAgeModel: self) + }() + + /// Creates an instance of this view model. + init() { + self.verificationState = .unverified + } + + /// Verifies the user's age is over 18. + func verifyUserAgeOver18() { + loader.verifyUserAgeOver18() + } +} + +extension VerifiedAgeViewModel { + /// An enumeration representing verified status. + enum VerificationState { + /// The user's account is verified and is the associated value of this case. + case verified(GIDVerifiedAccountDetailResult) + /// The user's account is not verified. + case unverified + } +} + +#endif diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift b/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift new file mode 100644 index 00000000..a7849f29 --- /dev/null +++ b/Samples/Swift/DaysUntilBirthday/Shared/Views/VerificationView.swift @@ -0,0 +1,62 @@ +/* + * 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 VerificationView: View { + @ObservedObject var verifiedAgeViewModel: VerifiedAgeViewModel + + var body: some View { + switch verifiedAgeViewModel.verificationState { + case .verified(let result): + VStack(alignment:.leading) { + Text("List of result object properties:") + .font(.headline) + + HStack(alignment: .top) { + Text("Access Token:") + Text(result.accessTokenString ?? "Not available") + } + HStack(alignment: .top) { + Text("Refresh Token:") + Text(result.refreshTokenString ?? "Not available") + } + HStack { + Text("Expiration:") + if let expirationDate = result.expirationDate { + Text(formatDateWithDateFormatter(expirationDate)) + } else { + Text("Not available") + } + } + Spacer() + } + .navigationTitle("Verified Account!") + case .unverified: + ProgressView() + .navigationTitle(NSLocalizedString("Unverified account", + comment: "Unverified account label")) + } + } + + func formatDateWithDateFormatter(_ date: Date) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + return dateFormatter.string(from: date) + } +} diff --git a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift index 93366f47..a82534ed 100644 --- a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift +++ b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift @@ -19,6 +19,9 @@ import GoogleSignIn struct UserProfileView: View { @EnvironmentObject var authViewModel: AuthenticationViewModel +#if os(iOS) + @StateObject var verifiedAgeViewModel = VerifiedAgeViewModel() +#endif @StateObject var birthdayViewModel = BirthdayViewModel() private var user: GIDGoogleUser? { return GIDSignIn.sharedInstance.currentUser @@ -50,6 +53,16 @@ struct UserProfileView: View { return } }) +#if os(iOS) + NavigationLink(NSLocalizedString("Verify My Age", comment: "Verify Age"), + destination: VerificationView(verifiedAgeViewModel: verifiedAgeViewModel) + .onAppear { + verifiedAgeViewModel.verifyUserAgeOver18() + } + .onDisappear { + verifiedAgeViewModel.verificationState = .unverified + }) +#endif Spacer() } .toolbar {