diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index e215d112..21e07d69 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -37,6 +37,42 @@ jobs: -scheme GoogleSignIn-Package \ -destination "platform=OS X" + spm-app-check-11: + # Package.swift accepts app-check 11.x and 12.x, but Package.resolved is gitignored, so + # spm-build-test always gets the newest (12.x). Pin the latest 11.x (what Firebase 12 + # apps resolve) to keep SPM + AppCheckCore 11 covered. iOS only: all App Check code is + # behind TARGET_OS_IOS && !TARGET_OS_MACCATALYST. + runs-on: macos-15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer + - name: Pin app-check to the latest 11.x + run: | + version=$(git ls-remote --tags --refs https://github.com/google/app-check.git '11.*' \ + | sed 's|.*refs/tags/||' | sort -V | tail -1) + echo "Pinning app-check $version" + # `resolve --version` can only re-pin an already-resolved dependency. + swift package resolve + swift package resolve app-check --version "$version" + - name: Build unit test target + run: | + xcodebuild \ + -scheme GoogleSignIn-Package \ + -sdk iphonesimulator \ + -destination "platform=iOS Simulator,name=iPhone 16" \ + -onlyUsePackageVersionsFromResolvedFile \ + build-for-testing + - name: Run unit test target + run: | + xcodebuild \ + -scheme GoogleSignIn-Package \ + -sdk iphonesimulator \ + -destination "platform=iOS Simulator,name=iPhone 16,OS=18.5" \ + -onlyUsePackageVersionsFromResolvedFile \ + test-without-building + + xcode-27-preview-archive: # Archive every supported platform against the Xcode 27 preview image. The # image is preview, so this job does not block merges. diff --git a/CHANGELOG.md b/CHANGELOG.md index 995c6b85..7a19b39d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Unreleased - Fix a crash when a server error response carries a non-string value under its `error` key. The EMM error handler sent `-hasPrefix:` to whatever value was present, raising an unrecognized selector exception on a number, array or object. - Fix a custom `nonce` and requested token `claims` being dropped when a sign-in is continued after a Device Policy app restart. +- Add Swift Package Manager support for AppCheckCore 12.x, enabling compatibility with Firebase 13. CocoaPods installations remain restricted to AppCheckCore 11.x. ([#626](https://github.com/google/GoogleSignIn-iOS/pull/626)) +- Fix a failed App Check preparation being recorded as successful, which made later calls to `-[GIDSignIn configureWithCompletion:]` report success without retrying. ([#626](https://github.com/google/GoogleSignIn-iOS/pull/626)) +- With AppCheckCore 11.x (including all CocoaPods installations), a custom App Check provider that returns neither a token nor an error is now treated as success instead of failing with `kGIDAppCheckUnexpectedError`. ([#626](https://github.com/google/GoogleSignIn-iOS/pull/626)) - Fix a data race on `GIDGoogleUser`'s tokens. Concurrent token updates could write the access, refresh and ID tokens from different threads at once, and readers could observe a partially updated set. The three tokens are now published together as one snapshot. - `GIDGoogleUser`'s `accessToken`, `refreshToken` and `idToken` are now all derived from that snapshot, so key-value observers of any one of them are notified whenever any of them changes, not only when that property's own value changes. - Key-value observers of those properties are notified while `GIDGoogleUser` holds its internal lock. Calling back into the same user from the observer is fine, but an observer must not synchronously wait (for example with `dispatch_sync`) on another thread that is updating the same user. diff --git a/GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.h b/GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.h index e294273f..5a862c16 100644 --- a/GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.h +++ b/GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.h @@ -16,7 +16,7 @@ #import #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST -#import +@import AppCheckCore; @class GACAppCheckToken; diff --git a/GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.m b/GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.m index 903fc3ff..44e54692 100644 --- a/GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.m +++ b/GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.m @@ -16,7 +16,7 @@ #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST -#import +@import AppCheckCore; NSUInteger const kGIDAppCheckProviderFakeError = 1; diff --git a/GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.m b/GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.m index a886eec4..c1bd33c9 100644 --- a/GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.m +++ b/GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.m @@ -18,13 +18,8 @@ #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST -#import -#import -#import -#import -#import +@import AppCheckCore; -#import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAppCheckError.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h" @@ -115,15 +110,8 @@ - (void)prepareForAppCheckWithCompletion:(nullable GIDAppCheckPrepareCompletion) } [self.appCheck limitedUseTokenWithCompletion:^(GACAppCheckTokenResult * _Nonnull result) { - NSError * __block maybeError = result.error; @synchronized (self) { - if (!result.token && !result.error) { - maybeError = [NSError errorWithDomain:kGIDAppCheckErrorDomain - code:kGIDAppCheckUnexpectedError - userInfo:nil]; - } - - if (result.token) { + if (!result.error) { [self.userDefaults setBool:YES forKey:kGIDAppCheckPreparedKey]; } @@ -134,7 +122,7 @@ - (void)prepareForAppCheckWithCompletion:(nullable GIDAppCheckPrepareCompletion) for (GIDAppCheckPrepareCompletion savedCompletion in callbacks) { - savedCompletion(maybeError); + savedCompletion(result.error); } }]; }); @@ -143,7 +131,7 @@ - (void)prepareForAppCheckWithCompletion:(nullable GIDAppCheckPrepareCompletion) - (void)getLimitedUseTokenWithCompletion:(nullable GIDAppCheckTokenCompletion)completion { dispatch_async(self.workerQueue, ^{ [self.appCheck limitedUseTokenWithCompletion:^(GACAppCheckTokenResult * _Nonnull result) { - if (result.token) { + if (!result.error) { [self.userDefaults setBool:YES forKey:kGIDAppCheckPreparedKey]; } if (completion) { diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index dc26c07f..38f4514f 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -30,7 +30,7 @@ #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h" #import "GoogleSignIn/Sources/GIDClaimsInternalOptions.h" #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST -#import +@import AppCheckCore; #import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h" #import "GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h" #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" diff --git a/GoogleSignIn/Tests/Unit/GIDAppCheckTest.m b/GoogleSignIn/Tests/Unit/GIDAppCheckTest.m index 7c0610ae..210dcee9 100644 --- a/GoogleSignIn/Tests/Unit/GIDAppCheckTest.m +++ b/GoogleSignIn/Tests/Unit/GIDAppCheckTest.m @@ -17,7 +17,7 @@ #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import -#import +@import AppCheckCore; #import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h" #import "GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.h" #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAppCheckError.h" @@ -65,6 +65,42 @@ - (void)testGetLimitedUseTokenFailureReturnsPlaceholder { }]; [self waitForExpectations:@[tokenFailExpectation] timeout:timeout]; + + XCTAssertFalse([appCheck isPrepared]); +} + +- (void)testPrepareFailureDoesNotMarkPrepared { + XCTestExpectation *firstPrepareFailExpectation = + [self expectationWithDescription:@"First App Check prepare fail"]; + NSError *expectedError = [NSError errorWithDomain:kGIDAppCheckErrorDomain + code:kGIDAppCheckProviderFakeError + userInfo:nil]; + + GIDAppCheckProviderFake *fakeProvider = + [[GIDAppCheckProviderFake alloc] initWithAppCheckToken:nil error:expectedError]; + GIDAppCheck *appCheck = [[GIDAppCheck alloc] initWithAppCheckProvider:fakeProvider + userDefaults:self.userDefaults]; + + [appCheck prepareForAppCheckWithCompletion:^(NSError * _Nullable error) { + XCTAssertEqualObjects(expectedError, error); + [firstPrepareFailExpectation fulfill]; + }]; + + [self waitForExpectations:@[firstPrepareFailExpectation] timeout:timeout]; + + XCTAssertFalse([appCheck isPrepared]); + + XCTestExpectation *secondPrepareFailExpectation = + [self expectationWithDescription:@"Second App Check prepare fail"]; + + [appCheck prepareForAppCheckWithCompletion:^(NSError * _Nullable error) { + XCTAssertEqualObjects(expectedError, error); + [secondPrepareFailExpectation fulfill]; + }]; + + [self waitForExpectations:@[secondPrepareFailExpectation] timeout:timeout]; + + XCTAssertFalse([appCheck isPrepared]); } - (void)testIsPreparedError { diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 630cd484..b633cc32 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -35,7 +35,7 @@ #import "GoogleSignIn/Sources/GIDClaimsInternalOptions.h" #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST -#import +@import AppCheckCore; #import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h" #import "GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.h" #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h" @@ -421,13 +421,16 @@ - (void)testConfigureSucceeds { } } -- (void)testConfigureFailsNoTokenOrError { +- (void)testConfigureFailsWithProviderError { if (@available(iOS 14, *)) { XCTestExpectation *configureFailsExpecation = [self expectationWithDescription:@"Configure fails expectation"]; + NSError *expectedError = [NSError errorWithDomain:kGIDAppCheckErrorDomain + code:kGIDAppCheckProviderFakeError + userInfo:nil]; GIDAppCheckProviderFake *fakeProvider = - [[GIDAppCheckProviderFake alloc] initWithAppCheckToken:nil error:nil]; + [[GIDAppCheckProviderFake alloc] initWithAppCheckToken:nil error:expectedError]; GIDAppCheck *appCheck = [[GIDAppCheck alloc] initWithAppCheckProvider:fakeProvider userDefaults:_testUserDefaults]; @@ -436,10 +439,10 @@ - (void)testConfigureFailsNoTokenOrError { authStateMigrationService:_authStateMigrationService appCheck:appCheck]; - // Should fail if missing both token and error + // Should fail and stay unprepared when the provider returns an error [signIn configureWithCompletion:^(NSError * _Nullable error) { XCTAssertNotNil(error); - XCTAssertEqual(error.code, kGIDAppCheckUnexpectedError); + XCTAssertEqualObjects(error, expectedError); [configureFailsExpecation fulfill]; }]; diff --git a/Package.swift b/Package.swift index bad348a0..03f18d99 100644 --- a/Package.swift +++ b/Package.swift @@ -46,7 +46,7 @@ let package = Package( from: "3.0.0"), .package( url: "https://github.com/google/app-check.git", - from: "11.0.0"), + "11.0.0" ..< "13.0.0"), .package( url: "https://github.com/google/GTMAppAuth.git", from: "6.0.0"),