From 234580efcc08d80b1fcb26d64da03fe5868b7be3 Mon Sep 17 00:00:00 2001 From: AkshatGandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:12:10 -0800 Subject: [PATCH 01/11] AMR claim implementation + unit tests (#573) --- GoogleSignIn/Sources/GIDClaim.m | 11 ++++++++++- GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h | 7 +++++++ GoogleSignIn/Tests/Unit/GIDClaimTest.m | 12 ++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/GoogleSignIn/Sources/GIDClaim.m b/GoogleSignIn/Sources/GIDClaim.m index 13792cae..2179ec91 100644 --- a/GoogleSignIn/Sources/GIDClaim.m +++ b/GoogleSignIn/Sources/GIDClaim.m @@ -16,7 +16,8 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h" -NSString * const kAuthTimeClaimName = @"auth_time"; +NSString *const kAuthTimeClaimName = @"auth_time"; +NSString *const kAMRClaimName = @"amr"; // Private interface to declare the internal initializer @interface GIDClaim () @@ -48,6 +49,14 @@ + (instancetype)essentialAuthTimeClaim { return [[self alloc] initWithName:kAuthTimeClaimName essential:YES]; } ++ (instancetype)AMRClaim { + return [[self alloc] initWithName:kAMRClaimName essential:NO]; +} + ++ (instancetype)essentialAMRClaim { + return [[self alloc] initWithName:kAMRClaimName essential:YES]; +} + #pragma mark - NSObject - (BOOL)isEqual:(id)object { diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h index bc266930..8ff96a95 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDClaim.h @@ -19,6 +19,7 @@ NS_ASSUME_NONNULL_BEGIN extern NSString *const kAuthTimeClaimName; +extern NSString *const kAMRClaimName; /** * An object representing a single OIDC claim to be requested for an ID token. @@ -42,6 +43,12 @@ extern NSString *const kAuthTimeClaimName; /// Creates an *essential* "auth_time" claim object. + (instancetype)essentialAuthTimeClaim; +/// Creates a *non-essential* (voluntary) "amr" claim object. ++ (instancetype)AMRClaim; + +/// Creates an *essential* "amr" claim object. ++ (instancetype)essentialAMRClaim; + @end NS_ASSUME_NONNULL_END diff --git a/GoogleSignIn/Tests/Unit/GIDClaimTest.m b/GoogleSignIn/Tests/Unit/GIDClaimTest.m index e014d885..bb955e8c 100644 --- a/GoogleSignIn/Tests/Unit/GIDClaimTest.m +++ b/GoogleSignIn/Tests/Unit/GIDClaimTest.m @@ -33,6 +33,18 @@ - (void)testEssentialAuthTimeClaim_PropertiesAreCorrect { XCTAssertTrue(claim.isEssential); } +- (void)testAMRClaim_PropertiesAreCorrect { + GIDClaim *claim = [GIDClaim AMRClaim]; + XCTAssertEqualObjects(claim.name, kAMRClaimName); + XCTAssertFalse(claim.isEssential); +} + +- (void)testEssentialAMRClaim_PropertiesAreCorrect { + GIDClaim *claim = [GIDClaim essentialAMRClaim]; + XCTAssertEqualObjects(claim.name, kAMRClaimName); + XCTAssertTrue(claim.isEssential); +} + - (void)testEquality_WithEqualClaims { GIDClaim *claim1 = [GIDClaim authTimeClaim]; GIDClaim *claim2 = [GIDClaim authTimeClaim]; From bd48ccb43ce85a908b15e927d60e99814cf9fd6f Mon Sep 17 00:00:00 2001 From: AkshatGandhi <54901287+AkshatG6@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:05:44 -0800 Subject: [PATCH 02/11] Add unit tests for AMR claim (#575) --- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 114 ++++++++++++++++-- .../Tests/Unit/OIDTokenResponse+Testing.h | 13 +- .../Tests/Unit/OIDTokenResponse+Testing.m | 30 ++--- 3 files changed, 116 insertions(+), 41 deletions(-) diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 13b4435e..ac245739 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -160,11 +160,18 @@ static NSString *const kGrantedScope = @"grantedScope"; static NSString *const kNewScope = @"newScope"; -static NSString *const kEssentialAuthTimeClaimsJsonString = +static NSString *const kEssentialAuthTimeClaimJsonString = @"{\"id_token\":{\"auth_time\":{\"essential\":true}}}"; -static NSString *const kNonEssentialAuthTimeClaimsJsonString = +static NSString *const kNonEssentialAuthTimeClaimJsonString = @"{\"id_token\":{\"auth_time\":{\"essential\":false}}}"; +static NSString *const kEssentialAMRClaimJsonString = + @"{\"id_token\":{\"amr\":{\"essential\":true}}}"; +static NSString *const kNonEssentialAMRClaimJsonString = + @"{\"id_token\":{\"amr\":{\"essential\":false}}}"; + +static NSString *const kMultipleClaimsJsonString = + @"{\"id_token\":{\"amr\":{\"essential\":false},\"auth_time\":{\"essential\":false}}}"; #if TARGET_OS_IOS || TARGET_OS_MACCATALYST // This category is used to allow the test to swizzle a private method. @@ -751,7 +758,7 @@ - (void)testOAuthLogin_AdditionalScopes { XCTAssertEqualObjects(_savedAuthorizationRequest.scope, expectedScopeString); } -- (void)testOAuthLogin_WithClaims_FormatsParametersCorrectly { +- (void)testOAuthLogin_WithAuthTimeClaim_FormatsParametersCorrectly { GIDClaim *authTimeClaim = [GIDClaim authTimeClaim]; GIDClaim *essentialAuthTimeClaim = [GIDClaim essentialAuthTimeClaim]; @@ -776,7 +783,7 @@ - (void)testOAuthLogin_WithClaims_FormatsParametersCorrectly { claims:[NSSet setWithObject:essentialAuthTimeClaim]]; XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], - kEssentialAuthTimeClaimsJsonString, + kEssentialAuthTimeClaimJsonString, @"Claims JSON should be correctly formatted"); [self OAuthLoginWithAddScopesFlow:NO @@ -795,12 +802,62 @@ - (void)testOAuthLogin_WithClaims_FormatsParametersCorrectly { claims:[NSSet setWithObject:authTimeClaim]]; XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], - kNonEssentialAuthTimeClaimsJsonString, + kNonEssentialAuthTimeClaimJsonString, + @"Claims JSON should be correctly formatted"); +} + +- (void)testOAuthLogin_WithAMRClaim_FormatsParametersCorrectly { + GIDClaim *AMRClaim = [GIDClaim AMRClaim]; + GIDClaim *essentialAMRClaim = [GIDClaim essentialAMRClaim]; + + OCMStub([_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] + ).andDo(^(NSInvocation *invocation){ + self->_keychainSaved = self->_saveAuthorizationReturnValue; + }); + + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + claimsAsJSONRequired:NO + keychainError:NO + claimsError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO + useAdditionalScopes:NO + additionalScopes:nil + manualNonce:nil + claims:[NSSet setWithObject:essentialAMRClaim]]; + + XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], + kEssentialAMRClaimJsonString, + @"Claims JSON should be correctly formatted"); + + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + claimsAsJSONRequired:NO + keychainError:NO + claimsError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO + useAdditionalScopes:NO + additionalScopes:nil + manualNonce:nil + claims:[NSSet setWithObject:AMRClaim]]; + + XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], + kNonEssentialAMRClaimJsonString, @"Claims JSON should be correctly formatted"); } -- (void)testOAuthLogin_WithClaims_ReturnsIdTokenWithCorrectClaims { +- (void)testOAuthLogin_WithMultipleClaims_FormatsParametersCorrectly { GIDClaim *authTimeClaim = [GIDClaim authTimeClaim]; + GIDClaim *AMRClaim = [GIDClaim AMRClaim]; + NSSet *claims = [NSSet setWithArray:@[authTimeClaim, AMRClaim]]; OCMStub([_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] ).andDo(^(NSInvocation *invocation){ @@ -820,7 +877,37 @@ - (void)testOAuthLogin_WithClaims_ReturnsIdTokenWithCorrectClaims { useAdditionalScopes:NO additionalScopes:nil manualNonce:nil - claims:[NSSet setWithObject:authTimeClaim]]; + claims:claims]; + + XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], + kMultipleClaimsJsonString, + @"Claims JSON should be correctly formatted"); +} + +- (void)testOAuthLogin_WithMultipleClaims_ReturnsIdTokenWithCorrectClaims { + GIDClaim *authTimeClaim = [GIDClaim authTimeClaim]; + GIDClaim *AMRClaim = [GIDClaim AMRClaim]; + NSSet *claims = [NSSet setWithArray:@[authTimeClaim, AMRClaim]]; + + OCMStub([_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] + ).andDo(^(NSInvocation *invocation){ + self->_keychainSaved = self->_saveAuthorizationReturnValue; + }); + + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + claimsAsJSONRequired:NO + keychainError:NO + claimsError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO + useAdditionalScopes:NO + additionalScopes:nil + manualNonce:nil + claims:claims]; XCTAssertNotNil(_signIn.currentUser, @"The currentUser should not be nil after a successful sign-in."); NSString *idTokenString = _signIn.currentUser.idToken.tokenString; @@ -830,10 +917,13 @@ - (void)testOAuthLogin_WithClaims_ReturnsIdTokenWithCorrectClaims { NSData *payloadData = [[NSData alloc] initWithBase64EncodedString:components[1] options:NSDataBase64DecodingIgnoreUnknownCharacters]; - NSDictionary *claims = [NSJSONSerialization JSONObjectWithData:payloadData options:0 error:nil]; - XCTAssertEqualObjects(claims[@"auth_time"], + NSDictionary *receivedClaims = [NSJSONSerialization JSONObjectWithData:payloadData options:0 error:nil]; + XCTAssertEqualObjects(receivedClaims[@"auth_time"], kAuthTime, @"The 'auth_time' claim should be present and correct."); + XCTAssertEqualObjects(receivedClaims[@"amr"], + [OIDTokenResponse stubbedAMRValues], + @"The 'amr' claim should be present and correct."); } - (void)testAddScopes { @@ -963,7 +1053,7 @@ - (void)testAddScopes_WithPreviouslyRequestedClaims { NSArray *expectedScopes = @[kNewScope, kGrantedScope]; XCTAssertEqualObjects(grantedScopes, expectedScopes); XCTAssertEqualObjects(_savedAuthorizationRequest.additionalParameters[@"claims"], - kNonEssentialAuthTimeClaimsJsonString, + kNonEssentialAuthTimeClaimJsonString, @"Claims JSON should be correctly formatted"); [_user verify]; @@ -1688,7 +1778,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow nonce:nonce errorString:authError]; - NSString *idToken = claims ? [OIDTokenResponse fatIDTokenWithAuthTime] : [OIDTokenResponse fatIDToken]; + NSString *idToken = claims ? [OIDTokenResponse fatIDTokenWithClaims] : [OIDTokenResponse fatIDToken]; OIDTokenResponse *tokenResponse = [OIDTokenResponse testInstanceWithIDToken:idToken accessToken:restoredSignIn ? kAccessToken : nil @@ -1958,7 +2048,7 @@ - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow additionalParameters[@"emm_passcode_info_required"] = @"1"; } if (claimsAsJSONRequired) { - additionalParameters[@"claims"] = kNonEssentialAuthTimeClaimsJsonString; + additionalParameters[@"claims"] = kNonEssentialAuthTimeClaimJsonString; } return [additionalParameters copy]; diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h index b8329c67..4ef08862 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h @@ -60,18 +60,13 @@ extern NSString * const kFatPictureURL; refreshToken:(NSString *)refreshToken tokenRequest:(OIDTokenRequest *)tokenRequest; -+ (instancetype)testInstanceWithIDToken:(NSString *)idToken - accessToken:(NSString *)accessToken - expiresIn:(NSNumber *)expiresIn - refreshToken:(NSString *)refreshToken - authTime:(NSString *)authTime - tokenRequest:(OIDTokenRequest *)tokenRequest; - + (NSString *)idToken; + (NSString *)fatIDToken; -+ (NSString *)fatIDTokenWithAuthTime; ++ (NSString *)fatIDTokenWithClaims; + ++ (NSArray *)stubbedAMRValues; /** * @sub The subject of the ID token. @@ -81,6 +76,6 @@ extern NSString * const kFatPictureURL; + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat; -+ (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat authTime:(NSString *)authTime; ++ (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat claims:(BOOL)claims; @end diff --git a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m index bf2a5fa7..4dc8b0ae 100644 --- a/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.m @@ -71,21 +71,6 @@ + (instancetype)testInstanceWithIDToken:(NSString *)idToken expiresIn:(NSNumber *)expiresIn refreshToken:(NSString *)refreshToken tokenRequest:(OIDTokenRequest *)tokenRequest { - return [OIDTokenResponse testInstanceWithIDToken:idToken - accessToken:accessToken - expiresIn:expiresIn - refreshToken:refreshToken - authTime:nil - tokenRequest:tokenRequest]; -} - -+ (instancetype)testInstanceWithIDToken:(NSString *)idToken - accessToken:(NSString *)accessToken - expiresIn:(NSNumber *)expiresIn - refreshToken:(NSString *)refreshToken - authTime:(NSString *)authTime - tokenRequest:(OIDTokenRequest *)tokenRequest { - NSMutableDictionary *parameters = [[NSMutableDictionary alloc] initWithDictionary:@{ @"access_token" : accessToken ?: kAccessToken, @"expires_in" : expiresIn ?: @(kAccessTokenExpiresIn), @@ -109,8 +94,12 @@ + (NSString *)fatIDToken { return [self idTokenWithSub:kUserID exp:@(kIDTokenExpires) fat:YES]; } -+ (NSString *)fatIDTokenWithAuthTime { - return [self idTokenWithSub:kUserID exp:@(kIDTokenExpires) fat:YES authTime:kAuthTime]; ++ (NSString *)fatIDTokenWithClaims { + return [self idTokenWithSub:kUserID exp:@(kIDTokenExpires) fat:YES claims:YES]; +} + ++ (NSArray *)stubbedAMRValues { + return @[ @"pwd", @"mfa", @"otp" ]; } + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp { @@ -120,13 +109,13 @@ + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp { + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat { - return [self idTokenWithSub:kUserID exp:exp fat:fat authTime:nil]; + return [self idTokenWithSub:kUserID exp:exp fat:fat claims:NO]; } + (NSString *)idTokenWithSub:(NSString *)sub exp:(NSNumber *)exp fat:(BOOL)fat - authTime:(NSString *)authTime{ + claims:(BOOL)claims { NSError *error; NSDictionary *headerContents = @{ @"alg" : kAlg, @@ -156,9 +145,10 @@ + (NSString *)idTokenWithSub:(NSString *)sub kFatPictureURLKey : kFatPictureURL, }]; } - if (authTime) { + if (claims) { [payloadContents addEntriesFromDictionary:@{ @"auth_time": kAuthTime, + @"amr": [OIDTokenResponse stubbedAMRValues], }]; } NSData *payloadJson = [NSJSONSerialization dataWithJSONObject:payloadContents From 042c17533f634bfbf48c0c414d82d177e72796d4 Mon Sep 17 00:00:00 2001 From: AkshatGandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:43:42 -0700 Subject: [PATCH 03/11] Gandhiakshat/sbc amr sample app (#597) --- .../project.pbxproj | 10 +++- .../Shared/Models/Claim.swift | 24 ++++++++ .../Services/GoogleSignInAuthenticator.swift | 2 +- .../ViewModels/AuthenticationViewModel.swift | 58 +++++++++++++------ .../iOS/UserProfileView.swift | 32 +++++++++- 5 files changed, 104 insertions(+), 22 deletions(-) create mode 100644 Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj index 4c12414e..7d947429 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -40,6 +40,9 @@ 73DB41932805FC3B0028B8D3 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7173A527F5110F00910319 /* UserProfileView.swift */; }; 73DB41952805FC5F0028B8D3 /* Birthday.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739FCC47270E659A00C92042 /* Birthday.swift */; }; 73DB419628060A9A0028B8D3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7345AD062703D9480020AFB1 /* Assets.xcassets */; }; + F8CE667E2F86AB020044EAFF /* Claim.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CE667D2F86AAFD0044EAFF /* Claim.swift */; }; + F8CE667F2F86AB020044EAFF /* Claim.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CE667D2F86AAFD0044EAFF /* Claim.swift */; }; + F8CE66802F86B6A30044EAFF /* Claim.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CE667D2F86AAFD0044EAFF /* Claim.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -73,6 +76,7 @@ 739FCC45270E467600C92042 /* BirthdayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BirthdayView.swift; sourceTree = ""; }; 739FCC47270E659A00C92042 /* Birthday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Birthday.swift; sourceTree = ""; }; 73DB417E2805F9850028B8D3 /* GoogleSignIn-iOS */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "GoogleSignIn-iOS"; path = ../../..; sourceTree = ""; }; + F8CE667D2F86AAFD0044EAFF /* Claim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Claim.swift; sourceTree = ""; }; FE2F2ABC2800D9C1005EA17F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FE71738027ECFAF400910319 /* DaysUntilBirthday (macOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DaysUntilBirthday (macOS).app"; sourceTree = BUILT_PRODUCTS_DIR; }; FE71738927ECFAF600910319 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -212,6 +216,7 @@ FE7173AD27F65B8500910319 /* Models */ = { isa = PBXGroup; children = ( + F8CE667D2F86AAFD0044EAFF /* Claim.swift */, 739FCC47270E659A00C92042 /* Birthday.swift */, ); path = Models; @@ -378,6 +383,7 @@ 739FCC46270E467600C92042 /* BirthdayView.swift in Sources */, 7345AD1B2703D9C30020AFB1 /* SignInView.swift in Sources */, 7345AD212703D9C30020AFB1 /* GoogleSignInAuthenticator.swift in Sources */, + F8CE667E2F86AB020044EAFF /* Claim.swift in Sources */, 7345AD232703D9C30020AFB1 /* UserProfileImageLoader.swift in Sources */, 7345AD1E2703D9C30020AFB1 /* UserProfileImageView.swift in Sources */, 736F49BC270E102C00580053 /* BirthdayViewModel.swift in Sources */, @@ -394,6 +400,7 @@ buildActionMask = 2147483647; files = ( 73508ED528134C7300ED7FB7 /* Credential.swift in Sources */, + F8CE66802F86B6A30044EAFF /* Claim.swift in Sources */, 73508EC82811BD9C00ED7FB7 /* DaysUntilBirthdayUITests_iOS.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -406,6 +413,7 @@ 73DB418B2805FBC40028B8D3 /* BirthdayLoader.swift in Sources */, 73DB418D2805FBD00028B8D3 /* AuthenticationViewModel.swift in Sources */, 73DB418F2805FBF50028B8D3 /* ContentView.swift in Sources */, + F8CE667F2F86AB020044EAFF /* Claim.swift in Sources */, 73DB418C2805FBC80028B8D3 /* UserProfileImageLoader.swift in Sources */, 73DB418E2805FBD40028B8D3 /* BirthdayViewModel.swift in Sources */, 73DB41952805FC5F0028B8D3 /* Birthday.swift in Sources */, diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift b/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift new file mode 100644 index 00000000..f6164ca4 --- /dev/null +++ b/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift @@ -0,0 +1,24 @@ +/* + * Copyright 2026 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 + +/// A decoded representation of a single ID token claim. +struct Claim: Identifiable { + let key: String + let value: String + var id: String { key } +} diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift index afa9ba49..48a0175f 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift @@ -20,7 +20,7 @@ import GoogleSignIn /// An observable class for authenticating via Google. final class GoogleSignInAuthenticator: ObservableObject { private var authViewModel: AuthenticationViewModel - private var claims: Set = Set([GIDClaim.authTime()]) + private let claims: Set = [GIDClaim.essentialAMR(), GIDClaim.authTime()] /// Creates an instance of this authenticator. /// - parameter authViewModel: The view model this authenticator will set logged in status on. diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift index b528fc68..868a978d 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift @@ -26,15 +26,15 @@ final class AuthenticationViewModel: ObservableObject { return GoogleSignInAuthenticator(authViewModel: self) } - /// The user's `auth_time` as found in `idToken`. - /// - note: If the user is logged out, then this will default to `nil`. - var authTime: Date? { + /// The user's `claims` as found in `idToken`. + /// - note: If the user is logged out, then this will default to empty. + var claims: [Claim] { switch state { case .signedIn(let user): - guard let idToken = user.idToken?.tokenString else { return nil } - return decodeAuthTime(fromJWT: idToken) + guard let idToken = user.idToken?.tokenString else { return [] } + return decodeClaims(fromJwt: idToken) case .signedOut: - return nil + return [] } } @@ -82,23 +82,25 @@ final class AuthenticationViewModel: ObservableObject { @MainActor func addBirthdayReadScope(completion: @escaping () -> Void) { authenticator.addBirthdayReadScope(completion: completion) } - - var formattedAuthTimeString: String? { - guard let date = authTime else { return nil } - let formatter = DateFormatter() - formatter.dateFormat = "MMM d, yyyy 'at' h:mm a" - return formatter.string(from: date) - } } private extension AuthenticationViewModel { - func decodeAuthTime(fromJWT jwt: String) -> Date? { + /// Returns a collection of formatted claim keys and values decoded from a JWT. + func decodeClaims(fromJwt jwt: String) -> [Claim] { let segments = jwt.components(separatedBy: ".") - guard let parts = decodeJWTSegment(segments[1]), - let authTimeInterval = parts["auth_time"] as? TimeInterval else { - return nil + + guard segments.count > 1, + let payload = decodeJWTSegment(segments[1]) + else { + return [] } - return Date(timeIntervalSince1970: authTimeInterval) + + let claims: [Claim?] = [ + formatAuthTime(from: payload), + formatAmr(from: payload) + ] + + return claims.compactMap { $0 } } func decodeJWTSegment(_ segment: String) -> [String: Any]? { @@ -124,6 +126,26 @@ private extension AuthenticationViewModel { } return Data(base64Encoded: base64, options: .ignoreUnknownCharacters) } + + /// Returns the `auth_time` claim from the given JWT, if present. + func formatAuthTime(from payload: [String: Any]) -> Claim? { + guard let authTime = payload["auth_time"] as? TimeInterval + else { + return nil + } + let date = Date(timeIntervalSince1970: authTime) + let formattedDate = DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .medium) + return Claim(key: "auth_time", value: formattedDate) + } + + /// Returns the `amr` claim from the given JWT, if present. + private func formatAmr(from payload: [String: Any]) -> Claim? { + guard let amr = payload["amr"] as? [String] + else { + return nil + } + return Claim(key: "amr", value: amr.joined(separator: ", ")) + } } extension AuthenticationViewModel { diff --git a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift index 256b777b..16392cc5 100644 --- a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift +++ b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift @@ -17,6 +17,27 @@ import SwiftUI import GoogleSignIn +/// A view that displays a list of ID token claims. +struct ClaimsListView: View { + let claims: [Claim] + var body: some View { + List(claims) { claim in + VStack(alignment: .leading, spacing: 4) { + Text(claim.key) + .font(.caption) + .foregroundColor(.secondary) + .fontWeight(.semibold) + + Text(claim.value) + .font(.body) + .lineLimit(4) + } + .padding(.vertical, 4) + } + .navigationTitle("ID Token Claims") + } +} + struct UserProfileView: View { @EnvironmentObject var authViewModel: AuthenticationViewModel @StateObject var birthdayViewModel = BirthdayViewModel() @@ -35,8 +56,15 @@ struct UserProfileView: View { Text(userProfile.name) .font(.headline) Text(userProfile.email) - if let authTimeString = authViewModel.formattedAuthTimeString { - Text("Last sign-in date: \(authTimeString)") + + if !authViewModel.claims.isEmpty { + NavigationLink(destination: ClaimsListView(claims: authViewModel.claims)) { + HStack { + Image(systemName: "list.bullet.rectangle.portrait") + Text("View ID Token Claims") + } + .foregroundColor(.blue) + } } } } From 3044684a1a00d54949b4944ab9ce66cdbfb4748d Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Mon, 4 May 2026 18:38:36 -0700 Subject: [PATCH 04/11] Merge main and resolve conflicts --- GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m | 1 - 1 file changed, 1 deletion(-) diff --git a/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m b/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m index 98c317a6..c971b115 100644 --- a/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m +++ b/GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m @@ -57,7 +57,6 @@ + (instancetype)testInstanceWithIDToken:(NSString *)idToken expiresIn:@(accessTokenExpiresIn) refreshToken:refreshToken refreshExpiresIn:@(refreshTokenExpiresIn) - authTime:nil tokenRequest:nil]; return [self testInstanceWithTokenResponse:newResponse]; } From 555cb914fceafd13a78b5443deb7e82938ac0a92 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:21:24 -0700 Subject: [PATCH 05/11] DO NOT SUBMIT - THIS IS A TEST BRANCH --- Samples/Swift/DaysUntilBirthday/Shared/DaysUntilBirthday.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/DaysUntilBirthday.swift b/Samples/Swift/DaysUntilBirthday/Shared/DaysUntilBirthday.swift index afa3083f..5092961b 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/DaysUntilBirthday.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/DaysUntilBirthday.swift @@ -43,3 +43,6 @@ struct DaysUntilBirthday: App { } } } + + +// testing From b0f4031d38e5be81fa1cf09764afc32f476af7cc Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:45:58 -0700 Subject: [PATCH 06/11] DO NOT SUBMIT - THIS IS A TEST BRANCH --- .../Shared/Services/GoogleSignInAuthenticator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift index 48a0175f..33e4c535 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift @@ -20,7 +20,7 @@ import GoogleSignIn /// An observable class for authenticating via Google. final class GoogleSignInAuthenticator: ObservableObject { private var authViewModel: AuthenticationViewModel - private let claims: Set = [GIDClaim.essentialAMR(), GIDClaim.authTime()] + private let claims: Set = [GIDClaim.authTime()] /// Creates an instance of this authenticator. /// - parameter authViewModel: The view model this authenticator will set logged in status on. From 13dc4b53d4bc63873057337e37b776482550abd5 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:04:34 -0700 Subject: [PATCH 07/11] DO NOT SUBMIT - THIS IS A TEST BRANCH --- .../DaysUntilBirthdayUITests_iOS.swift | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift index 1c80701c..76ce1ac0 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift @@ -126,11 +126,15 @@ extension DaysUntilBirthdayUITests_iOS { /// This will assumme the full flow where a user must type in an email and /// password to sign in with. func signInForTheFirstTime() -> Bool { - guard sampleApp.textFields["Email or phone"] - .waitForExistence(timeout: timeout) else { + let emailTextField = sampleApp.textFields["Email or phone"] + guard emailTextField.waitForExistence(timeout: timeout) else { XCTFail("Failed to find email textfield") return false } + + // 1. Explicitly tap the email field so the keyboard appears + emailTextField.tap() + guard sampleApp .keyboards .element @@ -140,14 +144,18 @@ extension DaysUntilBirthdayUITests_iOS { return false } - sampleApp.textFields["Email or phone"].typeText(Credential.email.rawValue) + emailTextField.typeText(Credential.email.rawValue) sampleApp.keyboards.element.buttons["return"].tap() - guard sampleApp.secureTextFields["Enter your password"] - .waitForExistence(timeout: timeout) else { + let passwordTextField = sampleApp.secureTextFields["Enter your password"] + guard passwordTextField.waitForExistence(timeout: timeout) else { XCTFail("Failed to find password textfield") return false } + + // 2. Explicitly tap the password field so its keyboard appears + passwordTextField.tap() + guard sampleApp .keyboards .element @@ -157,9 +165,7 @@ extension DaysUntilBirthdayUITests_iOS { return false } - sampleApp - .secureTextFields["Enter your password"] - .typeText(Credential.password.rawValue) + passwordTextField.typeText(Credential.password.rawValue) sampleApp.keyboards.element.buttons["return"].tap() if sampleApp From 07778a21e4b34ad6553149afe9004d4f48d9ce40 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:14:56 -0700 Subject: [PATCH 08/11] DO NOT SUBMIT - THIS IS A TEST BRANCH --- .../DaysUntilBirthdayUITests_iOS.swift | 31 +++---------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift index 76ce1ac0..d9b7f7d9 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift @@ -132,20 +132,9 @@ extension DaysUntilBirthdayUITests_iOS { return false } - // 1. Explicitly tap the email field so the keyboard appears + // Tap to focus, then type the email and automatically hit the enter key using "\n" emailTextField.tap() - - guard sampleApp - .keyboards - .element - .buttons["return"] - .waitForExistence(timeout: timeout) else { - XCTFail("Failed to find 'return' button") - return false - } - - emailTextField.typeText(Credential.email.rawValue) - sampleApp.keyboards.element.buttons["return"].tap() + emailTextField.typeText(Credential.email.rawValue + "\n") let passwordTextField = sampleApp.secureTextFields["Enter your password"] guard passwordTextField.waitForExistence(timeout: timeout) else { @@ -153,20 +142,9 @@ extension DaysUntilBirthdayUITests_iOS { return false } - // 2. Explicitly tap the password field so its keyboard appears + // Tap to focus, then type the password and automatically hit the enter key using "\n" passwordTextField.tap() - - guard sampleApp - .keyboards - .element - .buttons["return"] - .waitForExistence(timeout: timeout) else { - XCTFail("Failed to find 'return' button") - return false - } - - passwordTextField.typeText(Credential.password.rawValue) - sampleApp.keyboards.element.buttons["return"].tap() + passwordTextField.typeText(Credential.password.rawValue + "\n") if sampleApp .staticTexts[passwordManagerPrompt] @@ -188,6 +166,7 @@ extension DaysUntilBirthdayUITests_iOS { return true } + /// Signs in expecting a prior sign in. /// @discussion /// This will check that there is a `Credential.email` in a list to select and From dfeeccb929f633a752040aa525d17357c7506997 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:00:17 -0700 Subject: [PATCH 09/11] DO NOT SUBMIT - THIS IS A TEST BRANCH --- .../DaysUntilBirthdayUITests_iOS.swift | 123 +++++++++--------- 1 file changed, 63 insertions(+), 60 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift index d9b7f7d9..bf1ee63c 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift @@ -126,47 +126,52 @@ extension DaysUntilBirthdayUITests_iOS { /// This will assumme the full flow where a user must type in an email and /// password to sign in with. func signInForTheFirstTime() -> Bool { - let emailTextField = sampleApp.textFields["Email or phone"] - guard emailTextField.waitForExistence(timeout: timeout) else { - XCTFail("Failed to find email textfield") - return false - } + let emailField = sampleApp.textFields["Email or phone"] + guard emailField.waitForExistence(timeout: timeout) else { + XCTFail("Failed to find email textfield") + return false + } - // Tap to focus, then type the email and automatically hit the enter key using "\n" - emailTextField.tap() - emailTextField.typeText(Credential.email.rawValue + "\n") + // 1. Explicitly tap the field to bring up the keyboard + emailField.tap() - let passwordTextField = sampleApp.secureTextFields["Enter your password"] - guard passwordTextField.waitForExistence(timeout: timeout) else { - XCTFail("Failed to find password textfield") - return false - } + // 2. Type the text and append "\n" to simulate tapping the return key + emailField.typeText("\(Credential.email.rawValue)\n") + + let passwordField = sampleApp.secureTextFields["Enter your password"] + guard passwordField.waitForExistence(timeout: timeout) else { + XCTFail("Failed to find password textfield") + return false + } - // Tap to focus, then type the password and automatically hit the enter key using "\n" - passwordTextField.tap() - passwordTextField.typeText(Credential.password.rawValue + "\n") + // 1. Tap the password field to ensure focus + passwordField.tap() - if sampleApp - .staticTexts[passwordManagerPrompt] - .waitForExistence(timeout: timeout) { - guard sampleApp - .buttons[notNowText] - .waitForExistence(timeout: timeout) else { - XCTFail("Failed to find \(notNowText) button") - return false + // 2. Type password and append "\n" + passwordField.typeText("\(Credential.password.rawValue)\n") + + // 3. Handle System Password Prompt via SpringBoard (if it's an Apple system alert) + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + let notNowButton = springboard.buttons[notNowText] + + // Use a short timeout here so the test doesn't hang too long if the prompt doesn't appear + if notNowButton.waitForExistence(timeout: 3) { + notNowButton.tap() + } else { + // Fallback: Check inside sampleApp just in case it's a custom in-app prompt + if sampleApp.buttons[notNowText].waitForExistence(timeout: 1) { + sampleApp.buttons[notNowText].tap() + } } - sampleApp.buttons[notNowText].tap() - } - // Proceed through sign-in disclaimer and/or access request view(s) if needed - handleSignInDisclaimerIfNeeded() - handleAccessRequestIfNeeded() - handleReturningUserSignInDisclaimerIfNeeded() + // Proceed through sign-in disclaimer and/or access request view(s) if needed + handleSignInDisclaimerIfNeeded() + handleAccessRequestIfNeeded() + handleReturningUserSignInDisclaimerIfNeeded() - return true + return true } - /// Signs in expecting a prior sign in. /// @discussion /// This will check that there is a `Credential.email` in a list to select and @@ -252,41 +257,39 @@ extension DaysUntilBirthdayUITests_iOS { } /// Proceeds through the view with header "Days Until Birthday wants additional access to your Google Account" if needed. - func handleAccessRequestIfNeeded() { - let currentlyShowingAdditionalAccessRequest = sampleApp.staticTexts[additionalAccessHeaderText] - .waitForExistence(timeout: timeout) && sampleApp.staticTexts[appTrustWarningText] - .waitForExistence(timeout: timeout) && - sampleApp.buttons["Continue"] - .waitForExistence(timeout: timeout) - - if currentlyShowingAdditionalAccessRequest { - sampleApp.buttons["Continue"].tap() + /// Proceeds through the view with header "Days Until Birthday wants additional access to your Google Account" if needed. + func handleAccessRequestIfNeeded() { + // Use a short 3-second timeout for optional checks so the test doesn't hang + guard sampleApp.staticTexts[additionalAccessHeaderText].waitForExistence(timeout: 3) else { + return // View didn't appear, move on immediately + } + + // If the header exists, we can assume the rest of the view is there + if sampleApp.staticTexts[appTrustWarningText].exists && sampleApp.buttons["Continue"].exists { + sampleApp.buttons["Continue"].tap() + } } - } - /// Proceeds through the sign-in disclaimer view if needed. - func handleSignInDisclaimerIfNeeded() { - let currentlyShowingSignInDisclaimer = sampleApp.staticTexts[signInDisclaimerHeaderText] - .waitForExistence(timeout: timeout) && - sampleApp.buttons["Continue"] - .waitForExistence(timeout: timeout) + /// Proceeds through the sign-in disclaimer view if needed. + func handleSignInDisclaimerIfNeeded() { + guard sampleApp.staticTexts[signInDisclaimerHeaderText].waitForExistence(timeout: 3) else { + return + } - if currentlyShowingSignInDisclaimer { - sampleApp.buttons["Continue"].tap() + if sampleApp.buttons["Continue"].exists { + sampleApp.buttons["Continue"].tap() + } } - } - func handleReturningUserSignInDisclaimerIfNeeded() { - let currentlyShowingReturningUserSignInDisclaimer = - sampleApp.staticTexts[returningUserSignInDisclaimerHeaderText] - .waitForExistence(timeout: timeout) && - sampleApp.buttons["Continue"] - .waitForExistence(timeout: timeout) + func handleReturningUserSignInDisclaimerIfNeeded() { + guard sampleApp.staticTexts[returningUserSignInDisclaimerHeaderText].waitForExistence(timeout: 3) else { + return + } - if currentlyShowingReturningUserSignInDisclaimer { - sampleApp.buttons["Continue"].tap() + if sampleApp.buttons["Continue"].exists { + sampleApp.buttons["Continue"].tap() + } } - } /// This method looks for an account in the current view that reflects an already-signed-in state, and taps it. /// - returns: true if the signed-in account was found and tapped, otherwise returns false. From 86e30581fb5dd6f9f0253c4bae1b950d17ea4ce8 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:11:59 -0700 Subject: [PATCH 10/11] DO NOT SUBMIT - THIS IS A TEST BRANCH --- .../DaysUntilBirthdayUITests_iOS.swift | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift index bf1ee63c..bdd0d549 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift @@ -257,39 +257,41 @@ extension DaysUntilBirthdayUITests_iOS { } /// Proceeds through the view with header "Days Until Birthday wants additional access to your Google Account" if needed. - /// Proceeds through the view with header "Days Until Birthday wants additional access to your Google Account" if needed. - func handleAccessRequestIfNeeded() { - // Use a short 3-second timeout for optional checks so the test doesn't hang - guard sampleApp.staticTexts[additionalAccessHeaderText].waitForExistence(timeout: 3) else { - return // View didn't appear, move on immediately - } - - // If the header exists, we can assume the rest of the view is there - if sampleApp.staticTexts[appTrustWarningText].exists && sampleApp.buttons["Continue"].exists { - sampleApp.buttons["Continue"].tap() - } + func handleAccessRequestIfNeeded() { + let currentlyShowingAdditionalAccessRequest = sampleApp.staticTexts[additionalAccessHeaderText] + .waitForExistence(timeout: timeout) && sampleApp.staticTexts[appTrustWarningText] + .waitForExistence(timeout: timeout) && + sampleApp.buttons["Continue"] + .waitForExistence(timeout: timeout) + + if currentlyShowingAdditionalAccessRequest { + sampleApp.buttons["Continue"].tap() } + } - /// Proceeds through the sign-in disclaimer view if needed. - func handleSignInDisclaimerIfNeeded() { - guard sampleApp.staticTexts[signInDisclaimerHeaderText].waitForExistence(timeout: 3) else { - return - } + /// Proceeds through the sign-in disclaimer view if needed. + func handleSignInDisclaimerIfNeeded() { + let currentlyShowingSignInDisclaimer = sampleApp.staticTexts[signInDisclaimerHeaderText] + .waitForExistence(timeout: timeout) && + sampleApp.buttons["Continue"] + .waitForExistence(timeout: timeout) - if sampleApp.buttons["Continue"].exists { - sampleApp.buttons["Continue"].tap() - } + if currentlyShowingSignInDisclaimer { + sampleApp.buttons["Continue"].tap() } + } - func handleReturningUserSignInDisclaimerIfNeeded() { - guard sampleApp.staticTexts[returningUserSignInDisclaimerHeaderText].waitForExistence(timeout: 3) else { - return - } + func handleReturningUserSignInDisclaimerIfNeeded() { + let currentlyShowingReturningUserSignInDisclaimer = + sampleApp.staticTexts[returningUserSignInDisclaimerHeaderText] + .waitForExistence(timeout: timeout) && + sampleApp.buttons["Continue"] + .waitForExistence(timeout: timeout) - if sampleApp.buttons["Continue"].exists { - sampleApp.buttons["Continue"].tap() - } + if currentlyShowingReturningUserSignInDisclaimer { + sampleApp.buttons["Continue"].tap() } + } /// This method looks for an account in the current view that reflects an already-signed-in state, and taps it. /// - returns: true if the signed-in account was found and tapped, otherwise returns false. From 82e3ec1aa56ce348a0e6897a6b1d14681ca3041b Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:31:37 -0700 Subject: [PATCH 11/11] DO NOT SUBMIT - THIS IS A TEST BRANCH --- .../DaysUntilBirthdayUITests_iOS.swift | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift index bdd0d549..3e50c0ec 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayUITests(iOS)/DaysUntilBirthdayUITests_iOS.swift @@ -258,39 +258,24 @@ extension DaysUntilBirthdayUITests_iOS { /// Proceeds through the view with header "Days Until Birthday wants additional access to your Google Account" if needed. func handleAccessRequestIfNeeded() { - let currentlyShowingAdditionalAccessRequest = sampleApp.staticTexts[additionalAccessHeaderText] - .waitForExistence(timeout: timeout) && sampleApp.staticTexts[appTrustWarningText] - .waitForExistence(timeout: timeout) && - sampleApp.buttons["Continue"] - .waitForExistence(timeout: timeout) - - if currentlyShowingAdditionalAccessRequest { - sampleApp.buttons["Continue"].tap() - } - } + let continueButton = sampleApp.buttons["Continue"] + if continueButton.waitForExistence(timeout: 5) { + continueButton.tap() + } } /// Proceeds through the sign-in disclaimer view if needed. func handleSignInDisclaimerIfNeeded() { - let currentlyShowingSignInDisclaimer = sampleApp.staticTexts[signInDisclaimerHeaderText] - .waitForExistence(timeout: timeout) && - sampleApp.buttons["Continue"] - .waitForExistence(timeout: timeout) - - if currentlyShowingSignInDisclaimer { - sampleApp.buttons["Continue"].tap() - } + let continueButton = sampleApp.buttons["Continue"] + if continueButton.waitForExistence(timeout: 5) { + continueButton.tap() + } } func handleReturningUserSignInDisclaimerIfNeeded() { - let currentlyShowingReturningUserSignInDisclaimer = - sampleApp.staticTexts[returningUserSignInDisclaimerHeaderText] - .waitForExistence(timeout: timeout) && - sampleApp.buttons["Continue"] - .waitForExistence(timeout: timeout) - - if currentlyShowingReturningUserSignInDisclaimer { - sampleApp.buttons["Continue"].tap() - } + let continueButton = sampleApp.buttons["Continue"] + if continueButton.waitForExistence(timeout: 5) { + continueButton.tap() + } } /// This method looks for an account in the current view that reflects an already-signed-in state, and taps it.