Parcourir la source

Merge App Check beta branch to prep for launch (#428)

mdmathias il y a 1 an
Parent
commit
aab6d2e5c8
43 fichiers modifiés avec 3240 ajouts et 72 suppressions
  1. 74 1
      .github/workflows/integration_tests.yml
  2. 7 4
      .github/workflows/unit_tests.yml
  3. 3 0
      .gitignore
  4. 36 0
      CHANGELOG.md
  5. 2 1
      GoogleSignIn.podspec
  6. 45 0
      GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.h
  7. 56 0
      GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.m
  8. 75 0
      GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h
  9. 180 0
      GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.m
  10. 36 0
      GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h
  11. 51 0
      GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.m
  12. 177 37
      GoogleSignIn/Sources/GIDSignIn.m
  13. 9 4
      GoogleSignIn/Sources/GIDSignIn_Private.h
  14. 70 0
      GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.h
  15. 119 0
      GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.m
  16. 35 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDAppCheckError.h
  17. 35 6
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h
  18. 3 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h
  19. 189 0
      GoogleSignIn/Tests/Unit/GIDAppCheckTest.m
  20. 88 14
      GoogleSignIn/Tests/Unit/GIDSignInTest.m
  21. 7 1
      Package.swift
  22. 1 1
      Samples/ObjC/SignInSample/Podfile
  23. 3 1
      Samples/ObjC/SignInSample/SignInSample.xcodeproj/project.pbxproj
  24. 8 2
      Samples/ObjC/SignInSample/SignInSampleForPod.xcodeproj/project.pbxproj
  25. 572 0
      Samples/Swift/AppAttestExample/AppAttestExample.xcodeproj/project.pbxproj
  26. 106 0
      Samples/Swift/AppAttestExample/AppAttestExample.xcodeproj/xcshareddata/xcschemes/AppAttestExample.xcscheme
  27. 40 0
      Samples/Swift/AppAttestExample/AppAttestExample.xctestplan
  28. 8 0
      Samples/Swift/AppAttestExample/AppAttestExample/AppAttestExample.entitlements
  29. 57 0
      Samples/Swift/AppAttestExample/AppAttestExample/AppAttestExampleApp.swift
  30. 58 0
      Samples/Swift/AppAttestExample/AppAttestExample/AppCheckSecretReader.swift
  31. 111 0
      Samples/Swift/AppAttestExample/AppAttestExample/BirthdayLoader.swift
  32. 218 0
      Samples/Swift/AppAttestExample/AppAttestExample/ContentView.swift
  33. 23 0
      Samples/Swift/AppAttestExample/AppAttestExample/Info.plist
  34. 11 0
      Samples/Swift/AppAttestExample/AppAttestExample/Preview Content/Preview Assets.xcassets/Assets/AccentColor.colorset/Contents.json
  35. 13 0
      Samples/Swift/AppAttestExample/AppAttestExample/Preview Content/Preview Assets.xcassets/Assets/AppIcon.appiconset/Contents.json
  36. 6 0
      Samples/Swift/AppAttestExample/AppAttestExample/Preview Content/Preview Assets.xcassets/Assets/Contents.json
  37. 6 0
      Samples/Swift/AppAttestExample/AppAttestExample/Preview Content/Preview Assets.xcassets/Contents.json
  38. 16 0
      Samples/Swift/AppAttestExample/AppAttestExample/Secrets/AppCheckDefaultSecrets.xcconfig
  39. 471 0
      Samples/Swift/AppAttestExample/AppAttestExampleForPod.xcodeproj/project.pbxproj
  40. 113 0
      Samples/Swift/AppAttestExample/AppAttestExampleForPod.xcodeproj/xcshareddata/xcschemes/AppAttestExample.xcscheme
  41. 30 0
      Samples/Swift/AppAttestExample/AppAttestExampleTests/AppAttestExampleTests.swift
  42. 12 0
      Samples/Swift/AppAttestExample/Podfile
  43. 60 0
      Samples/Swift/AppAttestExample/README.md

+ 74 - 1
.github/workflows/integration_tests.yml

@@ -9,10 +9,48 @@ on:
 
 jobs:
 
+  grab-pr-body:
+    runs-on: ubuntu-latest
+    outputs:
+      PR_BODY: ${{ steps.body.outputs.PR_BODY }}
+    steps:
+    - id: body
+      env:
+        PR_BODY: ${{ github.event.pull_request.body }} 
+      run: |
+        {
+          echo "PR_BODY<<EOF"
+          echo "$PR_BODY" 
+          echo "EOF"
+        } >> "$GITHUB_OUTPUT"
+
+  check-pr-body-for-key:
+    runs-on: ubuntu-latest
+    needs: grab-pr-body
+    outputs:
+      RUN_INTEGRATION: ${{ steps.check_key.outputs.RUN_INTEGRATION }}
+    steps:
+    - id: check_key
+      env:
+        PR_BODY: ${{ needs.grab-pr-body.outputs.PR_BODY }}
+        SKIP_KEY: "SKIP_INTEGRATION_TESTS=YES"
+      name: Check for key and set bool to skip integration tests
+      run: |
+        if [[ "$PR_BODY" == *"$SKIP_KEY"* ]]; then
+          echo "Skipping integration tests for PR body:"
+          echo "$PR_BODY"
+          echo "RUN_INTEGRATION=no" >> "$GITHUB_OUTPUT"
+        else
+          echo "Running integration tests for PR body:"
+          echo "$PR_BODY"
+          echo "RUN_INTEGRATION=yes" >> "$GITHUB_OUTPUT"
+        fi
+
   swift-button-functional-test:
     runs-on: macOS-12
+    needs: check-pr-body-for-key
     # Don't run if triggered by a PR from a fork since our Secrets won't be provided to the runner.
-    if: "!github.event.pull_request.head.repo.fork"
+    if: ${{ needs.check-pr-body-for-key.outputs.RUN_INTEGRATION == 'yes' && !github.event.pull_request.head.repo.fork }}
     defaults:
       run:
         working-directory: Samples/Swift/DaysUntilBirthday
@@ -40,3 +78,38 @@ jobs:
           -destination 'platform=iOS Simulator,name=iPhone 11' \
           EMAIL_SECRET=$EMAIL_SECRET \
           PASSWORD_SECRET=$PASSWORD_SECRET
+
+  app-check-api-token-tests:
+    runs-on: macOS-13
+    # Don't run if triggered by a PR from a fork since our Secrets won't be provided to the runner.
+    if: "!github.event.pull_request.head.repo.fork"
+    defaults:
+      run:
+        working-directory: Samples/Swift/AppAttestExample
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v3
+    - name: Use Xcode 15.0.1
+      run: |
+        sudo xcode-select -s /Applications/Xcode_15.0.1.app
+    - name: Build test target for App Check Example
+      run: |
+        xcodebuild \
+          -project AppAttestExample.xcodeproj \
+          build-for-testing \
+          -scheme AppAttestExample \
+          -sdk iphonesimulator \
+          -destination 'platform=iOS Simulator,name=iPhone 15'
+    - name: Run test target for App Check Example
+      env:
+        AppCheckDebugToken : ${{ secrets.APP_CHECK_DEBUG_TOKEN }}
+        APP_CHECK_WEB_API_KEY : ${{ secrets.APP_CHECK_WEB_API_KEY }}
+      run: |
+        xcodebuild \
+          -project AppAttestExample.xcodeproj \
+          test-without-building \
+          -scheme AppAttestExample \
+          -sdk iphonesimulator \
+          -destination 'platform=iOS Simulator,name=iPhone 15' \
+          AppCheckDebugToken=$AppCheckDebugToken \
+          APP_CHECK_WEB_API_KEY=$APP_CHECK_WEB_API_KEY

+ 7 - 4
.github/workflows/unit_tests.yml

@@ -20,9 +20,10 @@ jobs:
           "",
           "--use-static-frameworks"
         ]
-        include:
-          - podspec: GoogleSignInSwiftSupport.podspec
-            includePodspecFlag: "--include-podspecs='GoogleSignIn.podspec'"
+        # See #400 (https://github.com/google/GoogleSignIn-iOS/issues/400)
+        # include:
+        #  - podspec: GoogleSignInSwiftSupport.podspec
+        #    includePodspecFlag: "--include-podspecs='GoogleSignIn.podspec'"
     steps:
     - uses: actions/checkout@v3
     - name: Update Bundler
@@ -30,9 +31,11 @@ jobs:
     - name: Install Ruby gems with Bundler
       run: bundle install
     - name: Lint podspec using local source
+      # See #400 (https://github.com/google/GoogleSignIn-iOS/issues/400)
       run: |
         pod lib lint ${{ matrix.podspec }} --verbose \
-           ${{ matrix.includePodspecFlag }} ${{ matrix.flag }}
+           --sources=https://cdn.cocoapods.org/ \
+           ${{ matrix.flag }}
 
   spm-build-test:
     runs-on: ${{ matrix.os }}

+ 3 - 0
.gitignore

@@ -16,3 +16,6 @@ Credentials.xcconfig
 Pods/
 gen/
 Podfile.lock
+
+# Firebase App Check Example
+**/GoogleService-Info.plist

+ 36 - 0
CHANGELOG.md

@@ -1,3 +1,13 @@
+# 7.1.0-fac-beta-1.1.0
+- Beta release supporting Firebase App Check tokens used
+to establish your application's integrity while signing in with Google
+- Adds privacy manifest support released in [v7.1.0](https://github.com/google/GoogleSignIn-iOS/releases/tag/7.1.0)
+- Internal
+  - Check integration test for presubmit instruction ([#368](https://github.com/google/GoogleSignIn-iOS/pull/368))
+  - Test skip integration key ([#374](https://github.com/google/GoogleSignIn-iOS/pull/374))
+  - Add Privacy Manifest to App Check Release Branch ([#392](https://github.com/google/GoogleSignIn-iOS/pull/392))
+  - [Add return type to init in GIDFakeFetcherService header](https://github.com/google/GoogleSignIn-iOS/commit/ebf681cac127497da55c932cb5bbf185971a29e7)
+
 # 7.1.0
 - Update to Swift 5.0 in `GoogleSignInSwiftSupport` pod ([#317](https://github.com/google/GoogleSignIn-iOS/pull/317))
 - Documentation updates ([#351](https://github.com/google/GoogleSignIn-iOS/pull/351), [#372](https://github.com/google/GoogleSignIn-iOS/pull/372))
@@ -10,6 +20,32 @@
   - Removes `macos-11` runner in GitHub workflows ([#302](https://github.com/google/GoogleSignIn-iOS/pull/302))
   - Updates button name reference so UI automation tests pass ([#308](https://github.com/google/GoogleSignIn-iOS/pull/308))
 
+# 7.1.0-fac-beta-1.0.0
+- Beta release supporting Firebase App Check tokens used
+to establish your application's integrity while signing in with Google
+- Internal
+  - Update SignInSample Podfile minimum iOS version ([#355](https://github.com/google/GoogleSignIn-iOS/pull/355))
+  - Update AppCheckExample unit test target to pass during continuous integration ([#356](https://github.com/google/GoogleSignIn-iOS/pull/356))
+
+# 7.1.0-fac-eap-1.0.0
+- Early Access Program (EAP) release supporting Firebase App Check tokens used
+to establish your application's integrity while signing in with Google
+  - Use [`-[GIDSignIn configureWithCompletion:]`](https://github.com/google/GoogleSignIn-iOS/blob/7.1.0-fac-eap-1.0.0/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h#L79)
+    to configure GSI to use Firebase App Check as early as possible in your app
+    to minimize latency.
+  - Use [`-[GIDSignIn configureDebugProviderWithAPIKey:completion:]`](https://github.com/google/GoogleSignIn-iOS/blob/7.1.0-fac-eap-1.0.0/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h#L91)
+    in debug builds or continuous integration environments.
+  - New [sample app](https://github.com/google/GoogleSignIn-iOS/tree/7.1.0-fac-eap-1.0.0/Samples/Swift/AppAttestExample)
+    showing example of configuring GSI to use Firebase App Check.
+- Internal
+  - Fix typo in `SFSafariViewController` ([#291](https://github.com/google/GoogleSignIn-iOS/pull/291))
+  - Removes `macos-11` runner in GitHub workflows ([#302](https://github.com/google/GoogleSignIn-iOS/pull/302))
+  - Updates button name reference so UI automation tests pass ([#308](https://github.com/google/GoogleSignIn-iOS/pull/308))
+  - Ensure that `completion` is not nil before calling
+    `-[GIDSignIn restorePreviousSignIn:]` ([#301](https://github.com/google/GoogleSignIn-iOS/pull/301))
+  - Use new [delegate protocol](https://github.com/google/GTMAppAuth/pull/224)
+    from GTMAppAuth 4.0.0 ([#299](https://github.com/google/GoogleSignIn-iOS/pull/299))
+
 # 7.0.0
 - All configuration can now be provided via your `Info.plist` file. ([#228](https://github.com/google/GoogleSignIn-iOS/pull/228))
   - Use the following keys in `<key>KEY</key><string>VALUE</string>` pairs to configure the SDK:

+ 2 - 1
GoogleSignIn.podspec

@@ -13,7 +13,7 @@ The Google Sign-In SDK allows users to sign in with their Google account from th
     :tag => s.version.to_s
   }
   s.swift_version = '4.0'
-  ios_deployment_target = '10.0'
+  ios_deployment_target = '11.0'
   osx_deployment_target = '10.15'
   s.ios.deployment_target = ios_deployment_target
   s.osx.deployment_target = osx_deployment_target
@@ -33,6 +33,7 @@ The Google Sign-In SDK allows users to sign in with their Google account from th
   ]
   s.ios.framework = 'UIKit'
   s.osx.framework = 'AppKit'
+  s.dependency 'AppCheckCore', '>= 10.19.1', '< 11.0'
   s.dependency 'AppAuth', '>= 1.7.3', '< 2.0'
   s.dependency 'GTMAppAuth', '>= 4.1.1', '< 5.0'
   s.dependency 'GTMSessionFetcher/Core', '~> 3.3'

+ 45 - 0
GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.h

@@ -0,0 +1,45 @@
+// Copyright 2023 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 <TargetConditionals.h>
+#import <Foundation/Foundation.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+#import <AppCheckCore/GACAppCheckProvider.h>
+
+@class GACAppCheckToken;
+
+NS_ASSUME_NONNULL_BEGIN
+
+extern NSUInteger const kGIDAppCheckProviderFakeError;
+
+NS_CLASS_AVAILABLE_IOS(14)
+@interface GIDAppCheckProviderFake : NSObject <GACAppCheckProvider>
+
+/// Creates an instance conforming to `GACAppCheckProvider` with the provided app check token and
+/// error.
+///
+/// @param token The `GACAppCheckToken` instance to pass into the completion called from
+///     `getTokenWithCompletion:`. Use `nil` if you would like a placeholder token from
+///     AppCheckCore.
+/// @param error The `NSError` to pass into the completion called from
+///     `getTokenWithCompletion:`.
+- (instancetype)initWithAppCheckToken:(nullable GACAppCheckToken *)token
+                                error:(nullable NSError *)error;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 56 - 0
GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.m

@@ -0,0 +1,56 @@
+// Copyright 2023 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 "GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.h"
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import <AppCheckCore/GACAppCheckToken.h>
+
+NSUInteger const kGIDAppCheckProviderFakeError = 1;
+
+@interface GIDAppCheckProviderFake ()
+
+@property(nonatomic, strong, nullable) GACAppCheckToken *token;
+@property(nonatomic, strong, nullable) NSError *error;
+
+@end
+
+@implementation GIDAppCheckProviderFake
+
+- (instancetype)initWithAppCheckToken:(nullable GACAppCheckToken *)token
+                                error:(nullable NSError *)error {
+  if (self = [super init]) {
+    _token = token;
+    _error = error;
+  }
+  return self;
+}
+
+- (void)getTokenWithCompletion:(void (^)(GACAppCheckToken *, NSError * _Nullable))handler {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    handler(self.token, self.error);
+  });
+}
+
+- (void)getLimitedUseTokenWithCompletion:(void (^)(GACAppCheckToken *,
+                                                   NSError * _Nullable))handler {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    handler(self.token, self.error);
+  });
+}
+
+@end
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 75 - 0
GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h

@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023 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 <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol GACAppCheckProvider;
+@class GACAppCheckToken;
+
+extern NSString *const kGIDAppCheckPreparedKey;
+
+NS_CLASS_AVAILABLE_IOS(14)
+@interface GIDAppCheck : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/// Creates the instance of this App Check wrapper class using `GACAppCheckDebugProvider`.
+///
+/// @param APIKey The API Key to use when creating the debug App Check provider.
+///
+/// The instance is created using `+[NSUserDefaults standardUserDefaults]`.
++ (instancetype)appCheckUsingDebugProviderWithAPIKey:(NSString *)APIKey;
+
+/// Creates the instance of this App Check wrapper class using `GACAppAttestProvider`.
+///
+/// The instance is created using `+[NSUserDefaults standardUserDefaults]`.
++ (instancetype)appCheckUsingAppAttestProvider;
+
+/// Creates the instance of this App Check wrapper class.
+///
+/// @param appCheckProvider The instance performing the Firebase App Check token requests. If `nil`,
+///     then a default implementation will be used.
+/// @param userDefaults The instance of `NSUserDefaults` that `GIDAppCheck` will use to store its
+///     preparation status. If nil, `GIDAppCheck` will use `-[NSUserDefaults standardUserDefaults]`.
+- (instancetype)initWithAppCheckProvider:(id<GACAppCheckProvider>)appCheckProvider
+                            userDefaults:(NSUserDefaults *)userDefaults NS_DESIGNATED_INITIALIZER;
+
+/// Prewarms the library for App Check by asking Firebase App Check to generate the App Attest key
+/// id and perform the initial attestation process (if needed).
+///
+/// @param completion A `nullable` callback with a `nullable` `NSError` if preparation fails.
+- (void)prepareForAppCheckWithCompletion:(nullable void (^)(NSError * _Nullable error))completion;
+
+/// Fetches the limited use Firebase token.
+///
+/// @param completion A `nullable` callback with the `FIRAppCheckToken`, or an `NSError` otherwise.
+- (void)getLimitedUseTokenWithCompletion:
+    (nullable void (^)(GACAppCheckToken *token, NSError * _Nullable error))completion;
+
+/// Whether or not the App Attest key ID created and the attestation object has been fetched.
+- (BOOL)isPrepared;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 180 - 0
GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.m

@@ -0,0 +1,180 @@
+/*
+ * Copyright 2023 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 "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h"
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import <AppCheckCore/GACAppCheck.h>
+#import <AppCheckCore/GACAppCheckSettings.h>
+#import <AppCheckCore/GACAppCheckTokenResult.h>
+#import <AppCheckCore/GACAppAttestProvider.h>
+#import <AppCheckCore/GACAppCheckDebugProvider.h>
+
+#import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAppCheckError.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+
+NSErrorDomain const kGIDAppCheckErrorDomain = @"com.google.GIDAppCheck";
+NSString *const kGIDAppCheckPreparedKey = @"com.google.GIDAppCheckPreparedKey";
+static NSString *const kGIDConfigClientIDKey = @"GIDClientID";
+static NSString *const kGIDAppAttestServiceName = @"GoogleSignIn-iOS";
+static NSString *const kGIDAppAttestResourceNameFormat = @"oauthClients/%@";
+static NSString *const kGIDAppAttestBaseURL = @"https://firebaseappcheck.googleapis.com/v1";
+
+typedef void (^GIDAppCheckPrepareCompletion)(NSError * _Nullable);
+typedef void (^GIDAppCheckTokenCompletion)(GACAppCheckToken *,NSError * _Nullable);
+
+@interface GIDAppCheck ()
+
+@property(nonatomic, strong) GACAppCheck *appCheck;
+@property(nonatomic, strong) dispatch_queue_t workerQueue;
+@property(nonatomic, strong) NSUserDefaults *userDefaults;
+@property(atomic, strong) NSMutableArray<GIDAppCheckPrepareCompletion> *prepareCompletions;
+@property(atomic) BOOL preparing;
+
+@end
+
+@implementation GIDAppCheck
+
++ (instancetype)appCheckUsingDebugProviderWithAPIKey:(NSString *)APIKey {
+  return [[self alloc] initWithAppCheckProvider:[GIDAppCheck debugAppCheckProviderWithAPIKey:APIKey]
+                                   userDefaults:[NSUserDefaults standardUserDefaults]];
+}
+
++ (instancetype)appCheckUsingAppAttestProvider {
+  return [[self alloc] initWithAppCheckProvider:[GIDAppCheck appAttestProvider]
+                                   userDefaults:[NSUserDefaults standardUserDefaults]];
+}
+
+- (instancetype)initWithAppCheckProvider:(id<GACAppCheckProvider>)appCheckProvider
+                            userDefaults:(NSUserDefaults *)userDefaults {
+  if (self = [super init]) {
+    _appCheck = [[GACAppCheck alloc] initWithServiceName:kGIDConfigClientIDKey
+                                            resourceName:[GIDAppCheck appAttestResourceName]
+                                        appCheckProvider:appCheckProvider
+                                                settings:[[GACAppCheckSettings alloc] init]
+                                           tokenDelegate:nil
+                                     keychainAccessGroup:nil];
+
+    _userDefaults = userDefaults;
+    _workerQueue = dispatch_queue_create("com.google.googlesignin.GIDAppCheckWorkerQueue", nil);
+    _prepareCompletions = [NSMutableArray array];
+    _preparing = NO;
+  }
+  return self;
+}
+
+- (BOOL)isPrepared {
+  return [self.userDefaults boolForKey:kGIDAppCheckPreparedKey];
+}
+
+- (void)prepareForAppCheckWithCompletion:(nullable GIDAppCheckPrepareCompletion)completion {
+  if (completion) {
+    @synchronized (self) {
+      [self.prepareCompletions addObject:completion];
+    }
+  }
+
+  @synchronized (self) {
+    if (self.preparing) {
+      return;
+    }
+
+    self.preparing = YES;
+  }
+
+  dispatch_async(self.workerQueue, ^{
+    NSArray * __block callbacks;
+
+    if ([self isPrepared]) {
+      NSArray *callbacks;
+      @synchronized (self) {
+        callbacks = [self.prepareCompletions copy];
+        [self.prepareCompletions removeAllObjects];
+        self.preparing = NO;
+      }
+
+      for (GIDAppCheckPrepareCompletion savedCompletion in callbacks) {
+        savedCompletion(nil);
+      }
+      return;
+    }
+
+    [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) {
+          [self.userDefaults setBool:YES forKey:kGIDAppCheckPreparedKey];
+        }
+
+        callbacks = [self.prepareCompletions copy];
+        [self.prepareCompletions removeAllObjects];
+        self.preparing = NO;
+      }
+
+
+      for (GIDAppCheckPrepareCompletion savedCompletion in callbacks) {
+        savedCompletion(maybeError);
+      }
+    }];
+  });
+}
+
+- (void)getLimitedUseTokenWithCompletion:(nullable GIDAppCheckTokenCompletion)completion {
+  dispatch_async(self.workerQueue, ^{
+    [self.appCheck limitedUseTokenWithCompletion:^(GACAppCheckTokenResult * _Nonnull result) {
+      if (result.token) {
+        [self.userDefaults setBool:YES forKey:kGIDAppCheckPreparedKey];
+      }
+      if (completion) {
+        completion(result.token, result.error);
+      }
+    }];
+  });
+}
+
++ (NSString *)appAttestResourceName {
+  NSString *clientID = [NSBundle.mainBundle objectForInfoDictionaryKey:kGIDConfigClientIDKey];
+  return [NSString stringWithFormat:kGIDAppAttestResourceNameFormat, clientID];
+}
+
++ (id<GACAppCheckProvider>)appAttestProvider {
+  return [[GACAppAttestProvider alloc] initWithServiceName:kGIDAppAttestServiceName
+                                              resourceName:[GIDAppCheck appAttestResourceName]
+                                                   baseURL:kGIDAppAttestBaseURL
+                                                    APIKey:nil
+                                       keychainAccessGroup:nil
+                                              requestHooks:nil];
+}
+
++ (id<GACAppCheckProvider>)debugAppCheckProviderWithAPIKey:(NSString *)APIKey {
+  return [[GACAppCheckDebugProvider alloc] initWithServiceName:kGIDAppAttestServiceName
+                                                  resourceName:[GIDAppCheck appAttestResourceName]
+                                                       baseURL:kGIDAppAttestBaseURL
+                                                        APIKey:APIKey
+                                                  requestHooks:nil];
+}
+
+@end
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 36 - 0
GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h

@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 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 <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// A `UIViewController` presented onscreen to indicate to the user that GSI is performing blocking
+/// work.
+@interface GIDActivityIndicatorViewController : UIViewController
+
+/// The indicator view spinning on screen.
+@property(nonatomic, strong, readonly) UIActivityIndicatorView *activityIndicator;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 51 - 0
GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.m

@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 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 "GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h"
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import <TargetConditionals.h>
+
+@implementation GIDActivityIndicatorViewController
+
+- (void)viewDidLoad {
+  [super viewDidLoad];
+  // Medium gray with transparency
+  self.view.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:0.25];
+
+  UIActivityIndicatorViewStyle style;
+  if (@available(iOS 13.0, *)) {
+    style = UIActivityIndicatorViewStyleLarge;
+  } else {
+    style = UIActivityIndicatorViewStyleGray;
+  }
+  _activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:style];
+  _activityIndicator.color = UIColor.whiteColor;
+  self.activityIndicator.translatesAutoresizingMaskIntoConstraints = NO;
+  [self.activityIndicator startAnimating];
+  [self.view addSubview:self.activityIndicator];
+
+  NSLayoutConstraint *centerX =
+      [self.activityIndicator.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor];
+  NSLayoutConstraint *centerY =
+      [self.activityIndicator.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor];
+  [NSLayoutConstraint activateConstraints:@[centerX, centerY]];
+}
+
+@end
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 177 - 37
GoogleSignIn/Sources/GIDSignIn.m

@@ -28,8 +28,12 @@
 #import "GoogleSignIn/Sources/GIDScopes.h"
 #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+#import <AppCheckCore/GACAppCheckToken.h>
+#import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h"
+#import "GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h"
 #import "GoogleSignIn/Sources/GIDAuthStateMigration.h"
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+#import "GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.h"
 #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
 #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
@@ -132,6 +136,12 @@ static NSString *const kIncludeGrantedScopesParameter = @"include_granted_scopes
 static NSString *const kLoginHintParameter = @"login_hint";
 static NSString *const kHostedDomainParameter = @"hd";
 
+// Parameters for auth and token exchange endpoints using App Attest.
+static NSString *const kClientAssertionParameter = @"client_assertion";
+static NSString *const kClientAssertionTypeParameter = @"client_assertion_type";
+static NSString *const kClientAssertionTypeParameterValue =
+    @"urn:ietf:params:oauth:client-assertion-type:appcheck";
+
 // Minimum time to expiration for a restored access token.
 static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
 
@@ -159,6 +169,9 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   // set when a sign-in flow is begun via |signInWithOptions:| when the options passed don't
   // represent a sign in continuation.
   GIDSignInInternalOptions *_currentOptions;
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  GIDAppCheck *_appCheck API_AVAILABLE(ios(14));
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
   // AppAuth configuration object.
   OIDServiceConfiguration *_appAuthConfiguration;
   // AppAuth external user-agent session state.
@@ -167,6 +180,12 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   BOOL _restarting;
   // Keychain manager for GTMAppAuth
   GTMKeychainStore *_keychainStore;
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  // The class used to manage presenting the loading screen for fetching app check tokens.
+  GIDTimedLoader *_timedLoader;
+  // Flag indicating developer's intent to use App Check.
+  BOOL _configureAppCheckCalled;
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 }
 
 #pragma mark - Public methods
@@ -443,11 +462,49 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   static dispatch_once_t once;
   static GIDSignIn *sharedInstance;
   dispatch_once(&once, ^{
-    sharedInstance = [[self alloc] initPrivate];
+    GTMKeychainStore *keychainStore =
+        [[GTMKeychainStore alloc] initWithItemName:kGTMAppAuthKeychainName];
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+    if (@available(iOS 14.0, *)) {
+      GIDAppCheck *appCheck = [GIDAppCheck appCheckUsingAppAttestProvider];
+      sharedInstance = [[self alloc] initWithKeychainStore:keychainStore
+                                                  appCheck:appCheck];
+    }
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+    if (!sharedInstance) {
+      sharedInstance = [[self alloc] initWithKeychainStore:keychainStore];
+    }
   });
   return sharedInstance;
 }
 
+#pragma mark - Configuring and pre-warming
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+- (void)configureWithCompletion:(nullable void (^)(NSError * _Nullable))completion {
+  @synchronized(self) {
+    _configureAppCheckCalled = YES;
+    [_appCheck prepareForAppCheckWithCompletion:^(NSError * _Nullable error) {
+      if (completion) {
+        completion(error);
+      }
+    }];
+  }
+}
+
+- (void)configureDebugProviderWithAPIKey:(NSString *)APIKey
+                              completion:(nullable void (^)(NSError * _Nullable))completion {
+  @synchronized(self) {
+    _appCheck = [GIDAppCheck appCheckUsingDebugProviderWithAPIKey:APIKey];
+    [_appCheck prepareForAppCheckWithCompletion:^(NSError * _Nullable error) {
+      if (completion) {
+        completion(error);
+      }
+    }];
+  }
+}
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
 #pragma mark - Private methods
 
 - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore {
@@ -470,14 +527,13 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
     }
 
     NSString *authorizationEnpointURL = [NSString stringWithFormat:kAuthorizationURLTemplate,
-        [GIDSignInPreferences googleAuthorizationServer]];
+                                         [GIDSignInPreferences googleAuthorizationServer]];
     NSString *tokenEndpointURL = [NSString stringWithFormat:kTokenURLTemplate,
-        [GIDSignInPreferences googleTokenServer]];
+                                  [GIDSignInPreferences googleTokenServer]];
     _appAuthConfiguration = [[OIDServiceConfiguration alloc]
-        initWithAuthorizationEndpoint:[NSURL URLWithString:authorizationEnpointURL]
-                        tokenEndpoint:[NSURL URLWithString:tokenEndpointURL]];
+                             initWithAuthorizationEndpoint:[NSURL URLWithString:authorizationEnpointURL]
+                             tokenEndpoint:[NSURL URLWithString:tokenEndpointURL]];
     _keychainStore = keychainStore;
-
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
     // Perform migration of auth state from old (before 5.0) versions of the SDK if needed.
     GIDAuthStateMigration *migration =
@@ -491,11 +547,17 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   return self;
 }
 
-- (instancetype)initPrivate {
-  GTMKeychainStore *keychainStore =
-      [[GTMKeychainStore alloc] initWithItemName:kGTMAppAuthKeychainName];
-  return [self initWithKeychainStore:keychainStore];
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore
+                             appCheck:(GIDAppCheck *)appCheck {
+  self = [self initWithKeychainStore:keychainStore];
+  if (self) {
+    _appCheck = appCheck;
+    _configureAppCheckCalled = NO;
+  }
+  return self;
 }
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
 // Does sanity check for parameters and then authenticates if necessary.
 - (void)signInWithOptions:(GIDSignInInternalOptions *)options {
@@ -557,11 +619,6 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 #pragma mark - Authentication flow
 
 - (void)authenticateInteractivelyWithOptions:(GIDSignInInternalOptions *)options {
-  GIDSignInCallbackSchemes *schemes =
-      [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID];
-  NSURL *redirectURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@",
-                                             [schemes clientIdentifierScheme],
-                                             kBrowserCallbackPath]];
   NSString *emmSupport;
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
   emmSupport = [[self class] isOperatingSystemAtLeast9] ? kEMMVersion : nil;
@@ -569,7 +626,102 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   emmSupport = nil;
 #endif // TARGET_OS_MACCATALYST || TARGET_OS_OSX
 
-  NSMutableDictionary<NSString *, NSString *> *additionalParameters = [@{} mutableCopy];
+  [self authorizationRequestWithOptions:options
+                             completion:^(OIDAuthorizationRequest * _Nullable request,
+                                          NSError * _Nullable error) {
+    self->_currentAuthorizationFlow =
+        [OIDAuthorizationService presentAuthorizationRequest:request
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+                                    presentingViewController:options.presentingViewController
+#elif TARGET_OS_OSX
+                                       presentingWindow:options.presentingWindow
+#endif // TARGET_OS_OSX
+                                                    callback:
+                                                      ^(OIDAuthorizationResponse *_Nullable authorizationResponse,
+                                                        NSError *_Nullable error) {
+      [self processAuthorizationResponse:authorizationResponse
+                                   error:error
+                              emmSupport:emmSupport];
+    }];
+  }];
+}
+
+- (void)authorizationRequestWithOptions:(GIDSignInInternalOptions *)options completion:
+    (void (^)(OIDAuthorizationRequest *_Nullable request, NSError *_Nullable error))completion {
+  BOOL shouldCreateAuthRequest = YES;
+  NSMutableDictionary<NSString *, NSString *> *additionalParameters =
+      [self additionalParametersFromOptions:options];
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  if (@available(iOS 14.0, *)) {
+    // Only use `_appCheck` (created via singleton `+[GIDSignIn sharedInstance]` call) if
+    // `GIDAppCheck` has been successfully prepared OR if the developer has attempted to configure.
+    // If former is false and the latter true, then preparation step failed for some reason; we
+    // still want to try to pass along the app check token (it just may take longer since the
+    // pre-warm step failed).
+    if ([_appCheck isPrepared] || _configureAppCheckCalled) {
+      shouldCreateAuthRequest = NO;
+      UIViewController *presentingVC = options.presentingViewController;
+      if (!_timedLoader) {
+        _timedLoader = [[GIDTimedLoader alloc] initWithPresentingViewController:presentingVC];
+      }
+      [_timedLoader startTiming];
+      [self->_appCheck getLimitedUseTokenWithCompletion:^(GACAppCheckToken * _Nullable token,
+                                                          NSError * _Nullable error) {
+        OIDAuthorizationRequest *request = nil;
+        if (token) {
+          additionalParameters[kClientAssertionTypeParameter] = kClientAssertionTypeParameterValue;
+          additionalParameters[kClientAssertionParameter] = token.token;
+        }
+        #if DEBUG
+        if (error) {
+          NSLog(@"[Google Sign-In iOS]: Error retrieving App Check limited use token: %@", error);
+        }
+        #endif
+        request = [self authorizationRequestWithOptions:options
+                                   additionalParameters:additionalParameters];
+        if (self->_timedLoader.animationStatus == GIDTimedLoaderAnimationStatusAnimating) {
+          [self->_timedLoader stopTimingWithCompletion:^{
+            completion(request, error);
+          }];
+        } else {
+          completion(request, error);
+        }
+      }];
+    }
+  }
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  if (shouldCreateAuthRequest) {
+    OIDAuthorizationRequest *request = [self authorizationRequestWithOptions:options
+                                                        additionalParameters:additionalParameters];
+    completion(request, nil);
+  }
+}
+
+
+- (OIDAuthorizationRequest *)
+    authorizationRequestWithOptions:(GIDSignInInternalOptions *)options
+               additionalParameters:(NSDictionary<NSString *, NSString *> *)additionalParameters {
+  OIDAuthorizationRequest *request =
+      [[OIDAuthorizationRequest alloc] initWithConfiguration:_appAuthConfiguration
+                                                    clientId:options.configuration.clientID
+                                                      scopes:options.scopes
+                                                 redirectURL:[self redirectURLWithOptions:options]
+                                                responseType:OIDResponseTypeCode
+                                        additionalParameters:additionalParameters];
+  return request;
+}
+
+- (NSMutableDictionary<NSString *, NSString *> *)
+    additionalParametersFromOptions:(GIDSignInInternalOptions *)options {
+  NSString *emmSupport;
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  emmSupport = [[self class] isOperatingSystemAtLeast9] ? kEMMVersion : nil;
+#elif TARGET_OS_MACCATALYST || TARGET_OS_OSX
+  emmSupport = nil;
+#endif // TARGET_OS_MACCATALYST || TARGET_OS_OSX
+
+  NSMutableDictionary<NSString *, NSString *> *additionalParameters =
+      [[NSMutableDictionary alloc] init];
   additionalParameters[kIncludeGrantedScopesParameter] = @"true";
   if (options.configuration.serverClientID) {
     additionalParameters[kAudienceParameter] = options.configuration.serverClientID;
@@ -592,27 +744,16 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   additionalParameters[kSDKVersionLoggingParameter] = GIDVersion();
   additionalParameters[kEnvironmentLoggingParameter] = GIDEnvironment();
 
-  OIDAuthorizationRequest *request =
-      [[OIDAuthorizationRequest alloc] initWithConfiguration:_appAuthConfiguration
-                                                    clientId:options.configuration.clientID
-                                                      scopes:options.scopes
-                                                 redirectURL:redirectURL
-                                                responseType:OIDResponseTypeCode
-                                        additionalParameters:additionalParameters];
+  return additionalParameters;
+}
 
-  _currentAuthorizationFlow = [OIDAuthorizationService
-      presentAuthorizationRequest:request
-#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
-         presentingViewController:options.presentingViewController
-#elif TARGET_OS_OSX
-                 presentingWindow:options.presentingWindow
-#endif // TARGET_OS_OSX
-                        callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse,
-                                   NSError *_Nullable error) {
-    [self processAuthorizationResponse:authorizationResponse
-                                 error:error
-                            emmSupport:emmSupport];
-  }];
+- (NSURL *)redirectURLWithOptions:(GIDSignInInternalOptions *)options {
+  GIDSignInCallbackSchemes *schemes =
+      [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:options.configuration.clientID];
+  NSURL *redirectURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@",
+                                             [schemes clientIdentifierScheme],
+                                             kBrowserCallbackPath]];
+  return redirectURL;
 }
 
 - (void)processAuthorizationResponse:(OIDAuthorizationResponse *)authorizationResponse
@@ -677,7 +818,6 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 
 // Perform authentication with the provided options.
 - (void)authenticateWithOptions:(GIDSignInInternalOptions *)options {
-
   // If this is an interactive flow, we're not going to try to restore any saved auth state.
   if (options.interactive) {
     [self authenticateInteractivelyWithOptions:options];

+ 9 - 4
GoogleSignIn/Sources/GIDSignIn_Private.h

@@ -29,6 +29,7 @@ NS_ASSUME_NONNULL_BEGIN
 @class GIDGoogleUser;
 @class GIDSignInInternalOptions;
 @class GTMKeychainStore;
+@class GIDAppCheck;
 
 /// Represents a completion block that takes a `GIDSignInResult` on success or an error if the
 /// operation was unsuccessful.
@@ -44,12 +45,16 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error);
 /// Redeclare |currentUser| as readwrite for internal use.
 @property(nonatomic, readwrite, nullable) GIDGoogleUser *currentUser;
 
-/// Private initializer for |GIDSignIn|.
-- (instancetype)initPrivate;
-
-/// Private initializer taking a `GTMKeychainStore` to use during tests.
+/// Private initializer taking a `GTMKeychainStore`.
 - (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore;
 
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+/// Private initializer taking a `GTMKeychainStore` and `GIDAppCheckProvider`.
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore
+                             appCheck:(GIDAppCheck *)appCheck
+API_AVAILABLE(ios(14));
+#endif // TARGET_OS_IOS || !TARGET_OS_MACCATALYST
+
 /// Authenticates with extra options.
 - (void)signInWithOptions:(GIDSignInInternalOptions *)options;
 

+ 70 - 0
GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.h

@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 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/Foundation.h>
+#import <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+/// An enumeration detailing the states of the timed loader.
+typedef NS_ENUM(NSUInteger, GIDTimedLoaderAnimationStatus) {
+  /// The timed loader has not started.
+  GIDTimedLoaderAnimationStatusNotStarted,
+  /// The timed loader's activity indicator is animating.
+  GIDTimedLoaderAnimationStatusAnimating,
+  /// The timed loader's activity indicator has stopped animating.
+  GIDTimedLoaderAnimationStatusStopped,
+};
+
+/// The minimum animation duration time for the timed loader's activity indicator.
+extern CFTimeInterval const kGIDTimedLoaderMinAnimationDuration;
+/// The maximum delay to wait before the time loader will display the loading activity indicator.
+extern CFTimeInterval const kGIDTimedLoaderMaxDelayBeforeAnimating;
+
+@class UIViewController;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// A type used to manage the presentation of a load screen for at least
+/// `kGIDTimedLoaderMinAnimationDuration` to prevent flashing.
+///
+/// `GIDTimedLoader` will also only show its loading screen until
+/// `kGIDTimedLoaderMaxDelayBeforeAnimating` has expired.
+@interface GIDTimedLoader : NSObject
+
+/// Created this timed loading controller with the provided presenting view controller, which will
+/// be used for presenting hte loading view controller with the activity indicator.
+- (instancetype)initWithPresentingViewController:(UIViewController *)presentingViewController;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/// Tells the controller to start keeping track of loading time.
+- (void)startTiming;
+
+/// Tells the controller to stop keeping track of loading time.
+///
+/// @param completion The block to invoke upon successfully stopping.
+/// @note Use the completion parameter to, for example, present the UI that should be shown after
+///     the work has completed.
+- (void)stopTimingWithCompletion:(void (^)(void))completion;
+
+@property(nonatomic) GIDTimedLoaderAnimationStatus animationStatus;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 119 - 0
GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.m

@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023 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 "GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.h"
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+@import UIKit;
+@import CoreMedia;
+#import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h"
+#import "GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h"
+
+CFTimeInterval const kGIDTimedLoaderMinAnimationDuration = 1.0;
+CFTimeInterval const kGIDTimedLoaderMaxDelayBeforeAnimating = 0.8;
+
+@interface GIDTimedLoader ()
+
+@property(nonatomic, strong) UIViewController *presentingViewController;
+@property(nonatomic, strong) GIDActivityIndicatorViewController *loadingViewController;
+@property(nonatomic, strong, nullable) NSTimer *loadingTimer;
+/// Timestamp representing when the loading view controller was presented and started animating
+@property(nonatomic) CFTimeInterval loadingTimeStamp;
+
+@end
+
+@implementation GIDTimedLoader
+
+- (instancetype)initWithPresentingViewController:(UIViewController *)presentingViewController {
+  if (self = [super init]) {
+    _presentingViewController = presentingViewController;
+    _loadingViewController = [[GIDActivityIndicatorViewController alloc] init];
+    _animationStatus = GIDTimedLoaderAnimationStatusNotStarted;
+  }
+  return self;
+}
+
+- (void)startTiming {
+  if (self.animationStatus == GIDTimedLoaderAnimationStatusAnimating) {
+    return;
+  }
+
+  self.animationStatus = GIDTimedLoaderAnimationStatusAnimating;
+  self.loadingTimer = [NSTimer scheduledTimerWithTimeInterval:kGIDTimedLoaderMaxDelayBeforeAnimating
+                                                       target:self
+                                                     selector:@selector(presentLoadingViewController)
+                                                     userInfo:nil
+                                                      repeats:NO];
+}
+
+- (void)presentLoadingViewController {
+  if (self.animationStatus == GIDTimedLoaderAnimationStatusStopped) {
+    return;
+  }
+  self.animationStatus = GIDTimedLoaderAnimationStatusAnimating;
+  self.loadingTimeStamp = CACurrentMediaTime();
+  dispatch_async(dispatch_get_main_queue(), ^{
+    // Since this loading VC may be reused, the activity indicator may have been stopped; restart it
+    self.loadingViewController.modalPresentationStyle = UIModalPresentationOverCurrentContext;
+    self.loadingViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
+    self.presentingViewController.definesPresentationContext = YES;
+    [self.loadingViewController.activityIndicator startAnimating];
+    [self.presentingViewController presentViewController:self.loadingViewController
+                                                animated:YES
+                                              completion:nil];
+  });
+}
+
+- (void)stopTimingWithCompletion:(void (^)(void))completion {
+  if (self.animationStatus != GIDTimedLoaderAnimationStatusAnimating) {
+    return;
+  }
+
+  [self.loadingTimer invalidate];
+  self.loadingTimer = nil;
+
+  dispatch_time_t deadline = [self remainingDurationToAnimate];
+  dispatch_after(deadline, dispatch_get_main_queue(), ^{
+    self.animationStatus = GIDTimedLoaderAnimationStatusStopped;
+    [self.loadingViewController.activityIndicator stopAnimating];
+    [self.loadingViewController dismissViewControllerAnimated:YES completion:nil];
+    completion();
+  });
+}
+
+- (dispatch_time_t)remainingDurationToAnimate {
+  // If we are not animating, then no need to wait
+  if (self.animationStatus != GIDTimedLoaderAnimationStatusAnimating) {
+    return 0;
+  }
+
+  CFTimeInterval now = CACurrentMediaTime();
+  CFTimeInterval durationWaited = now - self.loadingTimeStamp;
+  // If we have already waited for the minimum animation duration, then no need to wait
+  if (durationWaited >= kGIDTimedLoaderMinAnimationDuration) {
+    return 0;
+  }
+
+  CFTimeInterval diff = kGIDTimedLoaderMinAnimationDuration - durationWaited;
+  int64_t diffNanos = diff * NSEC_PER_SEC;
+  dispatch_time_t timeToWait = dispatch_time(DISPATCH_TIME_NOW, diffNanos);
+  return timeToWait;
+}
+
+@end
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 35 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDAppCheckError.h

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 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 <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// The error domain for `NSError`s returned by the Google Sign-In SDK related to App Check.
+extern NSErrorDomain const kGIDAppCheckErrorDomain;
+
+/// A list of potential error codes returned from the Google Sign-In SDK during App Check.
+typedef NS_ERROR_ENUM(kGIDAppCheckErrorDomain, GIDAppCheckErrorCode) {
+  /// An unexpected error was encountered.
+  kGIDAppCheckUnexpectedError = 1,
+};
+NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 35 - 6
GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h

@@ -66,6 +66,35 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) {
 /// The active configuration for this instance of `GIDSignIn`.
 @property(nonatomic, nullable) GIDConfiguration *configuration;
 
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+/// Configures `GIDSignIn` for use.
+///
+/// @param completion A nullable callback block passing back any error arising from the
+///     configuration process if any exists.
+///
+/// Call this method on `GIDSignIn` prior to use and as early as possible. This method generates App
+/// Attest key IDs and the attestation object eagerly to minimize latency later on during the sign
+/// in or add scopes flows.
+- (void)configureWithCompletion:(nullable void (^)(NSError * _Nullable error))completion
+NS_SWIFT_NAME(configure(completion:));
+
+/// Configures `GIDSignIn` for use in debug or test environments.
+///
+/// @param APIKey The API Key to use during configuration of the App Check debug provider.
+/// @param completion A nullable callback block passing back any error arising from the
+///     configuration process if any exists.
+///
+/// Call this method on `GIDSignIn` prior to use and as early as possible. This method generates App
+/// Attest key IDs and the attestation object eagerly to minimize latency later on during the sign
+/// in or add scopes flows.
+- (void)configureDebugProviderWithAPIKey:(NSString *)APIKey
+                              completion:(nullable void (^)(NSError * _Nullable error))completion
+API_AVAILABLE(ios(14))
+NS_SWIFT_NAME(configureDebugProvider(withAPIKey:completion:));
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
 /// Unavailable. Use the `sharedInstance` property to instantiate `GIDSignIn`.
 /// :nodoc:
 + (instancetype)new NS_UNAVAILABLE;
@@ -141,9 +170,9 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) {
 - (void)signInWithPresentingViewController:(UIViewController *)presentingViewController
                                       hint:(nullable NSString *)hint
                                 completion:
-    (nullable void (^)(GIDSignInResult *_Nullable signInResult,
-                       NSError *_Nullable error))completion
-    NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");
+(nullable void (^)(GIDSignInResult *_Nullable signInResult,
+                   NSError *_Nullable error))completion
+NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");
 
 /// Starts an interactive sign-in flow on iOS using the provided hint and additional scopes.
 ///
@@ -163,9 +192,9 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) {
                                       hint:(nullable NSString *)hint
                           additionalScopes:(nullable NSArray<NSString *> *)additionalScopes
                                 completion:
-    (nullable void (^)(GIDSignInResult *_Nullable signInResult,
-                       NSError *_Nullable error))completion
-    NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");
+(nullable void (^)(GIDSignInResult *_Nullable signInResult,
+                   NSError *_Nullable error))completion
+NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");
 
 #elif TARGET_OS_OSX
 

+ 3 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h

@@ -15,6 +15,9 @@
  */
 #import <TargetConditionals.h>
 
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+#import "GIDAppCheckError.h"
+#endif
 #import "GIDConfiguration.h"
 #import "GIDGoogleUser.h"
 #import "GIDProfileData.h"

+ 189 - 0
GoogleSignIn/Tests/Unit/GIDAppCheckTest.m

@@ -0,0 +1,189 @@
+// Copyright 2023 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 <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import <XCTest/XCTest.h>
+#import <AppCheckCore/GACAppCheckToken.h>
+#import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h"
+#import "GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAppCheckError.h"
+
+static NSUInteger const timeout = 1;
+static NSString *const kUserDefaultsTestSuiteName = @"GIDAppCheckTestKeySuiteName";
+
+NS_CLASS_AVAILABLE_IOS(14)
+@interface GIDAppCheckTest : XCTestCase
+
+@property(nonatomic, strong) NSUserDefaults *userDefaults;
+
+@end
+
+@implementation GIDAppCheckTest
+
+- (void)setUp {
+  [super setUp];
+  _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:kUserDefaultsTestSuiteName];
+}
+
+- (void)tearDown {
+  [super tearDown];
+  [self.userDefaults removeObjectForKey:kGIDAppCheckPreparedKey];
+  [self.userDefaults removeSuiteNamed:kUserDefaultsTestSuiteName];
+}
+
+- (void)testGetLimitedUseTokenFailureReturnsPlaceholder {
+  XCTestExpectation *tokenFailExpectation =
+      [self expectationWithDescription:@"App check token 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 getLimitedUseTokenWithCompletion:^(GACAppCheckToken *token,
+                                               NSError * _Nullable error) {
+    XCTAssertEqualObjects(expectedError, error);
+    XCTAssertNotNil(token); // If there is an error, we expect a placeholder token
+    [tokenFailExpectation fulfill];
+  }];
+
+  [self waitForExpectations:@[tokenFailExpectation] timeout:timeout];
+}
+
+- (void)testIsPreparedError {
+  XCTestExpectation *notAlreadyPreparedExpectation =
+      [self expectationWithDescription:@"App check not already prepared error"];
+
+  GACAppCheckToken *expectedToken = [[GACAppCheckToken alloc] initWithToken:@"foo"
+                                                             expirationDate:[NSDate distantFuture]];
+  // It doesn't matter what we pass for the error since we will check `isPrepared` and make one
+  GIDAppCheckProviderFake *fakeProvider =
+      [[GIDAppCheckProviderFake alloc] initWithAppCheckToken:expectedToken error:nil];
+
+  GIDAppCheck *appCheck = [[GIDAppCheck alloc] initWithAppCheckProvider:fakeProvider
+                                                           userDefaults:self.userDefaults];
+
+  [appCheck prepareForAppCheckWithCompletion:^(NSError * _Nullable error) {
+    XCTAssertNil(error);
+    [notAlreadyPreparedExpectation fulfill];
+  }];
+
+  [self waitForExpectations:@[notAlreadyPreparedExpectation] timeout:timeout];
+
+  XCTestExpectation *alreadyPreparedExpectation =
+      [self expectationWithDescription:@"App check already prepared error"];
+
+  // Should be no error since multiple calls to prepare should be fine.
+  [appCheck prepareForAppCheckWithCompletion:^(NSError * _Nullable error) {
+    XCTAssertNil(error);
+    [alreadyPreparedExpectation fulfill];
+  }];
+
+  [self waitForExpectations:@[alreadyPreparedExpectation] timeout:timeout];
+
+  XCTAssertTrue([appCheck isPrepared]);
+}
+
+- (void)testGetLimitedUseTokenSucceeds {
+  XCTestExpectation *prepareExpectation =
+      [self expectationWithDescription:@"Prepare for App Check expectation"];
+
+  GACAppCheckToken *expectedToken = [[GACAppCheckToken alloc] initWithToken:@"foo"
+                                                             expirationDate:[NSDate distantFuture]];
+
+  GIDAppCheckProviderFake *fakeProvider =
+      [[GIDAppCheckProviderFake alloc] initWithAppCheckToken:expectedToken error:nil];
+  GIDAppCheck *appCheck = [[GIDAppCheck alloc] initWithAppCheckProvider:fakeProvider
+                                                           userDefaults:self.userDefaults];
+
+  [appCheck prepareForAppCheckWithCompletion:^(NSError * _Nullable error) {
+    XCTAssertNil(error);
+    [prepareExpectation fulfill];
+  }];
+
+  // Wait for preparation to complete to test `isPrepared`
+  [self waitForExpectations:@[prepareExpectation] timeout:timeout];
+
+  XCTAssertTrue(appCheck.isPrepared);
+
+  XCTestExpectation *getLimitedUseTokenSucceedsExpectation =
+      [self expectationWithDescription:@"getLimitedUseToken should succeed"];
+
+  [appCheck getLimitedUseTokenWithCompletion:^(GACAppCheckToken *token,
+                                               NSError * _Nullable error) {
+    XCTAssertNil(error);
+    XCTAssertNotNil(token);
+    XCTAssertEqualObjects(token, expectedToken);
+    [getLimitedUseTokenSucceedsExpectation fulfill];
+  }];
+
+  [self waitForExpectations:@[getLimitedUseTokenSucceedsExpectation] timeout:timeout];
+
+  XCTAssertTrue([appCheck isPrepared]);
+}
+
+- (void)testAsyncCompletions {
+  XCTestExpectation *firstPrepareExpectation =
+      [self expectationWithDescription:@"First async prepare for App Check expectation"];
+
+  XCTestExpectation *secondPrepareExpectation =
+      [self expectationWithDescription:@"Second async prepare for App Check expectation"];
+
+  GACAppCheckToken *expectedToken = [[GACAppCheckToken alloc] initWithToken:@"foo"
+                                                             expirationDate:[NSDate distantFuture]];
+
+  GIDAppCheckProviderFake *fakeProvider =
+      [[GIDAppCheckProviderFake alloc] initWithAppCheckToken:expectedToken error:nil];
+  GIDAppCheck *appCheck = [[GIDAppCheck alloc] initWithAppCheckProvider:fakeProvider
+                                                           userDefaults:self.userDefaults];
+
+  dispatch_async(dispatch_get_main_queue(), ^{
+    [appCheck prepareForAppCheckWithCompletion:^(NSError * _Nullable error) {
+      XCTAssertNil(error);
+      [firstPrepareExpectation fulfill];
+    }];
+  });
+
+  dispatch_async(dispatch_get_main_queue(), ^{
+    [appCheck prepareForAppCheckWithCompletion:^(NSError * _Nullable error) {
+      XCTAssertNil(error);
+      [secondPrepareExpectation fulfill];
+    }];
+  });
+
+  [self waitForExpectations:@[firstPrepareExpectation, secondPrepareExpectation] timeout:timeout];
+
+  XCTAssertTrue([appCheck isPrepared]);
+
+  // Simulate requesting later on after `appCheck` is prepared
+  XCTestExpectation *preparedExpectation =
+      [self expectationWithDescription:@"Prepared expectation"];
+
+  [appCheck prepareForAppCheckWithCompletion:^(NSError * _Nullable error) {
+    XCTAssertNil(error);
+    [preparedExpectation fulfill];
+  }];
+
+  [self waitForExpectations:@[preparedExpectation] timeout:timeout];
+}
+
+@end
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 88 - 14
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -34,6 +34,9 @@
 #import "GoogleSignIn/Sources/GIDSignInPreferences.h"
 
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+#import <AppCheckCore/GACAppCheckToken.h>
+#import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h"
+#import "GoogleSignIn/Sources/GIDAppCheck/Implementations/Fake/GIDAppCheckProviderFake.h"
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
 #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
@@ -79,6 +82,9 @@
     return YES;\
 }]
 
+/// `NSUserDefaults` suite name for testing with `GIDAppCheck`.
+static NSString *const kUserDefaultsSuiteName = @"GIDAppCheckKeySuiteName";
+
 static NSString * const kFakeGaiaID = @"123456789";
 static NSString * const kFakeIDToken = @"FakeIDToken";
 static NSString * const kClientId = @"FakeClientID";
@@ -261,6 +267,11 @@ static NSString *const kNewScope = @"newScope";
 
   // Status returned by saveAuthorization:toKeychainForName:
   BOOL _saveAuthorizationReturnValue;
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  // Test userDefaults for use with `GIDAppCheck`
+  NSUserDefaults *_testUserDefaults;
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 }
 @end
 
@@ -325,12 +336,15 @@ static NSString *const kNewScope = @"newScope";
   [_fakeMainBundle fakeAllSchemesSupported];
 
   // Object under test
-  [[NSUserDefaults standardUserDefaults] setBool:YES
-                                          forKey:kAppHasRunBeforeKey];
+  [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kAppHasRunBeforeKey];
 
   _signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore];
   _hint = nil;
 
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  _testUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:kUserDefaultsSuiteName];
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
   __weak GIDSignInTest *weakSelf = self;
   _completion = ^(GIDSignInResult *_Nullable signInResult, NSError * _Nullable error) {
     GIDSignInTest *strongSelf = weakSelf;
@@ -357,6 +371,11 @@ static NSString *const kNewScope = @"newScope";
   OCMVerifyAll(_presentingWindow);
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
 
+  [[NSUserDefaults standardUserDefaults] removeObjectForKey:kAppHasRunBeforeKey];
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  [_testUserDefaults removeObjectForKey:kGIDAppCheckPreparedKey];
+  [_testUserDefaults removeSuiteNamed:kUserDefaultsSuiteName];
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
   [_fakeMainBundle stopFaking];
   [super tearDown];
@@ -364,14 +383,62 @@ static NSString *const kNewScope = @"newScope";
 
 #pragma mark - Tests
 
-- (void)testShareInstance {
-  GIDSignIn *signIn1 = GIDSignIn.sharedInstance;
-  GIDSignIn *signIn2 = GIDSignIn.sharedInstance;
-  XCTAssertTrue(signIn1 == signIn2, @"shared instance must be singleton");
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+- (void)testConfigureSucceeds {
+  if (@available(iOS 14, *)) {
+    XCTestExpectation *configureSucceedsExpecation =
+    [self expectationWithDescription:@"Configure succeeds expectation"];
+
+    GACAppCheckToken *token = [[GACAppCheckToken alloc] initWithToken:@"foo"
+                                                       expirationDate:[NSDate distantFuture]];
+    GIDAppCheckProviderFake *fakeProvider =
+        [[GIDAppCheckProviderFake alloc] initWithAppCheckToken:token error:nil];
+    GIDAppCheck *appCheck = [[GIDAppCheck alloc] initWithAppCheckProvider:fakeProvider
+                                                             userDefaults:_testUserDefaults];
+
+    GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore
+                                                        appCheck:appCheck];
+    [signIn configureWithCompletion:^(NSError * _Nullable error) {
+      XCTAssertNil(error);
+      [configureSucceedsExpecation fulfill];
+    }];
+
+    [self waitForExpectations:@[configureSucceedsExpecation] timeout:1];
+    XCTAssertTrue(appCheck.isPrepared);
+  }
+}
+
+- (void)testConfigureFailsNoTokenOrError {
+  if (@available(iOS 14, *)) {
+    XCTestExpectation *configureFailsExpecation =
+    [self expectationWithDescription:@"Configure fails expectation"];
+
+    GIDAppCheckProviderFake *fakeProvider =
+        [[GIDAppCheckProviderFake alloc] initWithAppCheckToken:nil error:nil];
+    GIDAppCheck *appCheck =
+        [[GIDAppCheck alloc] initWithAppCheckProvider:fakeProvider
+                                         userDefaults:_testUserDefaults];
+
+    GIDSignIn *signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore
+                                                        appCheck:appCheck];
+
+    // Should fail if missing both token and error
+    [signIn configureWithCompletion:^(NSError * _Nullable error) {
+      XCTAssertNotNil(error);
+      XCTAssertEqual(error.code, kGIDAppCheckUnexpectedError);
+      [configureFailsExpecation fulfill];
+    }];
+
+    [self waitForExpectations:@[configureFailsExpecation] timeout:1];
+    XCTAssertFalse(appCheck.isPrepared);
+  }
 }
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
-- (void)testInitPrivate {
-  GIDSignIn *signIn = [[GIDSignIn alloc] initPrivate];
+- (void)testInitWithKeychainStore {
+  GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"];
+  GIDSignIn *signIn;
+  signIn = [[GIDSignIn alloc] initWithKeychainStore:store];
   XCTAssertNotNil(signIn.configuration);
   XCTAssertEqual(signIn.configuration.clientID, kClientId);
   XCTAssertNil(signIn.configuration.serverClientID);
@@ -379,21 +446,26 @@ static NSString *const kNewScope = @"newScope";
   XCTAssertNil(signIn.configuration.openIDRealm);
 }
 
-- (void)testInitPrivate_noConfig {
+- (void)testInitWithKeychainStore_noConfig {
   [_fakeMainBundle fakeWithClientID:nil
                      serverClientID:nil
                        hostedDomain:nil
                         openIDRealm:nil];
-  GIDSignIn *signIn = [[GIDSignIn alloc] initPrivate];
+  GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"];
+  GIDSignIn *signIn;
+  signIn = [[GIDSignIn alloc] initWithKeychainStore:store];
   XCTAssertNil(signIn.configuration);
 }
 
-- (void)testInitPrivate_fullConfig {
+- (void)testInitWithKeychainStore_fullConfig {
   [_fakeMainBundle fakeWithClientID:kClientId
                      serverClientID:kServerClientId
                        hostedDomain:kFakeHostedDomain
                         openIDRealm:kOpenIDRealm];
-  GIDSignIn *signIn = [[GIDSignIn alloc] initPrivate];
+
+  GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"];
+  GIDSignIn *signIn;
+  signIn = [[GIDSignIn alloc] initWithKeychainStore:store];
   XCTAssertNotNil(signIn.configuration);
   XCTAssertEqual(signIn.configuration.clientID, kClientId);
   XCTAssertEqual(signIn.configuration.serverClientID, kServerClientId);
@@ -401,12 +473,14 @@ static NSString *const kNewScope = @"newScope";
   XCTAssertEqual(signIn.configuration.openIDRealm, kOpenIDRealm);
 }
 
-- (void)testInitPrivate_invalidConfig {
+- (void)testInitWithKeychainStore_invalidConfig {
   [_fakeMainBundle fakeWithClientID:@[ @"bad", @"config", @"values" ]
                      serverClientID:nil
                        hostedDomain:nil
                         openIDRealm:nil];
-  GIDSignIn *signIn = [[GIDSignIn alloc] initPrivate];
+  GTMKeychainStore *store = [[GTMKeychainStore alloc] initWithItemName:@"foo"];
+  GIDSignIn *signIn;
+  signIn = [[GIDSignIn alloc] initWithKeychainStore:store];
   XCTAssertNil(signIn.configuration);
 }
 

+ 7 - 1
Package.swift

@@ -24,7 +24,7 @@ let package = Package(
   defaultLocalization: "en",
   platforms: [
     .macOS(.v10_15),
-    .iOS(.v10)
+    .iOS(.v11)
   ],
   products: [
     .library(
@@ -45,6 +45,10 @@ let package = Package(
       name: "AppAuth",
       url: "https://github.com/openid/AppAuth-iOS.git",
       from: "1.7.3"),
+    .package(
+      name: "AppCheck",
+      url: "https://github.com/google/app-check.git",
+      "10.19.1" ..< "11.0.0"),
     .package(
       name: "GTMAppAuth",
       url: "https://github.com/google/GTMAppAuth.git",
@@ -67,6 +71,7 @@ let package = Package(
       name: "GoogleSignIn",
       dependencies: [
         .product(name: "AppAuth", package: "AppAuth"),
+        .product(name: "AppCheckCore", package: "AppCheck"),
         .product(name: "GTMAppAuth", package: "GTMAppAuth"),
         .product(name: "GTMSessionFetcherCore", package: "GTMSessionFetcher"),
       ],
@@ -106,6 +111,7 @@ let package = Package(
         "GoogleSignIn",
         "OCMock",
         .product(name: "AppAuth", package: "AppAuth"),
+        .product(name: "AppCheckCore", package: "AppCheck"),
         .product(name: "GTMAppAuth", package: "GTMAppAuth"),
         .product(name: "GTMSessionFetcherCore", package: "GTMSessionFetcher"),
         .product(name: "GULMethodSwizzler", package: "GoogleUtilities"),

+ 1 - 1
Samples/ObjC/SignInSample/Podfile

@@ -1,4 +1,4 @@
-platform :ios, '10.0'
+platform :ios, '11.0'
 use_frameworks!
 
 target 'SampleForPod' do

+ 3 - 1
Samples/ObjC/SignInSample/SignInSample.xcodeproj/project.pbxproj

@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 52;
+	objectVersion = 54;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -459,6 +459,7 @@
 					..,
 				);
 				INFOPLIST_FILE = "$(SRCROOT)/SignInSample-Info.plist";
+				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -477,6 +478,7 @@
 					..,
 				);
 				INFOPLIST_FILE = "$(SRCROOT)/SignInSample-Info.plist";
+				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",

+ 8 - 2
Samples/ObjC/SignInSample/SignInSampleForPod.xcodeproj/project.pbxproj

@@ -308,16 +308,22 @@
 			inputPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-SampleForPod/Pods-SampleForPod-frameworks.sh",
 				"${BUILT_PRODUCTS_DIR}/AppAuth/AppAuth.framework",
+				"${BUILT_PRODUCTS_DIR}/AppCheckCore/AppCheckCore.framework",
 				"${BUILT_PRODUCTS_DIR}/GTMAppAuth/GTMAppAuth.framework",
 				"${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework",
 				"${BUILT_PRODUCTS_DIR}/GoogleSignIn/GoogleSignIn.framework",
+				"${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework",
+				"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
 			);
 			name = "[CP] Embed Pods Frameworks";
 			outputPaths = (
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppAuth.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppCheckCore.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMAppAuth.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleSignIn.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
@@ -504,7 +510,7 @@
 				CLANG_ENABLE_MODULES = YES;
 				DEVELOPMENT_TEAM = "";
 				INFOPLIST_FILE = "$(SRCROOT)/SignInSample-Info.plist";
-				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.google.SignInSample;
 				PRODUCT_NAME = SignInSample;
@@ -521,7 +527,7 @@
 				CLANG_ENABLE_MODULES = YES;
 				DEVELOPMENT_TEAM = "";
 				INFOPLIST_FILE = "$(SRCROOT)/SignInSample-Info.plist";
-				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.google.SignInSample;
 				PRODUCT_NAME = SignInSample;

+ 572 - 0
Samples/Swift/AppAttestExample/AppAttestExample.xcodeproj/project.pbxproj

@@ -0,0 +1,572 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 60;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		736434232B2A914B00DA67DA /* AppAttestExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 736434222B2A914B00DA67DA /* AppAttestExampleTests.swift */; };
+		738B4A322AA8FE800056885D /* AppCheckSecretReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B4A312AA8FE800056885D /* AppCheckSecretReader.swift */; };
+		738D5F732A26BC3B00A7F11B /* BirthdayLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738D5F722A26BC3B00A7F11B /* BirthdayLoader.swift */; };
+		73A464042A1C3B3400BA8528 /* AppAttestExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73A464032A1C3B3400BA8528 /* AppAttestExampleApp.swift */; };
+		73A464062A1C3B3400BA8528 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73A464052A1C3B3400BA8528 /* ContentView.swift */; };
+		73A4640B2A1C3B3500BA8528 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 73A4640A2A1C3B3500BA8528 /* Preview Assets.xcassets */; };
+		73CD4AB22AAF915900642462 /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = 73CD4AB12AAF915900642462 /* GoogleSignIn */; };
+		73CD4AB42AAF915F00642462 /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 73CD4AB32AAF915F00642462 /* GoogleSignInSwift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		736434242B2A914B00DA67DA /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 73A463F82A1C3B3400BA8528 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 73A463FF2A1C3B3400BA8528;
+			remoteInfo = AppAttestExample;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+		73443A232A55F56900A4932E /* AppAttestExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AppAttestExample.entitlements; sourceTree = "<group>"; };
+		734555752AB167B80068F2B0 /* AppCheckDefaultSecrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppCheckDefaultSecrets.xcconfig; sourceTree = "<group>"; };
+		736434202B2A914A00DA67DA /* AppAttestExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppAttestExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		736434222B2A914B00DA67DA /* AppAttestExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAttestExampleTests.swift; sourceTree = "<group>"; };
+		736434292B2A923C00DA67DA /* AppAttestExample.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AppAttestExample.xctestplan; sourceTree = SOURCE_ROOT; };
+		738B4A312AA8FE800056885D /* AppCheckSecretReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCheckSecretReader.swift; sourceTree = "<group>"; };
+		738D5F722A26BC3B00A7F11B /* BirthdayLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BirthdayLoader.swift; sourceTree = "<group>"; };
+		73A065612A786D10007BC7FC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		73A464002A1C3B3400BA8528 /* AppAttestExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppAttestExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		73A464032A1C3B3400BA8528 /* AppAttestExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAttestExampleApp.swift; sourceTree = "<group>"; };
+		73A464052A1C3B3400BA8528 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
+		73A4640A2A1C3B3500BA8528 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
+		73DB85BB2B23DAEC00D051BA /* CoreAudioTypes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudioTypes.framework; path = System/Library/Frameworks/CoreAudioTypes.framework; sourceTree = SDKROOT; };
+		91F3A930BB86D9E0648046BC /* Pods_AppAttestExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AppAttestExample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		7364341D2B2A914A00DA67DA /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		73A463FD2A1C3B3400BA8528 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				73CD4AB22AAF915900642462 /* GoogleSignIn in Frameworks */,
+				73CD4AB42AAF915F00642462 /* GoogleSignInSwift in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		7345556B2AB127B00068F2B0 /* Secrets */ = {
+			isa = PBXGroup;
+			children = (
+				734555752AB167B80068F2B0 /* AppCheckDefaultSecrets.xcconfig */,
+			);
+			path = Secrets;
+			sourceTree = "<group>";
+		};
+		736434212B2A914B00DA67DA /* AppAttestExampleTests */ = {
+			isa = PBXGroup;
+			children = (
+				736434222B2A914B00DA67DA /* AppAttestExampleTests.swift */,
+				736434292B2A923C00DA67DA /* AppAttestExample.xctestplan */,
+			);
+			path = AppAttestExampleTests;
+			sourceTree = "<group>";
+		};
+		73A463F72A1C3B3400BA8528 = {
+			isa = PBXGroup;
+			children = (
+				73A464022A1C3B3400BA8528 /* AppAttestExample */,
+				736434212B2A914B00DA67DA /* AppAttestExampleTests */,
+				73A464012A1C3B3400BA8528 /* Products */,
+				A73FBC2B93918F4B411815A1 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		73A464012A1C3B3400BA8528 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				73A464002A1C3B3400BA8528 /* AppAttestExample.app */,
+				736434202B2A914A00DA67DA /* AppAttestExampleTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		73A464022A1C3B3400BA8528 /* AppAttestExample */ = {
+			isa = PBXGroup;
+			children = (
+				73443A232A55F56900A4932E /* AppAttestExample.entitlements */,
+				73A464032A1C3B3400BA8528 /* AppAttestExampleApp.swift */,
+				73A464052A1C3B3400BA8528 /* ContentView.swift */,
+				738D5F722A26BC3B00A7F11B /* BirthdayLoader.swift */,
+				738B4A312AA8FE800056885D /* AppCheckSecretReader.swift */,
+				7345556B2AB127B00068F2B0 /* Secrets */,
+				73A065612A786D10007BC7FC /* Info.plist */,
+				73A464092A1C3B3500BA8528 /* Preview Content */,
+			);
+			path = AppAttestExample;
+			sourceTree = "<group>";
+		};
+		73A464092A1C3B3500BA8528 /* Preview Content */ = {
+			isa = PBXGroup;
+			children = (
+				73A4640A2A1C3B3500BA8528 /* Preview Assets.xcassets */,
+			);
+			path = "Preview Content";
+			sourceTree = "<group>";
+		};
+		A73FBC2B93918F4B411815A1 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				73DB85BB2B23DAEC00D051BA /* CoreAudioTypes.framework */,
+				91F3A930BB86D9E0648046BC /* Pods_AppAttestExample.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		7364341F2B2A914A00DA67DA /* AppAttestExampleTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 736434262B2A914B00DA67DA /* Build configuration list for PBXNativeTarget "AppAttestExampleTests" */;
+			buildPhases = (
+				7364341C2B2A914A00DA67DA /* Sources */,
+				7364341D2B2A914A00DA67DA /* Frameworks */,
+				7364341E2B2A914A00DA67DA /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				736434252B2A914B00DA67DA /* PBXTargetDependency */,
+			);
+			name = AppAttestExampleTests;
+			packageProductDependencies = (
+			);
+			productName = AppAttestExampleTests;
+			productReference = 736434202B2A914A00DA67DA /* AppAttestExampleTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		73A463FF2A1C3B3400BA8528 /* AppAttestExample */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 73A4640E2A1C3B3500BA8528 /* Build configuration list for PBXNativeTarget "AppAttestExample" */;
+			buildPhases = (
+				73A463FC2A1C3B3400BA8528 /* Sources */,
+				73A463FD2A1C3B3400BA8528 /* Frameworks */,
+				73A463FE2A1C3B3400BA8528 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = AppAttestExample;
+			packageProductDependencies = (
+				73CD4AB12AAF915900642462 /* GoogleSignIn */,
+				73CD4AB32AAF915F00642462 /* GoogleSignInSwift */,
+			);
+			productName = AppAttestExample;
+			productReference = 73A464002A1C3B3400BA8528 /* AppAttestExample.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		73A463F82A1C3B3400BA8528 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = 1;
+				LastSwiftUpdateCheck = 1500;
+				LastUpgradeCheck = 1430;
+				TargetAttributes = {
+					7364341F2B2A914A00DA67DA = {
+						CreatedOnToolsVersion = 15.0.1;
+						TestTargetID = 73A463FF2A1C3B3400BA8528;
+					};
+					73A463FF2A1C3B3400BA8528 = {
+						CreatedOnToolsVersion = 14.3;
+					};
+				};
+			};
+			buildConfigurationList = 73A463FB2A1C3B3400BA8528 /* Build configuration list for PBXProject "AppAttestExample" */;
+			compatibilityVersion = "Xcode 14.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 73A463F72A1C3B3400BA8528;
+			packageReferences = (
+				73A0EE262B2BD381001595C9 /* XCLocalSwiftPackageReference "../../.." */,
+			);
+			productRefGroup = 73A464012A1C3B3400BA8528 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				73A463FF2A1C3B3400BA8528 /* AppAttestExample */,
+				7364341F2B2A914A00DA67DA /* AppAttestExampleTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		7364341E2B2A914A00DA67DA /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		73A463FE2A1C3B3400BA8528 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				73A4640B2A1C3B3500BA8528 /* Preview Assets.xcassets in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		7364341C2B2A914A00DA67DA /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				736434232B2A914B00DA67DA /* AppAttestExampleTests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		73A463FC2A1C3B3400BA8528 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				738D5F732A26BC3B00A7F11B /* BirthdayLoader.swift in Sources */,
+				738B4A322AA8FE800056885D /* AppCheckSecretReader.swift in Sources */,
+				73A464062A1C3B3400BA8528 /* ContentView.swift in Sources */,
+				73A464042A1C3B3400BA8528 /* AppAttestExampleApp.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		736434252B2A914B00DA67DA /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 73A463FF2A1C3B3400BA8528 /* AppAttestExample */;
+			targetProxy = 736434242B2A914B00DA67DA /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+		736434272B2A914B00DA67DA /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GENERATE_INFOPLIST_FILE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MARKETING_VERSION = 1.0;
+				OTHER_LDFLAGS = "";
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.AppAttestExampleTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AppAttestExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AppAttestExample";
+			};
+			name = Debug;
+		};
+		736434282B2A914B00DA67DA /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GENERATE_INFOPLIST_FILE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MARKETING_VERSION = 1.0;
+				OTHER_LDFLAGS = "";
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.AppAttestExampleTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AppAttestExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AppAttestExample";
+			};
+			name = Release;
+		};
+		73A4640C2A1C3B3500BA8528 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				FRAMEWORK_SEARCH_PATHS = "";
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				INFOPLIST_FILE = "";
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				OTHER_LDFLAGS = "";
+				SDKROOT = iphoneos;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		73A4640D2A1C3B3500BA8528 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				FRAMEWORK_SEARCH_PATHS = "";
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				INFOPLIST_FILE = "";
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				OTHER_LDFLAGS = "";
+				SDKROOT = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		73A4640F2A1C3B3500BA8528 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 734555752AB167B80068F2B0 /* AppCheckDefaultSecrets.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = AppAttestExample/AppAttestExample.entitlements;
+				CODE_SIGN_IDENTITY = "Apple Development";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Manual;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_ASSET_PATHS = "";
+				DEVELOPMENT_TEAM = "";
+				"DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV;
+				ENABLE_PREVIEWS = YES;
+				ENABLE_TESTING_SEARCH_PATHS = NO;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = AppAttestExample/Info.plist;
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				OTHER_LDFLAGS = "";
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental0.dev;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Experimental App 0 Dev";
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		73A464102A1C3B3500BA8528 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = AppAttestExample/AppAttestExample.entitlements;
+				CODE_SIGN_IDENTITY = "Apple Development";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Manual;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_ASSET_PATHS = "";
+				DEVELOPMENT_TEAM = "";
+				"DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV;
+				ENABLE_PREVIEWS = YES;
+				ENABLE_TESTING_SEARCH_PATHS = NO;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = AppAttestExample/Info.plist;
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				OTHER_LDFLAGS = "";
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental0.dev;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Experimental App 0 Dev";
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		736434262B2A914B00DA67DA /* Build configuration list for PBXNativeTarget "AppAttestExampleTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				736434272B2A914B00DA67DA /* Debug */,
+				736434282B2A914B00DA67DA /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		73A463FB2A1C3B3400BA8528 /* Build configuration list for PBXProject "AppAttestExample" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				73A4640C2A1C3B3500BA8528 /* Debug */,
+				73A4640D2A1C3B3500BA8528 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		73A4640E2A1C3B3500BA8528 /* Build configuration list for PBXNativeTarget "AppAttestExample" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				73A4640F2A1C3B3500BA8528 /* Debug */,
+				73A464102A1C3B3500BA8528 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+		73A0EE262B2BD381001595C9 /* XCLocalSwiftPackageReference "../../.." */ = {
+			isa = XCLocalSwiftPackageReference;
+			relativePath = ../../..;
+		};
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+		73CD4AB12AAF915900642462 /* GoogleSignIn */ = {
+			isa = XCSwiftPackageProductDependency;
+			productName = GoogleSignIn;
+		};
+		73CD4AB32AAF915F00642462 /* GoogleSignInSwift */ = {
+			isa = XCSwiftPackageProductDependency;
+			productName = GoogleSignInSwift;
+		};
+/* End XCSwiftPackageProductDependency section */
+	};
+	rootObject = 73A463F82A1C3B3400BA8528 /* Project object */;
+}

+ 106 - 0
Samples/Swift/AppAttestExample/AppAttestExample.xcodeproj/xcshareddata/xcschemes/AppAttestExample.xcscheme

@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1430"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "73A463FF2A1C3B3400BA8528"
+               BuildableName = "AppAttestExample.app"
+               BlueprintName = "AppAttestExample"
+               ReferencedContainer = "container:AppAttestExample.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <TestPlans>
+         <TestPlanReference
+            reference = "container:AppAttestExample.xctestplan"
+            default = "YES">
+         </TestPlanReference>
+      </TestPlans>
+      <Testables>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "7364341F2B2A914A00DA67DA"
+               BuildableName = "AppAttestExampleTests.xctest"
+               BlueprintName = "AppAttestExampleTests"
+               ReferencedContainer = "container:AppAttestExample.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "7364341F2B2A914A00DA67DA"
+               BuildableName = "AppAttestExampleTests.xctest"
+               BlueprintName = "AppAttestExampleTests"
+               ReferencedContainer = "container:AppAttestExample.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "73A463FF2A1C3B3400BA8528"
+            BuildableName = "AppAttestExample.app"
+            BlueprintName = "AppAttestExample"
+            ReferencedContainer = "container:AppAttestExample.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "73A463FF2A1C3B3400BA8528"
+            BuildableName = "AppAttestExample.app"
+            BlueprintName = "AppAttestExample"
+            ReferencedContainer = "container:AppAttestExample.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 40 - 0
Samples/Swift/AppAttestExample/AppAttestExample.xctestplan

@@ -0,0 +1,40 @@
+{
+  "configurations" : [
+    {
+      "id" : "CBD3C208-A37F-4B73-9881-D0EBF17004C2",
+      "name" : "Configuration 1",
+      "options" : {
+        "targetForVariableExpansion" : {
+          "containerPath" : "container:AppAttestExample.xcodeproj",
+          "identifier" : "7364341F2B2A914A00DA67DA",
+          "name" : "AppAttestExampleTests"
+        }
+      }
+    }
+  ],
+  "defaultOptions" : {
+    "commandLineArgumentEntries" : [
+
+    ],
+    "environmentVariableEntries" : [
+      {
+        "key" : "AppCheckDebugToken",
+        "value" : "$(AppCheckDebugToken)"
+      },
+      {
+        "key" : "APP_CHECK_WEB_API_KEY",
+        "value" : "$(APP_CHECK_WEB_API_KEY)"
+      }
+    ]
+  },
+  "testTargets" : [
+    {
+      "target" : {
+        "containerPath" : "container:AppAttestExample.xcodeproj",
+        "identifier" : "7364341F2B2A914A00DA67DA",
+        "name" : "AppAttestExampleTests"
+      }
+    }
+  ],
+  "version" : 1
+}

+ 8 - 0
Samples/Swift/AppAttestExample/AppAttestExample/AppAttestExample.entitlements

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.developer.devicecheck.appattest-environment</key>
+	<string>production</string>
+</dict>
+</plist>

+ 57 - 0
Samples/Swift/AppAttestExample/AppAttestExample/AppAttestExampleApp.swift

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023 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
+
+class AppDelegate: NSObject, UIApplicationDelegate {
+  func application(
+    _ application: UIApplication,
+    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
+  ) -> Bool {
+    #if targetEnvironment(simulator)
+    let secretReader = AppCheckSecretReader()
+    guard let APIKey = secretReader.APIKey else {
+      print("Unable to read API key from bundle or environment")
+      return true
+    }
+    GIDSignIn.sharedInstance.configureDebugProvider(withAPIKey: APIKey) { error in
+      if let error {
+        print("Error configuring `GIDSignIn` for Firebase App Check: \(error)")
+      }
+    }
+    #else
+    GIDSignIn.sharedInstance.configure { error in
+      if let error {
+        print("Error configuring `GIDSignIn` for Firebase App Check: \(error)")
+      }
+    }
+    #endif
+
+    return true
+  }
+}
+
+@main
+struct AppAttestExampleApp: App {
+  @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+  
+  var body: some Scene {
+    WindowGroup {
+      ContentView()
+    }
+  }
+}

+ 58 - 0
Samples/Swift/AppAttestExample/AppAttestExample/AppCheckSecretReader.swift

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 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 AppCheckSecretReader {
+  private let APIKeyName = "APP_CHECK_WEB_API_KEY"
+  private let APIKeyResourceName = "AppCheckSecrets"
+  private let APIKeyExtensionName = "json"
+  private let debugTokenName = "AppCheckDebugToken"
+
+  /// Method to read the App Check debug token from the environment
+  var debugToken: String? {
+    guard let debugToken = ProcessInfo.processInfo.environment[debugTokenName],
+          !debugToken.isEmpty else {
+      print("Failed to get \(debugTokenName) from environment.")
+      return nil
+    }
+    return debugToken
+  }
+
+  /// Method to read the App Check API key from either the bundle or the environment
+  var APIKey: String? {
+    return APIKeyFromBundle ?? APIKeyFromEnvironment
+  }
+
+  /// Method for retrieving API key from environment variable used during CI tests
+  private var APIKeyFromEnvironment: String? {
+    guard let APIKey = ProcessInfo.processInfo.environment[APIKeyName], !APIKey.isEmpty else {
+      print("Failed to get \(APIKeyName) from environment.")
+      return nil
+    }
+    return APIKey
+  }
+
+  /// Method for retrieving API key from the bundle during simulator or debug builds
+  private var APIKeyFromBundle: String? {
+    guard let APIKey = Bundle.main.object(forInfoDictionaryKey: APIKeyName) as? String,
+          !APIKey.isEmpty else {
+      print("Failed to get \(APIKeyName) from environment.")
+      return nil
+    }
+    return APIKey
+  }
+}

+ 111 - 0
Samples/Swift/AppAttestExample/AppAttestExample/BirthdayLoader.swift

@@ -0,0 +1,111 @@
+/*
+ * Copyright 2023 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
+
+class BirthdayLoader {
+  /// The scope required to read a user's birthday.
+  static let birthdayReadScope = "https://www.googleapis.com/auth/user.birthday.read"
+  private let baseUrlString = "https://people.googleapis.com/v1/people/me"
+  private let personFieldsQuery = URLQueryItem(name: "personFields", value: "birthdays")
+
+  private lazy var components: URLComponents? = {
+    var comps = URLComponents(string: baseUrlString)
+    comps?.queryItems = [personFieldsQuery]
+    return comps
+  }()
+
+  private lazy var request: URLRequest? = {
+    guard let components = components, let url = components.url else {
+      return nil
+    }
+    return URLRequest(url: url)
+  }()
+
+  private func sessionWithFreshToken(completion: @escaping (Result<URLSession, Error>) -> Void) {
+    GIDSignIn.sharedInstance.currentUser?.refreshTokensIfNeeded { user, error in
+      guard let token = user?.accessToken.tokenString else {
+        completion(.failure(.couldNotRefreshToken))
+        return
+      }
+      let config = URLSessionConfiguration.default
+      config.httpAdditionalHeaders = [
+        "Authorization": "Bearer \(token)"
+      ]
+      let session = URLSession(configuration: config)
+      completion(.success(session))
+    }
+  }
+
+  func requestBirthday(completion: @escaping (Result<Birthday, Error>) -> Void) {
+    guard let req = request else {
+      completion(.failure(Error.noRequest))
+      return
+    }
+    sessionWithFreshToken { sessionResult in
+      switch sessionResult {
+      case .success(let session):
+        let task = session.dataTask(with: req) { data, response, error in
+          guard let data else {
+            completion(.failure(Error.noData))
+            return
+          }
+          do {
+            let jsonData = try JSONSerialization.jsonObject(with: data)
+            guard let json = jsonData as? [String: Any] else {
+              completion(.failure(Error.jsonDataCannotCastToString))
+              return
+            }
+            guard let birthdays = json["birthdays"] as? [[String: Any]],
+                  let firstBday = birthdays.first?["date"] as? [String: Int],
+                  let day = firstBday["day"],
+                  let month = firstBday["month"] else {
+              completion(.failure(Error.noBirthday))
+              return
+            }
+            completion(.success(Birthday(day: day, month: month)))
+          } catch {
+            completion(.failure(Error.noJSON))
+          }
+        }
+        task.resume()
+      case .failure(let error):
+        completion(.failure(error))
+      }
+    }
+  }
+}
+
+extension BirthdayLoader {
+  enum Error: Swift.Error {
+    case noRequest
+    case noData
+    case noJSON
+    case jsonDataCannotCastToString
+    case noBirthday
+    case couldNotRefreshToken
+  }
+}
+
+struct Birthday: CustomStringConvertible {
+  let day: Int
+  let month: Int
+
+  var description: String {
+    return "Day: \(day); month: \(month)"
+  }
+}

+ 218 - 0
Samples/Swift/AppAttestExample/AppAttestExample/ContentView.swift

@@ -0,0 +1,218 @@
+/*
+ * Copyright 2023 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
+import GoogleSignInSwift
+
+struct ContentView: View {
+  @State private var userInfo = ""
+  @State private var shouldPresentAlert = false
+  @State private var shouldPresentBirthdayAlert = false
+  @State private var errorInfo: ErrorInfo?
+  @State private var birthday: Birthday?
+  private let birthdayLoader = BirthdayLoader()
+
+  var body: some View {
+    NavigationStack {
+      VStack {
+        HStack {
+          GoogleSignInButton(style: .wide) {
+            self.userInfo = ""
+            guard let rootViewController = self.rootViewController else {
+              print("No root view controller")
+              return
+            }
+            GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) { result, error in
+              guard let result else {
+                print("Error signing in: \(String(describing: error))")
+                return
+              }
+              print("Successfully signed in user")
+              self.userInfo = result.user.profile?.json ?? ""
+            }
+          }
+          .toolbar {
+            ToolbarItem(placement: .navigationBarTrailing) {
+              Button(NSLocalizedString("Sign out", comment: "sign out")) {
+                GIDSignIn.sharedInstance.signOut()
+                clearState()
+              }
+              .disabled(userInfo.isEmpty)
+            }
+            ToolbarItem(placement: .navigationBarLeading) {
+              Button(NSLocalizedString("Disconnect", comment: "disconnect")) {
+                GIDSignIn.sharedInstance.disconnect { error in
+                  if let error {
+                    print("Disconnection error: \(error)")
+                    return
+                  }
+                  print("Disconnected")
+                  clearState()
+                }
+              }
+              .disabled(shouldDisableDisconnect)
+            }
+            ToolbarItem(placement: .bottomBar) {
+              Button(NSLocalizedString("Add Birthday Scope", comment: "Add Scope Button")) {
+                guard let rvc = rootViewController else {
+                  print("No root view controller to use as presenter")
+                  return
+                }
+                GIDSignIn.sharedInstance.currentUser?.addScopes(
+                  [BirthdayLoader.birthdayReadScope],
+                  presenting: rvc
+                ) { result, error in
+                  shouldPresentAlert = true
+                  if let result {
+                    print("Successfully added scope: \(result)")
+                    errorInfo = ErrorInfo.success
+                    return
+                  }
+                  if let e = error {
+                    print("Failed to add scope: \(e)")
+                    if (e as NSError).code == GIDSignInError.scopesAlreadyGranted.rawValue {
+                      errorInfo = ErrorInfo(
+                        errorDescription: NSLocalizedString(
+                          "Did not add scope",
+                          comment: "no scope"
+                        ),
+                        failureReason: NSLocalizedString(
+                          "User already added scope.",
+                          comment: "already added scope"
+                        )
+                      )
+                    } else {
+                      errorInfo = ErrorInfo.unkownError
+                    }
+                    return
+                  }
+                }
+              }
+              .disabled(userInfo.isEmpty)
+            }
+            ToolbarItem(placement: .bottomBar) {
+              Button(NSLocalizedString("Fetch Birthday", comment: "birthday")) {
+                birthdayLoader.requestBirthday { birthdayResult in
+                  switch birthdayResult {
+                  case .success(let birthday):
+                    print(birthday)
+                    self.birthday = birthday
+                    errorInfo = ErrorInfo.birthday(with: birthday)
+                  case .failure(let error):
+                    print("Error fetching birthday: \(error)")
+                    errorInfo = ErrorInfo(
+                      errorDescription: (error as NSError).domain,
+                      failureReason: String((error as NSError).code)
+                    )
+                  }
+                  shouldPresentBirthdayAlert = true
+                }
+              }
+              .disabled(
+                !(GIDSignIn
+                  .sharedInstance
+                  .currentUser?
+                  .grantedScopes?
+                  .contains(BirthdayLoader.birthdayReadScope) ?? false)
+              )
+              .alert(
+                isPresented: $shouldPresentBirthdayAlert,
+                error: errorInfo) { info in
+                  Text(info.errorDescription ?? "Unknown birthday fetch error")
+                } message: { info in
+                  Text(info.birthday?.description ?? "No birthday")
+                }
+            }
+          }
+          .alert(
+            isPresented: $shouldPresentAlert,
+            error: errorInfo) { info in
+              Text(info.errorDescription ?? "Unknown Alert")
+            } message: { info in
+              Text(info.failureReason ?? "NA")
+            }
+        }
+        Text(userInfo)
+      }
+      .padding()
+    }
+  }
+}
+
+private extension ContentView {
+  var rootViewController: UIViewController? {
+    return UIApplication.shared.connectedScenes
+      .filter({ $0.activationState == .foregroundActive })
+      .compactMap { $0 as? UIWindowScene }
+      .compactMap { $0.keyWindow }
+      .first?.rootViewController
+  }
+
+  var shouldDisableDisconnect: Bool {
+    guard !userInfo.isEmpty else {
+      return true
+    }
+    return GIDSignIn.sharedInstance.currentUser?.grantedScopes?.isEmpty ?? false
+  }
+
+  func clearState() {
+    errorInfo = nil
+    shouldPresentAlert = false
+    userInfo = ""
+  }
+}
+
+private extension GIDProfileData {
+  var json: String {
+    """
+    success: {
+      Given Name: \(self.givenName ?? "None")
+      Family Name: \(self.familyName ?? "None")
+      Name: \(self.name)
+      Email: \(self.email)
+      Profile Photo: \(self.imageURL(withDimension: 1)?.absoluteString ?? "None");
+    }
+    """
+  }
+}
+
+fileprivate struct ErrorInfo: LocalizedError {
+  static var unkownError: ErrorInfo {
+    return ErrorInfo(
+      errorDescription: NSLocalizedString("Unknown Error!", comment: "unknown error"),
+      failureReason: NSLocalizedString("NA", comment: "NA error string")
+    )
+  }
+  static var success: ErrorInfo {
+    return ErrorInfo(
+      errorDescription: NSLocalizedString("Success!", comment: "no error"),
+      failureReason: NSLocalizedString("Added birthday read scope.", comment: "scope added")
+    )
+  }
+  static func birthday(with bday: Birthday) -> ErrorInfo {
+    return ErrorInfo(
+      errorDescription: NSLocalizedString("Success!", comment: "no error"),
+      failureReason: NSLocalizedString("Added birthday read scope.", comment: "scope added"),
+      birthday: bday
+    )
+  }
+  var errorDescription: String?
+  var failureReason: String?
+  var recoverySuggestion: String?
+  var helpAnchor: String?
+  var birthday: Birthday?
+}

+ 23 - 0
Samples/Swift/AppAttestExample/AppAttestExample/Info.plist

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>APP_CHECK_WEB_API_KEY</key>
+	<string>$(APP_CHECK_WEB_API_KEY)</string>
+	<key>CFBundleURLTypes</key>
+	<array>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>CFBundleURLName</key>
+			<string>com.googleusercontent.apps.665845761721-a9g0c1k6buv131av6nnmburou5scd63h</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>com.googleusercontent.apps.665845761721-a9g0c1k6buv131av6nnmburou5scd63h</string>
+			</array>
+		</dict>
+	</array>
+	<key>GIDClientID</key>
+	<string>665845761721-a9g0c1k6buv131av6nnmburou5scd63h.apps.googleusercontent.com</string>
+</dict>
+</plist>

+ 11 - 0
Samples/Swift/AppAttestExample/AppAttestExample/Preview Content/Preview Assets.xcassets/Assets/AccentColor.colorset/Contents.json

@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 13 - 0
Samples/Swift/AppAttestExample/AppAttestExample/Preview Content/Preview Assets.xcassets/Assets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,13 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 6 - 0
Samples/Swift/AppAttestExample/AppAttestExample/Preview Content/Preview Assets.xcassets/Assets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 6 - 0
Samples/Swift/AppAttestExample/AppAttestExample/Preview Content/Preview Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 16 - 0
Samples/Swift/AppAttestExample/AppAttestExample/Secrets/AppCheckDefaultSecrets.xcconfig

@@ -0,0 +1,16 @@
+//
+// Copyright 2023 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.
+
+APP_CHECK_WEB_API_KEY=

+ 471 - 0
Samples/Swift/AppAttestExample/AppAttestExampleForPod.xcodeproj/project.pbxproj

@@ -0,0 +1,471 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 56;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		734555A02AB39FBF0068F2B0 /* AppCheckDefaultSecrets.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 7345559F2AB39FBF0068F2B0 /* AppCheckDefaultSecrets.xcconfig */; };
+		738B4A322AA8FE800056885D /* AppCheckSecretReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B4A312AA8FE800056885D /* AppCheckSecretReader.swift */; };
+		738D5F732A26BC3B00A7F11B /* BirthdayLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738D5F722A26BC3B00A7F11B /* BirthdayLoader.swift */; };
+		73A464042A1C3B3400BA8528 /* AppAttestExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73A464032A1C3B3400BA8528 /* AppAttestExampleApp.swift */; };
+		73A464062A1C3B3400BA8528 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73A464052A1C3B3400BA8528 /* ContentView.swift */; };
+		73A4640B2A1C3B3500BA8528 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 73A4640A2A1C3B3500BA8528 /* Preview Assets.xcassets */; };
+		D8DD1A90FCD4C367E280C0F7 /* Pods_AppAttestExampleForPod.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4728D878D216B7D622E237DA /* Pods_AppAttestExampleForPod.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+		1C96B5B2B34E31F1A1CEE95E /* Pods-AppAttestExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppAttestExample.release.xcconfig"; path = "Target Support Files/Pods-AppAttestExample/Pods-AppAttestExample.release.xcconfig"; sourceTree = "<group>"; };
+		4728D878D216B7D622E237DA /* Pods_AppAttestExampleForPod.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AppAttestExampleForPod.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		73080B2A2AAF9BDE00DEF667 /* AppAttestExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAttestExampleTests.swift; sourceTree = "<group>"; };
+		73443A232A55F56900A4932E /* AppAttestExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AppAttestExample.entitlements; sourceTree = "<group>"; };
+		7345559F2AB39FBF0068F2B0 /* AppCheckDefaultSecrets.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = AppCheckDefaultSecrets.xcconfig; sourceTree = "<group>"; };
+		738B4A312AA8FE800056885D /* AppCheckSecretReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCheckSecretReader.swift; sourceTree = "<group>"; };
+		738D5F722A26BC3B00A7F11B /* BirthdayLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BirthdayLoader.swift; sourceTree = "<group>"; };
+		73A065612A786D10007BC7FC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		73A464002A1C3B3400BA8528 /* AppAttestExampleForPod.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppAttestExampleForPod.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		73A464032A1C3B3400BA8528 /* AppAttestExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAttestExampleApp.swift; sourceTree = "<group>"; };
+		73A464052A1C3B3400BA8528 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
+		73A4640A2A1C3B3500BA8528 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
+		73D87D512AAFE0FE002D841C /* AppAttestExample.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AppAttestExample.xctestplan; sourceTree = "<group>"; };
+		7D9832F2FFAF408698660CA8 /* Pods-AppAttestExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppAttestExample.debug.xcconfig"; path = "Target Support Files/Pods-AppAttestExample/Pods-AppAttestExample.debug.xcconfig"; sourceTree = "<group>"; };
+		DB9DDDAE875580597968F796 /* Pods-AppAttestExampleForPod.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppAttestExampleForPod.release.xcconfig"; path = "Target Support Files/Pods-AppAttestExampleForPod/Pods-AppAttestExampleForPod.release.xcconfig"; sourceTree = "<group>"; };
+		E27EB8376FDCD7AB29F8A522 /* Pods-AppAttestExampleForPod.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppAttestExampleForPod.debug.xcconfig"; path = "Target Support Files/Pods-AppAttestExampleForPod/Pods-AppAttestExampleForPod.debug.xcconfig"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		73A463FD2A1C3B3400BA8528 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				D8DD1A90FCD4C367E280C0F7 /* Pods_AppAttestExampleForPod.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		6B1005926777EEB3C903F93A /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				7D9832F2FFAF408698660CA8 /* Pods-AppAttestExample.debug.xcconfig */,
+				1C96B5B2B34E31F1A1CEE95E /* Pods-AppAttestExample.release.xcconfig */,
+				E27EB8376FDCD7AB29F8A522 /* Pods-AppAttestExampleForPod.debug.xcconfig */,
+				DB9DDDAE875580597968F796 /* Pods-AppAttestExampleForPod.release.xcconfig */,
+			);
+			path = Pods;
+			sourceTree = "<group>";
+		};
+		73080B292AAF9BDE00DEF667 /* AppAttestExampleTests */ = {
+			isa = PBXGroup;
+			children = (
+				73080B2A2AAF9BDE00DEF667 /* AppAttestExampleTests.swift */,
+			);
+			path = AppAttestExampleTests;
+			sourceTree = "<group>";
+		};
+		7345556C2AB142B40068F2B0 /* Secrets */ = {
+			isa = PBXGroup;
+			children = (
+				7345559F2AB39FBF0068F2B0 /* AppCheckDefaultSecrets.xcconfig */,
+			);
+			path = Secrets;
+			sourceTree = "<group>";
+		};
+		73A463F72A1C3B3400BA8528 = {
+			isa = PBXGroup;
+			children = (
+				73D87D512AAFE0FE002D841C /* AppAttestExample.xctestplan */,
+				73A464022A1C3B3400BA8528 /* AppAttestExample */,
+				73080B292AAF9BDE00DEF667 /* AppAttestExampleTests */,
+				73A464012A1C3B3400BA8528 /* Products */,
+				6B1005926777EEB3C903F93A /* Pods */,
+				A73FBC2B93918F4B411815A1 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		73A464012A1C3B3400BA8528 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				73A464002A1C3B3400BA8528 /* AppAttestExampleForPod.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		73A464022A1C3B3400BA8528 /* AppAttestExample */ = {
+			isa = PBXGroup;
+			children = (
+				73443A232A55F56900A4932E /* AppAttestExample.entitlements */,
+				73A464032A1C3B3400BA8528 /* AppAttestExampleApp.swift */,
+				73A464052A1C3B3400BA8528 /* ContentView.swift */,
+				738D5F722A26BC3B00A7F11B /* BirthdayLoader.swift */,
+				738B4A312AA8FE800056885D /* AppCheckSecretReader.swift */,
+				7345556C2AB142B40068F2B0 /* Secrets */,
+				73A065612A786D10007BC7FC /* Info.plist */,
+				73A464092A1C3B3500BA8528 /* Preview Content */,
+			);
+			path = AppAttestExample;
+			sourceTree = "<group>";
+		};
+		73A464092A1C3B3500BA8528 /* Preview Content */ = {
+			isa = PBXGroup;
+			children = (
+				73A4640A2A1C3B3500BA8528 /* Preview Assets.xcassets */,
+			);
+			path = "Preview Content";
+			sourceTree = "<group>";
+		};
+		A73FBC2B93918F4B411815A1 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				4728D878D216B7D622E237DA /* Pods_AppAttestExampleForPod.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		73A463FF2A1C3B3400BA8528 /* AppAttestExampleForPod */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 73A4640E2A1C3B3500BA8528 /* Build configuration list for PBXNativeTarget "AppAttestExampleForPod" */;
+			buildPhases = (
+				55BC416D6BAEAFD4409866C8 /* [CP] Check Pods Manifest.lock */,
+				73A463FC2A1C3B3400BA8528 /* Sources */,
+				73A463FD2A1C3B3400BA8528 /* Frameworks */,
+				73A463FE2A1C3B3400BA8528 /* Resources */,
+				D116F83514234BBFA76A4CC4 /* [CP] Copy Pods Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = AppAttestExampleForPod;
+			productName = AppAttestExample;
+			productReference = 73A464002A1C3B3400BA8528 /* AppAttestExampleForPod.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		73A463F82A1C3B3400BA8528 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = 1;
+				LastSwiftUpdateCheck = 1430;
+				LastUpgradeCheck = 1430;
+				TargetAttributes = {
+					73A463FF2A1C3B3400BA8528 = {
+						CreatedOnToolsVersion = 14.3;
+					};
+				};
+			};
+			buildConfigurationList = 73A463FB2A1C3B3400BA8528 /* Build configuration list for PBXProject "AppAttestExampleForPod" */;
+			compatibilityVersion = "Xcode 14.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 73A463F72A1C3B3400BA8528;
+			productRefGroup = 73A464012A1C3B3400BA8528 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				73A463FF2A1C3B3400BA8528 /* AppAttestExampleForPod */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		73A463FE2A1C3B3400BA8528 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				73A4640B2A1C3B3500BA8528 /* Preview Assets.xcassets in Resources */,
+				734555A02AB39FBF0068F2B0 /* AppCheckDefaultSecrets.xcconfig in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		55BC416D6BAEAFD4409866C8 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-AppAttestExampleForPod-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+		D116F83514234BBFA76A4CC4 /* [CP] Copy Pods Resources */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-AppAttestExampleForPod/Pods-AppAttestExampleForPod-resources-${CONFIGURATION}-input-files.xcfilelist",
+			);
+			name = "[CP] Copy Pods Resources";
+			outputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-AppAttestExampleForPod/Pods-AppAttestExampleForPod-resources-${CONFIGURATION}-output-files.xcfilelist",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AppAttestExampleForPod/Pods-AppAttestExampleForPod-resources.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		73A463FC2A1C3B3400BA8528 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				738D5F732A26BC3B00A7F11B /* BirthdayLoader.swift in Sources */,
+				738B4A322AA8FE800056885D /* AppCheckSecretReader.swift in Sources */,
+				73A464062A1C3B3400BA8528 /* ContentView.swift in Sources */,
+				73A464042A1C3B3400BA8528 /* AppAttestExampleApp.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		73A4640C2A1C3B3500BA8528 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7345559F2AB39FBF0068F2B0 /* AppCheckDefaultSecrets.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				INFOPLIST_FILE = "";
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		73A4640D2A1C3B3500BA8528 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				INFOPLIST_FILE = "";
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		73A4640F2A1C3B3500BA8528 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = E27EB8376FDCD7AB29F8A522 /* Pods-AppAttestExampleForPod.debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = AppAttestExample/AppAttestExample.entitlements;
+				CODE_SIGN_IDENTITY = "Apple Development";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Manual;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_ASSET_PATHS = "\"AppAttestExample/Preview Content\"";
+				DEVELOPMENT_TEAM = "";
+				"DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV;
+				ENABLE_PREVIEWS = YES;
+				ENABLE_TESTING_SEARCH_PATHS = NO;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = AppAttestExample/Info.plist;
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental0.dev;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Experimental App 0 Dev";
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		73A464102A1C3B3500BA8528 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = DB9DDDAE875580597968F796 /* Pods-AppAttestExampleForPod.release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = AppAttestExample/AppAttestExample.entitlements;
+				CODE_SIGN_IDENTITY = "Apple Development";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				CODE_SIGN_STYLE = Manual;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_ASSET_PATHS = "\"AppAttestExample/Preview Content\"";
+				DEVELOPMENT_TEAM = "";
+				"DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV;
+				ENABLE_PREVIEWS = YES;
+				ENABLE_TESTING_SEARCH_PATHS = NO;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = AppAttestExample/Info.plist;
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental0.dev;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Experimental App 0 Dev";
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		73A463FB2A1C3B3400BA8528 /* Build configuration list for PBXProject "AppAttestExampleForPod" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				73A4640C2A1C3B3500BA8528 /* Debug */,
+				73A4640D2A1C3B3500BA8528 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		73A4640E2A1C3B3500BA8528 /* Build configuration list for PBXNativeTarget "AppAttestExampleForPod" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				73A4640F2A1C3B3500BA8528 /* Debug */,
+				73A464102A1C3B3500BA8528 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 73A463F82A1C3B3400BA8528 /* Project object */;
+}

+ 113 - 0
Samples/Swift/AppAttestExample/AppAttestExampleForPod.xcodeproj/xcshareddata/xcschemes/AppAttestExample.xcscheme

@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1430"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "73A463FF2A1C3B3400BA8528"
+               BuildableName = "AppAttestExample.app"
+               BlueprintName = "AppAttestExample"
+               ReferencedContainer = "container:AppAttestExample.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <TestPlans>
+         <TestPlanReference
+            reference = "container:AppAttestExample.xctestplan"
+            default = "YES">
+         </TestPlanReference>
+      </TestPlans>
+      <Testables>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "73080B272AAF9BDE00DEF667"
+               BuildableName = "AppAttestExampleTests.xctest"
+               BlueprintName = "AppAttestExampleTests"
+               ReferencedContainer = "container:AppAttestExample.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "73A463FF2A1C3B3400BA8528"
+            BuildableName = "AppAttestExampleForPod.app"
+            BlueprintName = "AppAttestExampleForPod"
+            ReferencedContainer = "container:AppAttestExampleForPod.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "73A463FF2A1C3B3400BA8528"
+            BuildableName = "AppAttestExample.app"
+            BlueprintName = "AppAttestExample"
+            ReferencedContainer = "container:AppAttestExample.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "73A463FF2A1C3B3400BA8528"
+            BuildableName = "AppAttestExample.app"
+            BlueprintName = "AppAttestExample"
+            ReferencedContainer = "container:AppAttestExample.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "73A463FF2A1C3B3400BA8528"
+            BuildableName = "AppAttestExample.app"
+            BlueprintName = "AppAttestExample"
+            ReferencedContainer = "container:AppAttestExample.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 30 - 0
Samples/Swift/AppAttestExample/AppAttestExampleTests/AppAttestExampleTests.swift

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 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 XCTest
+@testable import AppAttestExample
+
+final class AppAttestExampleTests: XCTestCase {
+  func testThatAPIKeyCanBeReadFromBundleOrEnvironment() {
+    let secretsReader = AppCheckSecretReader()
+    XCTAssertNotNil(secretsReader.APIKey)
+  }
+
+  func testThatDebugTokenCanBeReadFromEnvironment() {
+    let secretsReader = AppCheckSecretReader()
+    XCTAssertNotNil(secretsReader.debugToken)
+  }
+}

+ 12 - 0
Samples/Swift/AppAttestExample/Podfile

@@ -0,0 +1,12 @@
+source 'https://github.com/CocoaPods/Specs.git'
+
+pod 'GoogleSignIn', :path => '../../../', :testspecs => ['unit']
+pod 'GoogleSignInSwiftSupport', :path => '../../../', :testspecs => ['unit']
+project 'AppAttestExampleForPod.xcodeproj'
+
+use_frameworks! :linkage => :static
+
+target 'AppAttestExampleForPod' do
+  pod 'AppCheckCore'
+  platform :ios, '14.0'
+end

+ 60 - 0
Samples/Swift/AppAttestExample/README.md

@@ -0,0 +1,60 @@
+# Google Sign-In with Firebase App Check Sample App
+
+## CocoaPods
+
+1. In the `../Samples/Swift/AppAttestExample/` folder, run the following 
+[CocoaPods](https://cocoapods.org) command.
+
+```
+pod install
+```
+
+2. Open the generated workspace:
+
+```
+open AppAttestExample.xcworkspace
+```
+
+3. Run the `AppAttestExampleForPod` target.
+
+## Swift Package Manager
+
+1. In the `../Samples/Swift/AppAttestExample/` folder, open the project:
+
+```
+open AppAttestExample.xcodeproj
+```
+2. Run the `AppAttestExample` target.
+
+## A Note on Provisioning Profiles
+
+You will need a provisioning profile with the App Attest entitlement.
+
+## Hiding Secrets
+
+This example app shows how you might hide your web API key and debug token
+(used during CI; AppCheckCore manages the debug token running locally in the 
+simulator). Both of these are required. Inside the `Secrets/` directory, we
+include a placeholder file entitled `AppCheckDefaultSecrets.xcconfig`. We have
+also set that as a configuration file for the project, which means that it will
+be used to find the web API key during debug builds on the simulator (for 
+example). You can either make a new file to fill in the stubbed data in
+`AppCheckDefaultSecrets.xcconfig` (which will require that you update where the
+projects finds its configurations), or you can add your API key there yourself.
+Do make sure that you do not commit this API key, or you will risk exposing
+this information on your repository.
+
+In builds running under continuous integration, make sure to use environment
+variables and `AppCheckSecretReader.swift` will find your web API key and debug
+token if you provide them to your `xcodebuild` command.
+
+## Integration Tests
+
+We show how you might hide your app's web API key and debug token when
+running locally and in CI environments. See GitHub's
+[secrets](https://docs.github.com/en/actions/learn-github-actions/contexts#secrets-context)
+documentation for how you might set those values in your repo.
+
+Locally, both the web API key and the debug token need to be passed to 
+`xcodebuild` as arguments: 
+`xcodebuild <other args> APP_CHECK_WEB_API_KEY=... AppCheckDebugToken=...`.