Ver Fonte

Initial open source project.

Peter Andrews há 5 anos atrás
commit
f8f05173e5
100 ficheiros alterados com 8159 adições e 0 exclusões
  1. 5 0
      .cocoapods.yml
  2. 6 0
      .gitignore
  3. 137 0
      CHANGELOG.md
  4. 28 0
      CONTRIBUTING.md
  5. 51 0
      GoogleSignIn.podspec
  6. 24 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/Info.plist
  7. BIN
      GoogleSignIn/Resources/GoogleSignIn.bundle/Roboto-Bold.ttf
  8. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/ar.lproj/GoogleSignIn.strings
  9. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/ca.lproj/GoogleSignIn.strings
  10. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/cs.lproj/GoogleSignIn.strings
  11. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/da.lproj/GoogleSignIn.strings
  12. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/de.lproj/GoogleSignIn.strings
  13. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/el.lproj/GoogleSignIn.strings
  14. 32 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/en.lproj/GoogleSignIn.strings
  15. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/en_GB.lproj/GoogleSignIn.strings
  16. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/es.lproj/GoogleSignIn.strings
  17. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/es_MX.lproj/GoogleSignIn.strings
  18. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/fi.lproj/GoogleSignIn.strings
  19. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/fr.lproj/GoogleSignIn.strings
  20. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/fr_CA.lproj/GoogleSignIn.strings
  21. BIN
      GoogleSignIn/Resources/GoogleSignIn.bundle/google.png
  22. BIN
      GoogleSignIn/Resources/GoogleSignIn.bundle/google@2x.png
  23. BIN
      GoogleSignIn/Resources/GoogleSignIn.bundle/google@3x.png
  24. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/he.lproj/GoogleSignIn.strings
  25. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/hi.lproj/GoogleSignIn.strings
  26. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/hr.lproj/GoogleSignIn.strings
  27. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/hu.lproj/GoogleSignIn.strings
  28. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/id.lproj/GoogleSignIn.strings
  29. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/it.lproj/GoogleSignIn.strings
  30. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/ja.lproj/GoogleSignIn.strings
  31. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/ko.lproj/GoogleSignIn.strings
  32. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/ms.lproj/GoogleSignIn.strings
  33. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/nb.lproj/GoogleSignIn.strings
  34. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/nl.lproj/GoogleSignIn.strings
  35. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/pl.lproj/GoogleSignIn.strings
  36. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/pt.lproj/GoogleSignIn.strings
  37. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/pt_BR.lproj/GoogleSignIn.strings
  38. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/pt_PT.lproj/GoogleSignIn.strings
  39. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/ro.lproj/GoogleSignIn.strings
  40. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/ru.lproj/GoogleSignIn.strings
  41. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/sk.lproj/GoogleSignIn.strings
  42. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/sv.lproj/GoogleSignIn.strings
  43. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/th.lproj/GoogleSignIn.strings
  44. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/tr.lproj/GoogleSignIn.strings
  45. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/uk.lproj/GoogleSignIn.strings
  46. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/vi.lproj/GoogleSignIn.strings
  47. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/zh_CN.lproj/GoogleSignIn.strings
  48. 44 0
      GoogleSignIn/Resources/GoogleSignIn.bundle/zh_TW.lproj/GoogleSignIn.strings
  49. 33 0
      GoogleSignIn/Sources/GIDAuthStateMigration.h
  50. 173 0
      GoogleSignIn/Sources/GIDAuthStateMigration.m
  51. 340 0
      GoogleSignIn/Sources/GIDAuthentication.m
  52. 54 0
      GoogleSignIn/Sources/GIDAuthentication_Private.h
  53. 52 0
      GoogleSignIn/Sources/GIDCallbackQueue.h
  54. 99 0
      GoogleSignIn/Sources/GIDCallbackQueue.m
  55. 125 0
      GoogleSignIn/Sources/GIDConfiguration.m
  56. 37 0
      GoogleSignIn/Sources/GIDEMMErrorHandler.h
  57. 285 0
      GoogleSignIn/Sources/GIDEMMErrorHandler.m
  58. 111 0
      GoogleSignIn/Sources/GIDGoogleUser.m
  59. 32 0
      GoogleSignIn/Sources/GIDGoogleUser_Private.h
  60. 41 0
      GoogleSignIn/Sources/GIDMDMPasscodeCache.h
  61. 296 0
      GoogleSignIn/Sources/GIDMDMPasscodeCache.m
  62. 50 0
      GoogleSignIn/Sources/GIDMDMPasscodeState.h
  63. 50 0
      GoogleSignIn/Sources/GIDMDMPasscodeState.m
  64. 35 0
      GoogleSignIn/Sources/GIDMDMPasscodeState_Private.h
  65. 129 0
      GoogleSignIn/Sources/GIDProfileData.m
  66. 33 0
      GoogleSignIn/Sources/GIDProfileData_Private.h
  67. 31 0
      GoogleSignIn/Sources/GIDScopes.h
  68. 65 0
      GoogleSignIn/Sources/GIDScopes.m
  69. 789 0
      GoogleSignIn/Sources/GIDSignIn.m
  70. 660 0
      GoogleSignIn/Sources/GIDSignInButton.m
  71. 28 0
      GoogleSignIn/Sources/GIDSignInButton_Private.h
  72. 44 0
      GoogleSignIn/Sources/GIDSignInCallbackSchemes.h
  73. 88 0
      GoogleSignIn/Sources/GIDSignInCallbackSchemes.m
  74. 49 0
      GoogleSignIn/Sources/GIDSignInInternalOptions.h
  75. 59 0
      GoogleSignIn/Sources/GIDSignInInternalOptions.m
  76. 33 0
      GoogleSignIn/Sources/GIDSignInPreferences.h
  77. 55 0
      GoogleSignIn/Sources/GIDSignInPreferences.m
  78. 38 0
      GoogleSignIn/Sources/GIDSignInStrings.h
  79. 47 0
      GoogleSignIn/Sources/GIDSignInStrings.m
  80. 34 0
      GoogleSignIn/Sources/GIDSignIn_Private.h
  81. 31 0
      GoogleSignIn/Sources/NSBundle+GID3PAdditions.h
  82. 60 0
      GoogleSignIn/Sources/NSBundle+GID3PAdditions.m
  83. 66 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h
  84. 77 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h
  85. 49 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h
  86. 47 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h
  87. 171 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h
  88. 63 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInButton.h
  89. 22 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h
  90. 207 0
      GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m
  91. 25 0
      GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h
  92. 46 0
      GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.m
  93. 636 0
      GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m
  94. 33 0
      GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.h
  95. 60 0
      GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.m
  96. 93 0
      GoogleSignIn/Tests/Unit/GIDConfigurationTest.m
  97. 459 0
      GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m
  98. 29 0
      GoogleSignIn/Tests/Unit/GIDFakeFetcher.h
  99. 54 0
      GoogleSignIn/Tests/Unit/GIDFakeFetcher.m
  100. 25 0
      GoogleSignIn/Tests/Unit/GIDFakeFetcherService.h

+ 5 - 0
.cocoapods.yml

@@ -0,0 +1,5 @@
+try:
+  install:
+    pre:
+      - git clone https://github.com/googlesamples/google-services
+  project: 'google-services/ios/signin/SignInExample.xcodeproj'

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+# MacOS
+.DS_Store
+
+# CocoaPods
+Pods/
+gen/

+ 137 - 0
CHANGELOG.md

@@ -0,0 +1,137 @@
+# 2020-4-7 -- v6.0.0
+- Google Sign-In for iOS is now open source.
+- Swift Package Manager Support.
+- Support for Simulator on M1 Macs.
+
+# 2019-11-7 -- v5.0.2
+- Fixes the wrong error code being sent to `signIn:didSignInForUser:withError:` when the user
+  cancels iOS's consent dialog during the sign-in flow.
+
+# 2019-10-9 -- v5.0.1
+- Fixes an issue that the sign in flow cannot be correctly started on iOS 13.
+- The zip distribution requires Xcode 11 or above.
+
+# 2019-8-14 -- v5.0.0
+- Changes to GIDSignIn
+    - `uiDelegate` has been replaced with `presentingViewController`.
+    - `hasAuthInKeychain` has been replaced with `hasPreviousSignIn`.
+    - `signInSilently` has been replaced with `restorePreviousSignIn`.
+    - Removed deprecated `kGIDSignInErrorCodeNoSignInHandlersInstalled` error code.
+- Changes to GIDAuthentication
+    - Removed deprecated methods `getAccessTokenWithHandler:` and `refreshAccessTokenWithHandler:`.
+- Changes to GIDGoogleUser
+    - Removed deprecated property `accessibleScopes`, use `grantedScopes` instead.
+- Adds dependencies on AppAuth and GTMAppAuth.
+- Removes the dependency on GoogleToolboxForMac.
+- Drops support for iOS 7.
+
+# 2018-11-26 -- v4.4.0
+- Removes the dependency on GTM OAuth 2.
+
+# 2018-10-1 -- v4.3.0
+- Supports Google's Enterprise Mobile Management.
+
+# 2018-8-10 -- v4.2.0
+- Adds `grantedScopes` to `GIDGoogleUser`, allowing confirmation of which scopes
+  have been granted after a successful sign-in.
+- Deprecates `accessibleScopes` in `GIDGoogleUser`, use `grantedScopes` instead.
+- Localizes `GIDSignInButton` for hi (Hindi) and fr-CA (French (Canada)).
+- Adds dependency to the system `LocalAuthentication` framework.
+
+# 2018-1-8 -- v4.1.2
+- Add `pod try` support for the GoogleSignIn CocoaPod.
+
+# 2017-10-17 -- v4.1.1
+- Fixes an issue that `GIDSignInUIDelegate`'s `signInWillDispatch:error:` was
+  not called on iOS 11. Please note that it is intended that neither
+  `signIn:presentViewController:` nor `signIn:dismissViewController:` is called
+  on iOS 11 because SFAuthenticationSession is not presented by the app's view
+  controller.
+
+# 2017-09-13 -- v4.1.0
+- Uses SFAuthenticationSession on iOS 11.
+
+# 2017-02-06 -- v4.0.2
+- No longer depends on GoogleAppUtilities.
+
+# 2016-10-24 -- v4.0.1
+- Switches to open source pod dependencies.
+- Appearance of sign-in button no longer depends on requested scopes.
+
+# 2016-04-21 -- v4.0.0
+- GoogleSignIn pod now takes form of a static framework. Import with
+  `#import <GoogleSignIn/GoogleSignIn.h>` in Objective-C.
+- Adds module support. You can also use `@import GoogleSignIn;` in Objective-C,
+  if module is enabled, and `import GoogleSignIn` in Swift without using a
+  bridge-header.
+- For users of the stand-alone zip distribution, multiple frameworks are now
+  provided and all need to be added to a project. This decomposition allows more
+  flexibility in case of duplicated dependencies.
+- Removes deprecated method `checkGoogleSignInAppInstalled` from `GIDSignIn`.
+- Removes `allowsSignInWithBrowser` and `allowsSignInWithWebView` properties
+  from `GIDSignIn`.
+- No longer requires adding bundle ID as a URL scheme supported by the app.
+
+# 2016-03-04 -- v3.0.0
+- Provides `givenName` and `familyName` properties on `GIDProfileData`.
+- Allows setting the `loginHint` property on `GIDSignIn` to prefill the user's
+  ID or email address in the sign-in flow.
+- Removed the `UIViewController(SignIn)` category as well as the `delegate`
+  property from `GIDSignInButton`.
+- Requires that `uiDelegate` has been set properly on `GIDSignIn` and that
+  SafariServices framework has been linked.
+- Removes the dependency on StoreKit.
+- Provides bitcode support.
+- Requires Xcode 7.0 or above due to bitcode incompatibilities with Xcode 6.
+
+# 2015-10-26 -- v2.4.0
+- Updates sign-in button with the new Google logo.
+- Supports domain restriction for sign-in.
+- Allows refreshing ID tokens.
+
+# 2015-10-09 -- v2.3.2
+- No longer requires Xcode 7.
+
+# 2015-10-01 -- v2.3.1
+- Fixes a crash in `GIDProfileData`'s `imageURLWithDimension:`.
+
+# 2015-09-25 -- v2.3.0
+- Requires Xcode 7.0 or above.
+- Uses SFSafariViewController for signing in on iOS 9.  `uiDelegate` must be
+  set for this to work.
+- Optimizes fetching user profile.
+- Supports GTMFetcherAuthorizationProtocol in GIDAuthentication.
+
+# 2015-07-15 -- v2.2.0
+- Compatible with iOS 9 (beta).  Note that this version of the Sign-In SDK does
+  not include bitcode, so you must set ENABLE_BITCODE to NO in your project if
+  you use Xcode 7.
+- Adds descriptive identifiers for GIDSignInButton's Auto Layout constraints.
+- `signInSilently` no longer requires setting `uiDelegate`.
+
+# 2015-06-17 -- v2.1.0
+- Fixes Auto Layout issues with GIDSignInButton.
+- Adds API to refresh access token in GIDAuthentication.
+- Better exception description for unassigned clientID in GIDSignIn.
+- Other minor bug fixes.
+
+# 2015-05-28 -- v2.0.1
+- Bug fixes
+
+# 2015-05-21 -- v2.0.0
+- Supports sign-in via UIWebView rather than app switching to a browser,
+  configurable with the new `allowsSignInWithWebView` property.
+- Now apps which have disabled the app switch to a browser via the
+  `allowsSignInWithBrowser` and in-app web view via `allowsSignInWithWebView`
+  properties have the option to display a prompt instructing the user to
+  download the Google app from the App Store.
+- Fixes sign-in button sizing issue when auto-layout is enabled
+- `signInSilently` now calls the delegate with error when `hasAuthInKeychain`
+  is `NO` as documented
+- Other minor bug fixes
+
+# 2015-03-12 -- v1.0.0
+- New sign-in focused SDK with refreshed API
+- Dynamically rendered sign-in button with contextual branding
+- Basic profile support
+- Added allowsSignInWithBrowser property

+ 28 - 0
CONTRIBUTING.md

@@ -0,0 +1,28 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution;
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code Reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google/conduct/).

+ 51 - 0
GoogleSignIn.podspec

@@ -0,0 +1,51 @@
+Pod::Spec.new do |s|
+  s.name             = 'GoogleSignIn'
+  s.version          = '6.0.0'
+  s.summary          = 'Enables iOS apps to sign in with Google.'
+  s.description      = <<-DESC
+The Google Sign-In SDK allows users to sign in with their Google account from third-party apps.
+                       DESC
+  s.homepage         = 'https://developers.google.com/identity/sign-in/ios/'
+  s.license          = { :type => 'Apache', :file => 'LICENSE' }
+  s.authors          = 'Google, Inc.'
+  s.source           = {
+    :git => 'https://developers.google.com/identity/sign-in/ios/',
+    :tag => 'CocoaPods-' + s.version.to_s
+  }
+  ios_deployment_target = '9.0'
+  s.ios.deployment_target = ios_deployment_target
+  s.prefix_header_file = false
+  s.source_files = [
+    'GoogleSignIn/Sources/**/*.[mh]',
+  ]
+  s.public_header_files = [
+    'GoogleSignIn/Sources/Public/GoogleSignIn/*.h',
+  ]
+  s.frameworks = [
+    'CoreGraphics',
+    'CoreText',
+    'Foundation',
+    'LocalAuthentication',
+    'Security',
+    'UIKit'
+  ]
+  s.dependency 'AppAuth', '~> 1.4'
+  s.dependency 'GTMAppAuth', '~> 1.0'
+  s.dependency 'GTMSessionFetcher/Core', '~> 1.1'
+  s.resources = 'GoogleSignIn/Resources/GoogleSignIn.bundle'
+  s.pod_target_xcconfig = {
+    'GCC_C_LANGUAGE_STANDARD' => 'c99',
+    'GCC_PREPROCESSOR_DEFINITIONS' => 'GID_SDK_VERSION=' + s.version.to_s,
+    'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"'
+  }
+  s.test_spec 'unit' do |unit_tests|
+    unit_tests.platforms = {:ios => ios_deployment_target}
+    unit_tests.source_files = [
+      'GoogleSignIn/Tests/Unit/**/*.[mh]',
+    ]
+    unit_tests.requires_app_host = true
+    unit_tests.dependency 'OCMock'
+    unit_tests.dependency 'GoogleUtilities/MethodSwizzler', '~> 7.2'
+    unit_tests.dependency 'GoogleUtilities/SwizzlerTestHelpers', '~> 7.2'
+  end
+end

+ 24 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/Info.plist

@@ -0,0 +1,24 @@
+<?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>CFBundleDevelopmentRegion</key>
+	<string>English</string>
+	<key>CFBundleIconFile</key>
+	<string></string>
+	<key>CFBundleIdentifier</key>
+	<string>com.google.${PRODUCT_NAME:rfc1034identifier}</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>${PRODUCT_NAME}</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>NSPrincipalClass</key>
+	<string></string>
+</dict>
+</plist>

BIN
GoogleSignIn/Resources/GoogleSignIn.bundle/Roboto-Bold.ttf


+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/ar.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "تسجيل الدخول";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "تسجيل الدخول باستخدام Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "تسجيل الدخول باستخدام Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "احصل على تطبيق Google المجاني وسجل الدخول إلى التطبيقات من خلال حساب Google. لا توجد حاجة لتذكر كلمات المرور.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "إلغاء";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "جلب";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "موافق";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "إلغاء";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "إعدادات";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "يتعذَّر تسجيل الدخول إلى الحساب";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "يطلب منك المشرف تعيين رمز مرور على هذا الجهاز للدخول إلى هذا الحساب. يُرجى تعيين رمز المرور وإعادة المحاولة.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "لا يتوافق هذا الجهاز مع سياسة الأمان التي أعدها مشرفك";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "هل تريد الربط بتطبيق Device Policy؟";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "يجب الربط مع تطبيق Device Policy قبل تسجيل الدخول لحماية بيانات مؤسستك.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "ربط";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/ca.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Inicia la sessió";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Inicia la sessió amb Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Inicia la sessió amb Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Obteniu l'aplicació Google gratuïta i inicieu la sessió a les aplicacions amb el vostre compte de Google. D'aquesta manera, ja no haureu de recordar cap més contrasenya.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Cancel·la";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Obtén";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "D’acord";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Cancel·la";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Configuració";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "No es pot iniciar la sessió al compte";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "L'administrador requereix que estableixis una contrasenya en aquest dispositiu per accedir al compte. Estableix una contrasenya i torna-ho a provar.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "El dispositiu no compleix la política de seguretat establerta pel teu administrador.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Vols connectar-te amb l'aplicació Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Per protegir les dades de la teva organització, t'has de connectar amb l'aplicació Device Policy abans d'iniciar la sessió.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Vull connectar-me";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/cs.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Přihlásit se";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Přihlásit se účtem Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Přihlašujte se účtem Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Nainstalujte si zdarma aplikaci Google a přihlašujte se do aplikací pomocí účtu Google. Nebudete si už muset pamatovat spoustu hesel.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Zrušit";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Instalovat";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Zrušit";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Nastavení";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Nelze se přihlásit k účtu";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Administrátor vyžaduje, abyste v tomto zařízení nastavili heslo pro přístup k tomuto účtu. Nastavte prosím heslo a zkuste to znovu.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Zařízení nevyhovuje bezpečnostním zásadám nastaveným administrátorem.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Propojit s aplikací Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Aby bylo možné chránit data vaší organizace, před přihlášením je nutné aktivovat propojení s aplikací Device Policy.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Propojit";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/da.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Log ind";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Log ind med Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Log ind med Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Hent den gratis Google-app, og log ind på apps med din Google-konto. Du slipper for at huske på adgangskoder.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Annuller";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Hent";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Annuller";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Indstillinger";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Der kunne ikke logges ind på kontoen";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Din administrator kræver, at du angiver en adgangskode på enheden for at få adgang til kontoen. Angiv en adgangskode, og prøv igen.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Enheden overholder ikke den sikkerhedspolitik, der er angivet af din administrator.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Vil du oprette forbindelse til appen Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Du skal oprette forbindelse til appen Device Policy, inden du logger ind, for at beskytte din organisations data.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Opret forbindelse";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/de.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Anmelden";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Über Google anmelden";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Über Google anmelden";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Installieren Sie die kostenlose Google App und melden Sie sich mit Ihrem Google-Konto in Apps an. So müssen Sie sich keine Passwörter mehr merken.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Abbrechen";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Installieren";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Abbrechen";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Einstellungen";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Anmelden im Konto nicht möglich";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Ihr Administrator hat festgelegt, dass auf diesem Gerät ein Sicherheitscode eingerichtet werden muss, um auf dieses Konto zuzugreifen. Bitte legen Sie einen Sicherheitscode fest und versuchen Sie es noch einmal.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Das Gerät ist nicht mit den von Ihrem Administrator festgelegten Sicherheitsrichtlinien konform.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Mit der Device Policy App verknüpfen?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Zum Schutz der Daten Ihrer Organisation müssen Sie Ihr Gerät zuerst mit der Device Policy App verknüpfen, bevor Sie sich anmelden.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Verknüpfen";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/el.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Σύνδεση";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Συνδεθείτε με το Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Συνδεθείτε με το Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Αποκτήστε τη δωρεάν εφαρμογή Google και συνδεθείτε σε εφαρμογές με το Λογαριασμό σας Google. Δεν χρειάζεται να απομνημονεύετε κωδικούς πρόσβασης.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Ακύρωση";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Λήψη";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "ΟΚ";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Άκυρο";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Ρυθμίσεις";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Δεν είναι δυνατή η σύνδεση στον λογαριασμό";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Ο διαχειριστής σας απαιτεί να ορίσετε έναν κωδικό πρόσβασης στη συσκευή, για να έχετε πρόσβαση σε αυτόν τον λογαριασμό. Ορίστε έναν κωδικό πρόσβασης και δοκιμάστε ξανά.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Η συσκευή δεν συμμορφώνεται με την πολιτική ασφαλείας που έχει ορίσει ο διαχειριστής σας.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Σύνδεση με την εφαρμογή Device Policy;";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Προκειμένου να προστατεύσετε τα δεδομένα του οργανισμού σας, θα πρέπει να συνδεθείτε με την εφαρμογή Device Policy προτού συνδεθείτε στον λογαριασμό σας.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Σύνδεση";

+ 32 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/en.lproj/GoogleSignIn.strings

@@ -0,0 +1,32 @@
+/* Sign-in button text */
+"Sign in" = "Sign in";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Sign in with Google";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Cancel";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Settings";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Unable to sign in to account";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Your administrator requires you to set a passcode on this device to access this account. Please set a passcode and try again.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "The device is not compliant with the security policy set by your administrator.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Connect with Device Policy App?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "In order to protect your organization's data, you must connect with the Device Policy app before logging in.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Connect";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/en_GB.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Sign in";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Sign in with Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Sign in with Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Get the free Google app and sign in to apps with your Google Account. No need to remember passwords.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Cancel";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Get";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Cancel";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Settings";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Unable to sign in to account";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Your administrator requires you to set a passcode on this device to access this account. Please set a passcode and try again.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "The device is not compliant with the security policy set by your administrator.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Connect with Device Policy App?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "In order to protect your organisation's data, you must connect with the Device Policy app before logging in.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Connect";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/es.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Iniciar sesión";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Iniciar sesión con Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Iniciar sesión con Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Obtén la aplicación Google gratuita e inicia sesión en aplicaciones con tu cuenta de Google. No tendrás que recordar las contraseñas.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Cancelar";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Obtener";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "Aceptar";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Cancelar";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Configuración";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "No se ha podido iniciar sesión en la cuenta";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "El administrador requiere que configures una contraseña en este dispositivo para acceder a esta cuenta. Inténtalo de nuevo cuando lo hayas hecho.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "El dispositivo no cumple la política de privacidad que ha definido tu administrador.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "¿Has conectado tu dispositivo con la aplicación Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Para proteger los datos de tu organización, debes conectar tu dispositivo con la aplicación Device Policy antes de iniciar sesión.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Conectar";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/es_MX.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Acceder";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Acceder con Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Acceder con Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Obtén Google app y accede a aplicaciones con tu cuenta de Google. No hace falta recordar contraseñas.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Cancelar";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Obtener";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "Aceptar";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Cancelar";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Configuración";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "No es posible acceder a la cuenta";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Para acceder a esta cuenta, tu administrador requiere que establezcas una contraseña en el dispositivo. Configúrala y vuelve a intentarlo.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "El dispositivo no cumple con la política de seguridad que estableció el administrador.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "¿Deseas conectarte con la app de Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Para proteger los datos de tu organización, debes conectarte con la app de Device Policy antes de acceder.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Conectar";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/fi.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Kirjaudu sisään";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Kirjaudu Google-tilin tunnuksilla";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Kirjaudu Google-tilin tunnuksilla";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Hanki ilmainen Google-sovellus ja kirjaudu sovelluksiin Google-tililläsi. Sinun ei tarvitse muistaa salasanoja.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Peruuta";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Hae";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Peruuta";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Asetukset";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Kirjautuminen tilille ei onnistu";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Järjestelmänvalvoja edellyttää tunnuskoodin määrittämistä, ennen kuin voit käyttää tiliä tällä laitteella. Määritä tunnuskoodi ja yritä uudelleen.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Laite ei noudata järjestelmänvalvojan määrittämää verkkotunnuskäytäntöä.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Muodostetaanko yhteys Device Policy ‑sovellukseen?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Suojaa organisaatiosi dataa muodostamalla yhteys Device Policy ‑sovellukseen ennen kirjautumista.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Muodosta yhteys";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/fr.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Se connecter";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Se connecter avec Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Se connecter avec Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Installez l'appli Google gratuite et connectez-vous à des applications avec votre compte Google. Plus besoin de vous souvenir de vos mots de passe.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Annuler";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Installer";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Annuler";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Paramètres";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Impossible de se connecter au compte";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Votre administrateur exige que vous définissiez un mot de passe sur cet appareil pour accéder à ce compte. Veuillez définir un mot de passe, puis réessayer.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "L'appareil ne respecte pas les règles de sécurité définies par votre administrateur.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Se connecter à l'application Device Policy ?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Afin de protéger les données de votre organisation, vous devez vous connecter à l'application Device Policy avant de vous connecter à votre compte.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Connexion";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/fr_CA.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Se connecter";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Se connecter à Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Connectez-vous à Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Téléchargez gratuitement l'application Google et connectez-vous à des applications avec votre compte Google. Plus besoin de mémoriser vos mots de passe.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Annuler";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Télécharger";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Annuler";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Paramètres";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Impossible de se connecter au compte";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Pour que votre administrateur puisse accéder à ce compte, vous devez définir un mot de passe sur cet appareil. Veuillez définir un mot de passe et réessayer.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "L'appareil n'est pas conforme à la politique de sécurité définie par votre administrateur.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Connexion avec l'application Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Pour protéger les données de votre organisation, vous devez vous connecter à l'application Device Policy avant de vous connecter.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Connexion";

BIN
GoogleSignIn/Resources/GoogleSignIn.bundle/google.png


BIN
GoogleSignIn/Resources/GoogleSignIn.bundle/google@2x.png


BIN
GoogleSignIn/Resources/GoogleSignIn.bundle/google@3x.png


+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/he.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "היכנס";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "היכנס באמצעות Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "כניסה באמצעות Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "התקן את Google app בחינם והיכנס אל אפליקציות באמצעות חשבון Google. לא תצטרך עוד לזכור סיסמאות.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "בטל";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "התקן";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "אישור";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "ביטול";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "הגדרות";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "לא ניתן להיכנס לחשבון";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "מנהל המערכת דורש ממך להגדיר קוד סיסמה במכשיר זה כדי לגשת לחשבון זה. יש להגדיר קוד סיסמה ולנסות שוב.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "המכשיר אינו פועל בהתאם למדיניות האבטחה שנקבעה על-ידי מנהל המערכת.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "האם להתחבר באמצעות האפליקציית Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "על מנת להגן על נתוני הארגון שלך, יש להתחבר באמצעות אפליקציית Device Policy לפני הכניסה לחשבון.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "התחברות";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/hi.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "साइन इन करें";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Google के साथ साइन इन करें";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Google के साथ साइन इन करें";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "मुफ़्त Google ऐप्लिकेशन पाएं और अपने Google खाते से ऐप्लिकेशन में साइन इन करें. पासवर्ड याद रखने की ज़रूरत नहीं.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "अभी नहीं";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "पाएं";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "ठीक";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "अभी नहीं";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "सेटिंग";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "खाते में साइन इन नहीं किया जा सका";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "आपके एडमिन के लिए ज़रूरी है कि आप यह खाता एक्सेस करने के लिए इस डिवाइस पर एक पासकोड सेट करें. कृपया पासकोड सेट करें और दोबारा कोशिश करें.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "डिवाइस आपके एडमिन के ज़रिए सेट की गई सुरक्षा नीति का अनुपालन नहीं करता है.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "क्या Device Policy ऐप्लिकेशन से कनेक्ट करना चाहते हैं?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "अपने संगठन डेटा की सुरक्षा के लिए, आपको लॉग-इन करने से पहले Device Policy ऐप्लिकेशन से कनेक्ट करना होगा.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "कनेक्ट करें";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/hr.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Prijava";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Prijavite se putem Googlea";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Prijavite se putem Googlea";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Preuzmite besplatnu aplikaciju Google i prijavljujte se na aplikacije svojim Google računom. Ne morate pamtiti zaporke.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Odustani";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Nabavi";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "U redu";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Odbaci";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Postavke";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Prijava na račun nije moguća";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Vaš administrator zahtijeva da postavite šifru zaporke na ovom uređaju da biste pristupili računu. Postavite šifru zaporke i pokušajte ponovo.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Uređaj nije usklađen sa sigurnosnim pravilima koja je postavio vaš administrator.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Želite li se povezati s aplikacijom Pravila za uređaje?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Da biste zaštitili podatke svoje organizacije, morate se povezati s aplikacijom Pravila za uređaje prije prijave.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Poveži";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/hu.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Bejelentkezés";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Bejelentkezés Google-fiókkal";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Bejelentkezés Google-fiókkal";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Telepítse az ingyenes Google alkalmazást, és jelentkezzen be az egyes termékekbe Google-fiókjával. Nem kell különböző jelszavakat megjegyeznie.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Mégse";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Telepítés";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Mégse";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Beállítások";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Nem sikerült bejelentkezni a fiókba";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Adminisztrátora biztonsági kód beállítását kéri ezen az eszközön a fiókhoz való hozzáféréshez. Kérjük, állítson be biztonsági kódot, majd próbálja újra.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Az eszköz nem felel meg a rendszergazda által beállított biztonsági házirendnek.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Csatlakozik a Device Policy alkalmazáshoz?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "A szervezet adatainak védelme érdekében a bejelentkezés előtt csatlakoznia kell a Device Policy alkalmazáshoz.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Csatlakozás";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/id.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Masuk";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Masuk dengan Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Masuk dengan Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Dapatkan Google app gratis dan masuk ke aplikasi dengan Akun Google. Tidak perlu mengingat sandi.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Batal";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Ambil";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "Oke";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Batal";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Setelan";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Tidak dapat login ke akun";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Administrator mengharuskan Anda menyetel kode sandi di perangkat ini untuk mengakses akun ini. Setel kode sandi dan coba lagi.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Perangkat ini tidak sesuai dengan kebijakan keamanan yang disetel oleh administrator.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Sambungkan dengan Aplikasi Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Untuk melindungi data organisasi, Anda harus tersambung dengan aplikasi Device Policy sebelum login.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Sambungkan";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/it.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Accedi";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Accedi con Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Accedi con Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Scarica gratis l'app Google app e accedi alle app con il tuo account Google: liberati dai vincoli delle password.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Annulla";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Scarica";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Annulla";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Impostazioni";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Impossibile accedere all'account";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "L'amministratore richiede l'impostazione di un passcode sul dispositivo per accedere a questo account. Imposta un passcode e riprova.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Il dispositivo non è conforme alle norme di sicurezza stabilite dall'amministratore.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Vuoi collegarti all'app Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Per proteggere i dati della tua organizzazione, devi collegarti all'app Device Policy prima di accedere.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Collega";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/ja.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "ログイン";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Googleでログイン";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Googleでログイン";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "無料のGoogleアプリをインストールして、Googleアカウントでアプリにログインしよう。パスワードを覚えておく必要はありません。";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "キャンセル";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "インストール";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "キャンセル";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "設定";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "アカウントにログインできません";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "このアカウントにアクセスするには、この端末でパスコードを設定する必要があります。パスコードを設定してから、もう一度お試しください。";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "この端末は、管理者が設定したセキュリティ ポリシーに準拠していません。";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Device Policy アプリと接続しますか?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "組織のデータを保護するために、ログインする前に Device Policy アプリと接続する必要があります。";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "接続";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/ko.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "로그인";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Google 계정으로 로그인";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Google 계정으로 로그인";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "무료 Google 앱을 다운로드하여 Google 계정으로 앱에 로그인하세요. 비밀번호를 기억할 필요가 없습니다.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "취소";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "설치";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "확인";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "취소";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "설정";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "계정에 로그인할 수 없음";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "관리자의 설정에 따라 이 계정에 액세스하려면 사용 중인 기기에 비밀번호를 설정해야 합니다. 비밀번호를 설정한 후 다시 시도해 주세요.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "관리자가 설정한 보안 정책을 준수하지 않는 기기입니다.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Device Policy 앱과 연결하시겠습니까?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "조직의 데이터를 보호하려면 로그인하기 전에 Device Policy 앱과 연결해야 합니다.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "연결";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/ms.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Log masuk";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Log masuk dengan Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Log masuk dengan Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Dapatkan apl Google percuma dan log masuk ke apl menggunakan Akaun Google anda. Tidak perlu mengingati kata laluan.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Batal";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Dapatkan";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Batal";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Tetapan";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Tidak dapat log masuk ke akaun";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Pentadbir menghendaki anda menetapkan kod laluan pada peranti ini untuk mengakses akaun ini. Sila tetapkan kod laluan, kemudian cuba lagi.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Peranti tidak mematuhi dasar keselamatan yang ditetapkan oleh pentadbir anda.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Berhubung dengan Apl Dasar Peranti?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Untuk melindungi data organisasi anda, anda mesti berhubung dengan apl Dasar Peranti sebelum log masuk.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Hubungkan";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/nb.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Logg på";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Logg på med Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Logg på med Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Skaff deg den gratis Google-appen, og logg på apper med Google-kontoen din. Du trenger ikke å huske passord.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Avbryt";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Hent";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Avbryt";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Innstillinger";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Kan ikke logge på kontoen";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Administratoren din krever at du angir en adgangskode på denne enheten for å få tilgang til kontoen. Angi en adgangskode, og prøv på nytt.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Enheten overholder ikke retningslinjene for sikkerhet som ble angitt av administratoren din.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Vil du koble til med Device Policy-appen?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "For å beskytte dataene til organisasjonen din må du koble til med Device Policy-appen før du logger på.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Koble til";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/nl.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Inloggen";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Inloggen met Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Inloggen met Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Installeer de gratis Google-app en log in bij apps met uw Google-account. U hoeft geen wachtwoorden te onthouden.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Annuleren";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Installeren";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Annuleren";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Instellingen";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Kan niet inloggen op account";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Uw beheerder vereist dat u een toegangscode instelt op dit apparaat om toegang te krijgen tot dit account. Stel een toegangscode in en probeer het opnieuw.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Het apparaat voldoet niet aan het beveiligingsbeleid dat is ingesteld door uw beheerder.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Verbinden met Device Policy-app?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Ter bescherming van de gegevens van uw organisatie moet u verbinding maken met de Device Policy-app voordat u inlogt.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Verbinden";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/pl.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Zaloguj się";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Zaloguj się przez Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Zaloguj się przez Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Pobierz darmową aplikację Google i zaloguj się do aplikacji, używając konta Google. Nie musisz pamiętać haseł.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Anuluj";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Pobierz";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Anuluj";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Ustawienia";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Nie można zalogować się na konto";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Administrator wymaga ustawienia kodu dostępu do konta na tym urządzeniu. Ustaw kod dostępu i spróbuj ponownie.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Urządzenie nie jest zgodne z zasadami bezpieczeństwa ustanowionymi przez Twojego administratora.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Połączyć z aplikacją Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Aby chronić dane organizacji, przed zalogowaniem musisz się połączyć z aplikacją Device Policy.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Połącz";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/pt.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Fazer login";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Fazer login com o Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Fazer login com o Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Faça o download do Google app gratuitamente e faça login em aplicativos com sua Conta do Google. Não há necessidade de lembrar senhas.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Cancelar";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Instalar";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Cancelar";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Configurações";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Não foi possível fazer login na conta";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Seu administrador exige que você defina uma senha neste dispositivo para acessar esta conta. Defina uma senha e tente novamente.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "O dispositivo não está em conformidade com a política de segurança definida pelo administrador.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Conectar-se ao app Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Para proteger os dados da sua organização, você precisa se conectar ao app Device Policy antes de fazer login.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Conectar";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/pt_BR.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Fazer login";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Fazer login com o Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Fazer login com o Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Faça o download do Google app gratuitamente e faça login em aplicativos com sua Conta do Google. Não há necessidade de lembrar senhas.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Cancelar";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Instalar";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Cancelar";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Configurações";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Não foi possível fazer login na conta";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Seu administrador exige que você defina uma senha neste dispositivo para acessar esta conta. Defina uma senha e tente novamente.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "O dispositivo não está em conformidade com a política de segurança definida pelo administrador.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Conectar-se ao app Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Para proteger os dados da sua organização, você precisa se conectar ao app Device Policy antes de fazer login.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Conectar";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/pt_PT.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Iniciar sessão";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Iniciar sessão com o Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Iniciar sessão com o Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Obtenha a aplicação Google gratuita e inicie sessão nas aplicações com a sua Conta Google. Não precisa de memorizar palavras-passe.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Cancelar";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Obter";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Cancelar";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Definições";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Não é possível iniciar sessão na conta";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "O administrador requer a definição de um código secreto neste dispositivo para aceder a esta conta. Defina um código secreto e tente novamente.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "O dispositivo não está em conformidade com a política de segurança definida pelo seu administrador.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Pretende ligar-se à aplicação Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Para proteger os dados da sua entidade, tem de se ligar à aplicação Device Policy antes de iniciar sessão.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Ligar";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/ro.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Conectați-vă";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Conectați-vă cu Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Conectați-vă cu Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Instalați aplicația Google gratuită și conectați-vă la aplicații folosind Contul Google. Nu mai trebuie să rețineți parolele.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Anulați";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Instalați";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Anulați";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Setări";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Nu vă puteți conecta la cont";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Administratorul impune să setați o parolă pe acest dispozitiv ca să accesați contul. Setați o parolă și încercați din nou.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Dispozitivul nu respectă politica de securitate stabilită de administrator.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Vă conectați cu aplicația Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Pentru a vă proteja datele organizației, trebuie să vă conectați cu aplicația Device Policy înainte de a vă conecta.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Conectați";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/ru.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Войти";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Войти в аккаунт Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Надоело вводить пароль?";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Установите бесплатное приложение Google и входите в другие мобильные программы, используя учетные данные своего аккаунта.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Отмена";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Установить";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "ОК";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Отмена";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Настройки";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Не удалось войти в аккаунт";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "В соответствии с требованиями администратора для входа в аккаунт необходимо установить на устройстве код доступа. Сделайте это и повторите попытку.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Устройство не соответствует правилам безопасности, которые установлены администратором.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Подключить приложение Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "В целях защиты корпоративных данных перед входом в аккаунт необходимо подключить приложение Device Policy.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Подключить";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/sk.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Prihlásiť sa";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Prihlásiť sa pomocou účtu Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Prihlásenie pomocou účtu Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Nainštalujte si zdarma aplikáciu Google a prihlasujte sa do aplikácií pomocou účtu Google. Nebudete si už musieť pamätať rôzne heslá.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Zrušiť";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Inštalovať";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Zrušiť";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Nastavenia";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Nedá sa prihlásiť do účtu";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Správca vyžaduje, aby ste v tomto zariadení nastavili vstupný kód na prístup do príslušného účtu. Nastavte vstupný kód a skúste to znova.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Zariadenie nespĺňa pravidlá zabezpečenia nastavené vaším správcom.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Prepojiť s aplikáciou Pravidlá pre zariadenie?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Na to, aby bolo možné chrániť dáta vašej organizácie, je nutné pred prihlásením aktivovať prepojenie s aplikáciou Pravidlá pre zariadenie.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Prepojiť";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/sv.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Logga in";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Logga in med Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Logga in med Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Hämta Google-appen utan kostnad och logga in i appar med ditt Google-konto. Du behöver inte komma ihåg en massa lösenord.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Avbryt";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Hämta";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "Ok";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Avbryt";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Inställningar";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Det gick inte att logga in på kontot";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Administratören kräver att du anger ett lösenord på den här enheten för att få åtkomst till kontot. Ange ett lösenord och försök igen.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Säkerhetspolicyn som administratören har angett efterlevs inte på enheten.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Vill du ansluta med appen Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Du måste ansluta med appen Device Policy innan du loggar in för att skydda organisationens data.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Anslut";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/th.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "ลงชื่อเข้าใช้";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "ลงชื่อเข้าใช้ด้วย Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "ลงชื่อเข้าใช้ด้วย Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "ติดตั้งแอป Google ฟรีและลงชื่อเข้าใช้แอปต่างๆ ด้วยบัญชี Google คุณไม่ต้องจำรหัสผ่านอีกแล้ว";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "ยกเลิก";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "ติดตั้ง";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "ตกลง";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "ยกเลิก";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "การตั้งค่า";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "ลงชื่อเข้าใช้บัญชีไม่ได้";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "ผู้ดูแลระบบกำหนดให้คุณตั้งรหัสผ่านในอุปกรณ์นี้เพื่อเข้าถึงบัญชีนี้ โปรดตั้งรหัสผ่าน แล้วลองอีกครั้ง";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "อุปกรณ์ไม่ตรงตามนโยบายความปลอดภัยที่กำหนดโดยผู้ดูแลระบบของคุณ";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "เชื่อมต่อแอป Device Policy ไหม";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "เพื่อปกป้องข้อมูลขององค์กร คุณต้องเชื่อมต่อแอป Device Policy ก่อนลงชื่อเข้าสู่ระบบ";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "เชื่อมต่อ";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/tr.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Oturum aç";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Google ile oturum aç";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Google ile oturum aç";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Ücretsiz Google uygulamasını edinin ve uygulamalarda Google Hesabınızla oturum açın. Şifrelerinizi hatırlamanız gerekmez.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "İptal";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Yükle";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "Tamam";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "İptal";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Ayarlar";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Hesapta oturum açılamıyor";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Yöneticiniz, bu hesaba erişmek için bu cihazda bir şifre kodu ayarlamanızı gerektiriyor. Lütfen şifre kodu ayarlayın ve tekrar deneyin.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Bu cihaz, yöneticinizin ayarladığı güvenlik politikasıyla uyumlu değil.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Cihaz Politika Uygulamasına bağlanılsın mı?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Kuruluşunuzun verilerini korumak için, giriş yapmadan önce Cihaz Politikası uygulamasına bağlanmalısınız.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Bağlan";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/uk.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Увійти";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Увійти в обліковий запис Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Входьте в обліковий запис Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Установіть безкоштовний додаток Google і входьте в обліковий запис Google у додатках. Не потрібно запам’ятовувати паролі.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Скасувати";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Установити";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Скасувати";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Налаштування";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Не вдається ввійти в обліковий запис";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Щоб увійти в обліковий запис, потрібно налаштувати код доступу на пристрої. Зробіть це й повторіть спробу.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Пристрій не відповідає правилу безпеки, яке налаштував адміністратор.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "З’єднатися з додатком Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Щоб захистити дані організації, потрібно з’єднатися з додатком Device Policy, перш ніж увійти.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "З’єднатися";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/vi.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "Đăng nhập";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "Đăng nhập bằng Google";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "Đăng nhập bằng Google";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "Tải ứng dụng Google miễn phí và đăng nhập vào các ứng dụng bằng Tài khoản Google của bạn. Không cần phải nhớ mật khẩu.";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "Hủy";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "Tải";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "OK";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "Hủy";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "Cài đặt";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "Không thể đăng nhập vào tài khoản";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "Quản trị viên của bạn yêu cầu bạn phải đặt mật mã trên thiết bị này để truy cập vào tài khoản này. Hãy đặt mật mã và thử lại.";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "Thiết bị này không tuân thủ chính sách bảo mật do quản trị viên của bạn thiết lập.";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "Kết nối với ứng dụng Device Policy?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "Để bảo vệ dữ liệu của tổ chức của mình, bạn phải kết nối với ứng dụng Device Policy trước khi đăng nhập.";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "Kết nối";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/zh_CN.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "登录";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "使用 Google 帐号登录";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "使用 Google 帐号登录";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "安装免费的“Google”应用后,您可以使用自己的 Google 帐号登录众多应用(无需记住众多密码)。";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "取消";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "安装";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "确定";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "取消";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "设置";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "无法登录帐号";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "您的管理员要求您必须先在此设备上设置密码,然后才能访问此帐号。请设置密码并重试。";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "该设备不符合管理员设置的安全政策。";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "要关联 Device Policy 应用吗?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "要保护您组织的数据,您必须在登录前关联 Device Policy 应用。";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "关联";

+ 44 - 0
GoogleSignIn/Resources/GoogleSignIn.bundle/zh_TW.lproj/GoogleSignIn.strings

@@ -0,0 +1,44 @@
+/* Sign-in button text */
+"Sign in" = "登入";
+
+/* Long form sign-in button text */
+"Sign in with Google" = "登入 Google 帳戶";
+
+/* The title of the promotional prompt to install the Google app. */
+"PromoTitle" = "登入 Google 帳戶";
+
+/* The body message of the promotional prompt to install the Google app. */
+"PromoMessage" = "只要安裝免費的 Google app,即可使用 Google 帳戶登入應用程式,而不必費心記住密碼。";
+
+/* The cancel button on the promotional prompt to install the Google app. */
+"PromoActionCancel" = "取消";
+
+/* The install button on the promotional prompt to install the Google app. */
+"PromoActionInstall" = "安裝";
+
+/* The text for the button for user to acknowledge and dismiss a dialog. */
+"OK" = "確定";
+
+/* The text for the button for user to dismiss a dialog without taking any action. */
+"Cancel" = "取消";
+
+/* The name of the iOS native "Settings" app. */
+"SettingsAppName" = "設定";
+
+/* The title for the error dialog for unable to sign in because of EMM policy. */
+"EmmErrorTitle" = "無法登入帳戶";
+
+/* The text in the error dialog asking user to set up a passcode for the device due to EMM policy. */
+"EmmPasscodeRequired" = "管理員要求您必須為這個裝置設定通行碼,才能存取這個帳戶。請設定通行碼,然後再試一次。";
+
+/* The text in the error dialog informing user that EMM policy prevented sign-in on the device. */
+"EmmGeneralError" = "這部裝置不符合您的管理員所設定的安全性政策規定。";
+
+/* The title in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectTitle" = "要連結 Device Policy 應用程式嗎?";
+
+/* The text in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectText" = "為了保護貴機構的資料,您必須在登入前連結 Device Policy 應用程式。";
+
+/* The action button label in the error dialog informing user that connecting with Device Policy app is required. */
+"EmmConnectLabel" = "連結";

+ 33 - 0
GoogleSignIn/Sources/GIDAuthStateMigration.h

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// A class providing migration support for auth state saved by older versions of the SDK.
+@interface GIDAuthStateMigration : NSObject
+
+// Perform a one-time migration for auth state saved by GPPSignIn 1.x or GIDSignIn 1.0 - 4.x to the
+// GTMAppAuth storage introduced in GIDSignIn 5.0.
++ (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL
+                       callbackPath:(NSString *)callbackPath
+                       keychainName:(NSString *)keychainName
+                     isFreshInstall:(BOOL)isFreshInstall;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 173 - 0
GoogleSignIn/Sources/GIDAuthStateMigration.m

@@ -0,0 +1,173 @@
+// Copyright 2021 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/GIDAuthStateMigration.h"
+
+#import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+@import GTMAppAuth;
+#else
+#import <AppAuth/AppAuth.h>
+#import <GTMAppAuth/GTMAppAuth.h>
+#import <GTMAppAuth/GTMKeychain.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+// User preference key to detect whether or not the migration check has been performed.
+static NSString *const kMigrationCheckPerformedKey = @"GID_MigrationCheckPerformed";
+
+// Keychain account used to store additional state in SDKs previous to v5, including GPPSignIn.
+static NSString *const kOldKeychainAccount = @"GooglePlus";
+
+// The value used for the kSecAttrGeneric key by GTMAppAuth and GTMOAuth2.
+static NSString *const kGenericAttribute = @"OAuth";
+
+// Keychain service name used to store the last used fingerprint value.
+static NSString *const kFingerprintService = @"fingerprint";
+
+@implementation GIDAuthStateMigration
+
++ (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL
+                       callbackPath:(NSString *)callbackPath
+                       keychainName:(NSString *)keychainName
+                     isFreshInstall:(BOOL)isFreshInstall {
+  // See if we've performed the migration check previously.
+  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
+  if ([defaults boolForKey:kMigrationCheckPerformedKey]) {
+    return;
+  }
+
+  // If this is not a fresh install, attempt to migrate state.  If this is a fresh install, take no
+  // action and go on to mark the migration check as having been performed.
+  if (!isFreshInstall) {
+    // Attempt migration
+    GTMAppAuthFetcherAuthorization *authorization =
+        [self extractAuthorizationWithTokenURL:tokenURL callbackPath:callbackPath];
+
+    // If migration was successful, save our migrated state to the keychain.
+    if (authorization) {
+      // If we're unable to save to the keychain, return without marking migration performed.
+      if (![GTMAppAuthFetcherAuthorization saveAuthorization:authorization
+                                           toKeychainForName:keychainName]) {
+        return;
+      };
+    }
+  }
+
+  // Mark the migration check as having been performed.
+  [defaults setBool:YES forKey:kMigrationCheckPerformedKey];
+}
+
+// Returns a |GTMAppAuthFetcherAuthorization| object containing any old auth state or |nil| if none
+// was found or the migration failed.
++ (nullable GTMAppAuthFetcherAuthorization *)
+    extractAuthorizationWithTokenURL:(NSURL *)tokenURL callbackPath:(NSString *)callbackPath {
+  // Retrieve the last used fingerprint.
+  NSString *fingerprint = [GIDAuthStateMigration passwordForService:kFingerprintService];
+  if (!fingerprint) {
+    return nil;
+  }
+
+  // Retrieve the GTMOAuth2 persistence string.
+  NSString *GTMOAuth2PersistenceString = [GTMKeychain passwordFromKeychainForName:fingerprint];
+  if (!GTMOAuth2PersistenceString) {
+    return nil;
+  }
+
+  // Parse the fingerprint.
+  NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier];
+  NSString *pattern =
+      [NSString stringWithFormat:@"^%@-(.+)-(?:email|profile|https:\\/\\/).*$", bundleID];
+  NSError *error;
+  NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
+                                                                         options:0
+                                                                           error:&error];
+  NSRange matchRange = NSMakeRange(0, fingerprint.length);
+  NSArray<NSTextCheckingResult *> *matches = [regex matchesInString:fingerprint
+                                                            options:0
+                                                              range:matchRange];
+  if ([matches count] != 1) {
+    return nil;
+  }
+
+  // Extract the client ID from the fingerprint.
+  NSString *clientID = [fingerprint substringWithRange:[matches[0] rangeAtIndex:1]];
+
+  // Generate the redirect URI from the extracted client ID.
+  NSString *scheme =
+      [[[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:clientID] clientIdentifierScheme];
+  NSString *redirectURI = [NSString stringWithFormat:@"%@:%@", scheme, callbackPath];
+
+  // Retrieve the additional token request parameters value.
+  NSString *additionalTokenRequestParametersService =
+      [NSString stringWithFormat:@"%@~~atrp", fingerprint];
+  NSString *additionalTokenRequestParameters =
+      [GIDAuthStateMigration passwordForService:additionalTokenRequestParametersService];
+
+  // Generate a persistence string that includes additional token request parameters if present.
+  NSString *persistenceString = GTMOAuth2PersistenceString;
+  if (additionalTokenRequestParameters) {
+    persistenceString = [NSString stringWithFormat:@"%@&%@",
+                         GTMOAuth2PersistenceString,
+                         additionalTokenRequestParameters];
+  }
+
+  // Use |GTMOAuth2KeychainCompatibility| to generate a |GTMAppAuthFetcherAuthorization| from the
+  // persistence string, redirect URI, client ID, and token endpoint URL.
+  GTMAppAuthFetcherAuthorization *authorization = [GTMOAuth2KeychainCompatibility
+      authorizeFromPersistenceString:persistenceString
+                            tokenURL:tokenURL
+                         redirectURI:redirectURI
+                            clientID:clientID
+                        clientSecret:nil];
+
+  return authorization;
+}
+
+// Returns the password string for a given service string stored by an old version of the SDK or
+// |nil| if no matching keychain item was found.
++ (nullable NSString *)passwordForService:(NSString *)service {
+  if (!service.length) {
+    return nil;
+  }
+  CFDataRef result = NULL;
+  NSDictionary<id, id> *query = @{
+    (id)kSecClass : (id)kSecClassGenericPassword,
+    (id)kSecAttrGeneric : kGenericAttribute,
+    (id)kSecAttrAccount : kOldKeychainAccount,
+    (id)kSecAttrService : service,
+    (id)kSecReturnData : (id)kCFBooleanTrue,
+    (id)kSecMatchLimit : (id)kSecMatchLimitOne,
+  };
+  OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef *)&result);
+  NSData *passwordData;
+  if (status == noErr && [(__bridge NSData *)result length] > 0) {
+    passwordData = [(__bridge NSData *)result copy];
+  }
+  if (result != NULL) {
+    CFRelease(result);
+  }
+  if (!passwordData) {
+    return nil;
+  }
+  NSString *password = [[NSString alloc] initWithData:passwordData encoding:NSUTF8StringEncoding];
+  return password;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 340 - 0
GoogleSignIn/Sources/GIDAuthentication.m

@@ -0,0 +1,340 @@
+// Copyright 2021 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/Public/GoogleSignIn/GIDAuthentication.h"
+
+#import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
+
+#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
+#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+#import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+@import GTMAppAuth;
+#else
+#import <AppAuth/OIDAuthState.h>
+#import <AppAuth/OIDAuthorizationRequest.h>
+#import <AppAuth/OIDAuthorizationResponse.h>
+#import <AppAuth/OIDAuthorizationService.h>
+#import <AppAuth/OIDError.h>
+#import <AppAuth/OIDIDToken.h>
+#import <AppAuth/OIDTokenRequest.h>
+#import <AppAuth/OIDTokenResponse.h>
+#import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Minimal time interval before expiration for the access token or it needs to be refreshed.
+NSTimeInterval kMinimalTimeToExpire = 60.0;
+
+// Key constants used for encode and decode.
+static NSString *const kAuthStateKey = @"authState";
+
+// Additional parameter names for EMM.
+static NSString *const kEMMSupportParameterName = @"emm_support";
+static NSString *const kEMMOSVersionParameterName = @"device_os";
+static NSString *const kEMMPasscodeInfoParameterName = @"emm_passcode_info";
+
+// Old UIDevice system name for iOS.
+static NSString *const kOldIOSSystemName = @"iPhone OS";
+
+// New UIDevice system name for iOS.
+static NSString *const kNewIOSSystemName = @"iOS";
+
+// The specialized GTMAppAuthFetcherAuthorization delegate that handles potential EMM error
+// responses.
+@interface GTMAppAuthFetcherAuthorizationEMMChainedDelegate : NSObject
+
+// Initializes with chained delegate and selector.
+- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector;
+
+// The callback method for GTMAppAuthFetcherAuthorization to invoke.
+- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth
+               request:(NSMutableURLRequest *)request
+     finishedWithError:(nullable NSError *)error;
+
+@end
+
+@implementation GTMAppAuthFetcherAuthorizationEMMChainedDelegate {
+  // We use a weak reference here to match GTMAppAuthFetcherAuthorization.
+  __weak id _delegate;
+  SEL _selector;
+  // We need to maintain a reference to the chained delegate because GTMAppAuthFetcherAuthorization
+  // only keeps a weak reference.
+  GTMAppAuthFetcherAuthorizationEMMChainedDelegate *_retained_self;
+}
+
+- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector {
+  self = [super init];
+  if (self) {
+    _delegate = delegate;
+    _selector = selector;
+    _retained_self = self;
+  }
+  return self;
+}
+
+- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth
+               request:(NSMutableURLRequest *)request
+     finishedWithError:(nullable NSError *)error {
+  [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
+    if (!_delegate || !_selector) {
+      return;
+    }
+    NSMethodSignature *signature = [_delegate methodSignatureForSelector:_selector];
+    if (!signature) {
+      return;
+    }
+    id argument1 = auth;
+    id argument2 = request;
+    id argument3 = error;
+    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
+    [invocation setTarget:_delegate];  // index 0
+    [invocation setSelector:_selector];  // index 1
+    [invocation setArgument:&argument1 atIndex:2];
+    [invocation setArgument:&argument2 atIndex:3];
+    [invocation setArgument:&argument3 atIndex:4];
+    [invocation invoke];
+  }];
+  // Prepare to deallocate the chained delegate instance because the above block will retain the
+  // iVar references it uses.
+  _retained_self = nil;
+}
+
+@end
+
+// A specialized GTMAppAuthFetcherAuthorization subclass with EMM support.
+@interface GTMAppAuthFetcherAuthorizationWithEMMSupport : GTMAppAuthFetcherAuthorization
+@end
+
+@implementation GTMAppAuthFetcherAuthorizationWithEMMSupport
+
+- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
+                delegate:(id)delegate
+       didFinishSelector:(SEL)sel {
+  GTMAppAuthFetcherAuthorizationEMMChainedDelegate *chainedDelegate =
+      [[GTMAppAuthFetcherAuthorizationEMMChainedDelegate alloc] initWithDelegate:delegate
+                                                                        selector:sel];
+  [super authorizeRequest:request
+                 delegate:chainedDelegate
+        didFinishSelector:@selector(authentication:request:finishedWithError:)];
+}
+
+- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
+       completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)handler {
+  [super authorizeRequest:request completionHandler:^(NSError *_Nullable error) {
+    [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
+      handler(error);
+    }];
+  }];
+}
+
+@end
+
+@implementation GIDAuthentication {
+  // A queue for pending authentication handlers so we don't fire multiple requests in parallel.
+  // Access to this ivar should be synchronized.
+  NSMutableArray *_authenticationHandlerQueue;
+}
+
+- (instancetype)initWithAuthState:(OIDAuthState *)authState {
+  if (!authState) {
+    return nil;
+  }
+  self = [super init];
+  if (self) {
+    _authenticationHandlerQueue = [[NSMutableArray alloc] init];
+    _authState = authState;
+  }
+  return self;
+}
+
+#pragma mark - Public property accessors
+
+- (NSString *)clientID {
+  return _authState.lastAuthorizationResponse.request.clientID;
+}
+
+- (NSString *)accessToken {
+  return _authState.lastTokenResponse.accessToken;
+}
+
+- (NSDate *)accessTokenExpirationDate {
+  return _authState.lastTokenResponse.accessTokenExpirationDate;
+}
+
+- (NSString *)refreshToken {
+  return _authState.refreshToken;
+}
+
+- (nullable NSString *)idToken {
+  return _authState.lastTokenResponse.idToken;
+}
+
+- (nullable NSDate *)idTokenExpirationDate {
+  return [[[OIDIDToken alloc] initWithIDTokenString:self.idToken] expiresAt];
+}
+
+#pragma mark - Private property accessors
+
+- (NSString *)emmSupport {
+  return
+      _authState.lastAuthorizationResponse.request.additionalParameters[kEMMSupportParameterName];
+}
+
+#pragma mark - Public methods
+
+- (id<GTMFetcherAuthorizationProtocol>)fetcherAuthorizer {
+  GTMAppAuthFetcherAuthorization *authorization = self.emmSupport ?
+      [[GTMAppAuthFetcherAuthorizationWithEMMSupport alloc] initWithAuthState:_authState] :
+      [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:_authState];
+  authorization.tokenRefreshDelegate = self;
+  return authorization;
+}
+
+- (void)doWithFreshTokens:(GIDAuthenticationAction)action {
+  if (!([self.accessTokenExpirationDate timeIntervalSinceNow] < kMinimalTimeToExpire ||
+      (self.idToken && [self.idTokenExpirationDate timeIntervalSinceNow] < kMinimalTimeToExpire))) {
+    action(self, nil);
+    return;
+  }
+  @synchronized (_authenticationHandlerQueue) {
+    // Push the handler into the callback queue.
+    [_authenticationHandlerQueue addObject:[action copy]];
+    if (_authenticationHandlerQueue.count > 1) {
+      // This is not the first handler in the queue, no fetch is needed.
+      return;
+    }
+  }
+  // This is the first handler in the queue, a fetch is needed.
+  OIDTokenRequest *tokenRefreshRequest =
+      [_authState tokenRefreshRequestWithAdditionalParameters:
+          [GIDAuthentication updatedEMMParametersWithParameters:
+              _authState.lastTokenResponse.request.additionalParameters]];
+  [OIDAuthorizationService performTokenRequest:tokenRefreshRequest
+                 originalAuthorizationResponse:_authState.lastAuthorizationResponse
+                                      callback:^(OIDTokenResponse *_Nullable tokenResponse,
+                                                 NSError *_Nullable error) {
+    if (tokenResponse) {
+      [self willChangeValueForKey:NSStringFromSelector(@selector(accessToken))];
+      [self willChangeValueForKey:NSStringFromSelector(@selector(accessTokenExpirationDate))];
+      [self willChangeValueForKey:NSStringFromSelector(@selector(idToken))];
+      [self willChangeValueForKey:NSStringFromSelector(@selector(idTokenExpirationDate))];
+      [_authState updateWithTokenResponse:tokenResponse error:nil];
+      [self didChangeValueForKey:NSStringFromSelector(@selector(accessToken))];
+      [self didChangeValueForKey:NSStringFromSelector(@selector(accessTokenExpirationDate))];
+      [self didChangeValueForKey:NSStringFromSelector(@selector(idToken))];
+      [self didChangeValueForKey:NSStringFromSelector(@selector(idTokenExpirationDate))];
+    } else {
+      if (error.domain == OIDOAuthTokenErrorDomain) {
+        [_authState updateWithAuthorizationError:error];
+      }
+    }
+    [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
+      // Process the handler queue to call back.
+      NSArray *authenticationHandlerQueue;
+      @synchronized(_authenticationHandlerQueue) {
+        authenticationHandlerQueue = [_authenticationHandlerQueue copy];
+        [_authenticationHandlerQueue removeAllObjects];
+      }
+      for (GIDAuthenticationAction action in authenticationHandlerQueue) {
+        action(error ? nil : self, error);
+      }
+    }];
+  }];
+}
+
+#pragma mark - Private methods
+
++ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters
+                                emmSupport:(nullable NSString *)emmSupport
+                    isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired {
+  if (!emmSupport) {
+    return parameters;
+  }
+  NSMutableDictionary *allParameters = [(parameters ?: @{}) mutableCopy];
+  allParameters[kEMMSupportParameterName] = emmSupport;
+  UIDevice *device = [UIDevice currentDevice];
+  NSString *systemName = device.systemName;
+  if ([systemName isEqualToString:kOldIOSSystemName]) {
+    systemName = kNewIOSSystemName;
+  }
+  allParameters[kEMMOSVersionParameterName] =
+      [NSString stringWithFormat:@"%@ %@", systemName, device.systemVersion];
+  if (isPasscodeInfoRequired) {
+    allParameters[kEMMPasscodeInfoParameterName] = [GIDMDMPasscodeState passcodeState].info;
+  }
+  allParameters[kSDKVersionLoggingParameter] = GIDVersion();
+  return allParameters;
+}
+
++ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters {
+  return [self parametersWithParameters:parameters
+                             emmSupport:parameters[kEMMSupportParameterName]
+                 isPasscodeInfoRequired:parameters[kEMMPasscodeInfoParameterName] != nil];
+}
+
++ (void)handleTokenFetchEMMError:(nullable NSError *)error
+                      completion:(void (^)(NSError *_Nullable))completion {
+  NSDictionary *errorJSON = error.userInfo[OIDOAuthErrorResponseErrorKey];
+  if (errorJSON) {
+    __block BOOL handled = NO;
+    handled = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:errorJSON
+                                                                completion:^() {
+      if (handled) {
+        completion([NSError errorWithDomain:kGIDSignInErrorDomain
+                                       code:kGIDSignInErrorCodeEMM
+                                   userInfo:error.userInfo]);
+      } else {
+        completion(error);
+      }
+    }];
+  } else {
+    completion(error);
+  }
+}
+
+#pragma mark - GTMAppAuthFetcherAuthorizationTokenRefreshDelegate
+
+- (nullable NSDictionary *)additionalRefreshParameters:
+    (GTMAppAuthFetcherAuthorization *)authorization {
+  return [GIDAuthentication updatedEMMParametersWithParameters:
+      authorization.authState.lastTokenResponse.request.additionalParameters];
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)decoder {
+  self = [super init];
+  if (self) {
+    _authenticationHandlerQueue = [[NSMutableArray alloc] init];
+    _authState = [decoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthStateKey];
+  }
+  return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)encoder {
+  [encoder encodeObject:_authState forKey:kAuthStateKey];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 54 - 0
GoogleSignIn/Sources/GIDAuthentication_Private.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 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/Public/GoogleSignIn/GIDAuthentication.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+@import GTMAppAuth;
+#else
+#import <AppAuth/AppAuth.h>
+#import <GTMAppAuth/GTMAppAuth.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Internal methods for the class that are not part of the public API.
+@interface GIDAuthentication () <GTMAppAuthFetcherAuthorizationTokenRefreshDelegate>
+
+// A representation of the state of the OAuth session for this instance.
+@property(nonatomic, readonly) OIDAuthState *authState;
+
+// A string indicating support for Enterprise Mobility Management.
+@property(nonatomic, readonly) NSString *emmSupport;
+
+- (instancetype)initWithAuthState:(OIDAuthState *)authState;
+
+// Gets a new set of URL parameters that also contains EMM-related URL parameters if needed.
++ (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters
+                                emmSupport:(nullable NSString *)emmSupport
+                    isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired;
+
+// Gets a new set of URL parameters that contains updated EMM-related URL parameters if needed.
++ (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters;
+
+// Handles potential EMM error from token fetch response.
++ (void)handleTokenFetchEMMError:(nullable NSError *)error
+                      completion:(void (^)(NSError *_Nullable))completion;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 52 - 0
GoogleSignIn/Sources/GIDCallbackQueue.h

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class GIDCallbackQueue;
+
+// The block type of callbacks in the queue.
+typedef void (^GIDCallbackQueueCallback)();
+
+// The class handles a queue for callbacks for asynchronous operations.
+// The queue starts in a ready state. Call |wait| and |next| to mark the
+// start and end of asynchronous operations.
+@interface GIDCallbackQueue : NSObject
+
+// Marks the start of an asynchronous operation. Any remaining callbacks will
+// not be called until |next| is called. The queue object will be retained while
+// some asynchronous operation is pending.
+- (void)wait;
+
+// Marks the end of an asynchronous operation. If no more operation remain,
+// all remaining callbacks are called in the order they are added. Note that
+// some earlier callbackes can start asynchronous operations themselves, thus
+// blocking later callbacks until they are finished.
+- (void)next;
+
+// Resets the callback queue to the ready state and removes all callbacks.
+- (void)reset;
+
+// Adds a callback to the end of the callback queue. Callbacks added later will
+// only be called when both the callbacks added eariler and the asynchronous
+// operations they started if any are finished.
+- (void)addCallback:(GIDCallbackQueueCallback)callback;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 99 - 0
GoogleSignIn/Sources/GIDCallbackQueue.m

@@ -0,0 +1,99 @@
+// Copyright 2021 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/GIDCallbackQueue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GIDCallbackQueue () {
+  // Whether we are in the middle of firing callbacks loop.
+  BOOL _firing;
+
+  // Number of currently pending operations.
+  int _pending;  // number of pending operations
+
+  // The ordered list of callback blocks.
+  NSMutableArray *_queue;
+
+  // A strong reference back to self to prevent it from being released when
+  // there is operation pending.
+  GIDCallbackQueue *_strongSelf;
+}
+
+@end
+
+@implementation GIDCallbackQueue
+
+- (id)init {
+  self = [super init];
+  if (self) {
+    _queue = [NSMutableArray new];
+  }
+  return self;
+}
+
+- (void)wait {
+  _pending++;
+  // The queue itself should be retained as long as there are pending
+  // operations.
+  _strongSelf = self;
+}
+
+- (void)next {
+  if (!_pending) {
+    return;
+  }
+  _pending--;
+  if (!_pending) {
+    // Use an autoreleasing variable to hold self temporarily so it is not
+    // released while this method is executing.
+    __autoreleasing GIDCallbackQueue *autoreleasingSelf = self;
+    _strongSelf = nil;
+    [autoreleasingSelf fire];
+  }
+}
+
+- (void)reset {
+  [_queue removeAllObjects];
+  _pending = 0;
+  _strongSelf = nil;
+}
+
+- (void)addCallback:(GIDCallbackQueueCallback)callback {
+  if (!callback) {
+    return;
+  }
+  [_queue addObject:[callback copy]];
+  if (!_pending) {
+    [self fire];
+  }
+}
+
+// Fires the callbacks.
+- (void)fire {
+  if (_firing) {
+    return;
+  }
+  _firing = YES;
+  while (!_pending && [_queue count]) {
+    GIDCallbackQueueCallback callback = [_queue objectAtIndex:0];
+    [_queue removeObjectAtIndex:0];
+    callback();
+  }
+  _firing = NO;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 125 - 0
GoogleSignIn/Sources/GIDConfiguration.m

@@ -0,0 +1,125 @@
+// Copyright 2021 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/Public/GoogleSignIn/GIDConfiguration.h"
+
+// The key for the clientID property to be used with NSSecureCoding.
+static NSString *const kClientIDKey = @"clientID";
+
+// The key for the serverClientID property to be used with NSSecureCoding.
+static NSString *const kServerClientIDKey = @"serverClientID";
+
+// The key for the loginHint property to be used with NSSecureCoding.
+static NSString *const kLoginHintKey = @"loginHint";
+
+// The key for the hostedDomain property to be used with NSSecureCoding.
+static NSString *const kHostedDomainKey = @"hostedDomain";
+
+// The key for the openIDRealm property to be used with NSSecureCoding.
+static NSString *const kOpenIDRealmKey = @"openIDRealm";
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation GIDConfiguration
+
+- (instancetype)initWithClientID:(NSString *)clientID {
+  return [self initWithClientID:clientID
+                 serverClientID:nil
+                      loginHint:nil
+                   hostedDomain:nil
+                    openIDRealm:nil];
+}
+
+- (instancetype)initWithClientID:(NSString *)clientID
+                  serverClientID:(nullable NSString *)serverClientID {
+  return [self initWithClientID:clientID
+                 serverClientID:serverClientID
+                      loginHint:nil
+                   hostedDomain:nil
+                    openIDRealm:nil];
+}
+
+- (instancetype)initWithClientID:(NSString *)clientID
+                  serverClientID:(nullable NSString *)serverClientID
+                       loginHint:(nullable NSString *)loginHint
+                    hostedDomain:(nullable NSString *)hostedDomain
+                     openIDRealm:(nullable NSString *)openIDRealm {
+  self = [super init];
+  if (self) {
+    _clientID = [clientID copy];
+    _serverClientID = [serverClientID copy];
+    _loginHint = [loginHint copy];
+    _hostedDomain = [hostedDomain copy];
+    _openIDRealm = [openIDRealm copy];
+  }
+  return self;
+}
+
+// Extend NSObject's default description for easier debugging.
+- (NSString *)description {
+  return [NSString stringWithFormat:
+      @"<%@: %p, clientID: %@, serverClientID: %@, loginHint: %@, hostedDomain: %@, "
+          "openIDRealm: %@>",
+      NSStringFromClass([self class]),
+      self,
+      _clientID,
+      _serverClientID,
+      _loginHint,
+      _hostedDomain,
+      _openIDRealm];
+}
+
+#pragma mark - NSCopying
+
+- (instancetype)copyWithZone:(nullable NSZone *)zone {
+  // Instances of this class are immutable so return a reference to the original per NSCopying docs.
+  return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)coder {
+  NSString *clientID = [coder decodeObjectOfClass:[NSString class] forKey:kClientIDKey];
+  NSString *serverClientID = [coder decodeObjectOfClass:[NSString class] forKey:kServerClientIDKey];
+  NSString *loginHint = [coder decodeObjectOfClass:[NSString class] forKey:kLoginHintKey];
+  NSString *hostedDomain = [coder decodeObjectOfClass:[NSString class] forKey:kHostedDomainKey];
+  NSString *openIDRealm = [coder decodeObjectOfClass:[NSString class] forKey:kOpenIDRealmKey];
+
+  // We must have a client ID.
+  if (!clientID) {
+    return nil;
+  }
+
+  return [self initWithClientID:clientID
+                 serverClientID:serverClientID
+                      loginHint:loginHint
+                   hostedDomain:hostedDomain
+                    openIDRealm:openIDRealm];
+}
+
+- (void)encodeWithCoder:(NSCoder *)coder {
+  [coder encodeObject:_clientID forKey:kClientIDKey];
+  [coder encodeObject:_serverClientID forKey:kServerClientIDKey];
+  [coder encodeObject:_loginHint forKey:kLoginHintKey];
+  [coder encodeObject:_hostedDomain forKey:kHostedDomainKey];
+  [coder encodeObject:_openIDRealm forKey:kOpenIDRealmKey];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 37 - 0
GoogleSignIn/Sources/GIDEMMErrorHandler.h

@@ -0,0 +1,37 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// The handler for displaying EMM-specific errors to users.
+@interface GIDEMMErrorHandler : NSObject
+
+// Retrieve the shared instance of this class.
++ (instancetype)sharedInstance;
+
+// Handles EMM specific error that is returned in server response.
+// Returns whether or not an EMM-specific error is being handled by this invocation.
+// If the return value is |YES|, |completion| will be called asynchronously in the main thread
+// after the user interacts with the error dialog;
+// if the return value is |NO|, |completion| will be called before returning.
+- (BOOL)handleErrorFromResponse:(NSDictionary<NSString *, id> *)response
+                     completion:(void (^)())completion;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 285 - 0
GoogleSignIn/Sources/GIDEMMErrorHandler.m

@@ -0,0 +1,285 @@
+// Copyright 2021 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/GIDEMMErrorHandler.h"
+
+#import <UIKit/UIKit.h>
+
+#import "GoogleSignIn/Sources/GIDSignInStrings.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// The error key in the server response.
+static NSString *const kErrorKey = @"error";
+
+// Error strings in the server response.
+static NSString *const kGeneralErrorPrefix = @"emm_";
+static NSString *const kScreenlockRequiredError = @"emm_passcode_required";
+static NSString *const kAppVerificationRequiredErrorPrefix = @"emm_app_verification_required";
+
+// Optional separator between error prefix and the payload.
+static NSString *const kErrorPayloadSeparator = @":";
+
+// A list for recognized error codes.
+typedef enum {
+  ErrorCodeNone = 0,
+  ErrorCodeDeviceNotCompliant,
+  ErrorCodeScreenlockRequired,
+  ErrorCodeAppVerificationRequired,
+} ErrorCode;
+
+@implementation GIDEMMErrorHandler {
+  // Whether or not a dialog is pending user interaction.
+  BOOL _pendingDialog;
+}
+
++ (instancetype)sharedInstance {
+  static dispatch_once_t once;
+  static GIDEMMErrorHandler *sharedInstance;
+  dispatch_once(&once, ^{
+    sharedInstance = [[self alloc] init];
+  });
+  return sharedInstance;
+}
+
+- (BOOL)handleErrorFromResponse:(NSDictionary<NSString *, id> *)response
+                     completion:(void (^)())completion {
+  ErrorCode errorCode = ErrorCodeNone;
+  NSURL *appVerificationURL;
+  @synchronized(self) {  // for accessing _pendingDialog
+    if (!_pendingDialog && [UIAlertController class] &&
+        [response isKindOfClass:[NSDictionary class]]) {
+      id errorValue = response[kErrorKey];
+      if ([errorValue isEqual:kScreenlockRequiredError]) {
+        errorCode = ErrorCodeScreenlockRequired;
+      } else if ([errorValue hasPrefix:kAppVerificationRequiredErrorPrefix]) {
+        errorCode = ErrorCodeAppVerificationRequired;
+        NSString *appVerificationString =
+            [errorValue substringFromIndex:kAppVerificationRequiredErrorPrefix.length];
+        if ([appVerificationString hasPrefix:kErrorPayloadSeparator]) {
+          appVerificationString =
+              [appVerificationString substringFromIndex:kErrorPayloadSeparator.length];
+        }
+        appVerificationString = [appVerificationString
+            stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
+        if (appVerificationString.length) {
+          appVerificationURL = [NSURL URLWithString:appVerificationString];
+        }
+      } else if ([errorValue hasPrefix:kGeneralErrorPrefix]) {
+        errorCode = ErrorCodeDeviceNotCompliant;
+      }
+      if (errorCode) {
+        _pendingDialog = YES;
+      }
+    }
+  }
+  if (!errorCode) {
+    completion();
+    return NO;
+  }
+  // All UI must happen in the main thread.
+  dispatch_async(dispatch_get_main_queue(), ^() {
+    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
+    CGRect keyWindowBounds = CGRectIsEmpty(keyWindow.bounds) ?
+        keyWindow.bounds : [UIScreen mainScreen].bounds;
+    UIWindow *alertWindow = [[UIWindow alloc] initWithFrame:keyWindowBounds];
+    alertWindow.backgroundColor = [UIColor clearColor];
+    alertWindow.rootViewController = [[UIViewController alloc] init];
+    alertWindow.rootViewController.view.backgroundColor = [UIColor clearColor];
+    alertWindow.windowLevel = UIWindowLevelAlert;
+    [alertWindow makeKeyAndVisible];
+    void (^finish)() = ^{
+      alertWindow.hidden = YES;
+      alertWindow.rootViewController = nil;
+      [keyWindow makeKeyAndVisible];
+      _pendingDialog = NO;
+      completion();
+    };
+    UIAlertController *alert;
+    switch (errorCode) {
+      case ErrorCodeNone:
+        break;
+      case ErrorCodeScreenlockRequired:
+        alert = [self passcodeRequiredAlertWithCompletion:finish];
+        break;
+      case ErrorCodeAppVerificationRequired:
+        alert = [self appVerificationRequiredAlertWithURL:appVerificationURL completion:finish];
+        break;
+      case ErrorCodeDeviceNotCompliant:
+        alert = [self deviceNotCompliantAlertWithCompletion:finish];
+        break;
+    }
+    if (alert) {
+      [alertWindow.rootViewController presentViewController:alert animated:YES completion:nil];
+    } else {
+      // Should not happen but just in case.
+      finish();
+    }
+  });
+  return YES;
+}
+
+#pragma mark - Alerts
+
+// Returns an alert controller for device not compliant error.
+- (UIAlertController *)deviceNotCompliantAlertWithCompletion:(void (^)())completion {
+  UIAlertController *alert =
+      [UIAlertController alertControllerWithTitle:[self unableToAccessString]
+                                          message:[self deviceNotCompliantString]
+                                   preferredStyle:UIAlertControllerStyleAlert];
+  [alert addAction:[UIAlertAction actionWithTitle:[self okayString]
+                                            style:UIAlertActionStyleDefault
+                                          handler:^(UIAlertAction *action) {
+    completion();
+  }]];
+  return alert;
+};
+
+// Returns an alert controller for passcode required error.
+- (UIAlertController *)passcodeRequiredAlertWithCompletion:(void (^)())completion {
+  UIAlertController *alert =
+      [UIAlertController alertControllerWithTitle:[self unableToAccessString]
+                                          message:[self passcodeRequiredString]
+                                   preferredStyle:UIAlertControllerStyleAlert];
+  BOOL canOpenSettings = YES;
+  if ([[UIDevice currentDevice].systemVersion hasPrefix:@"10."]) {
+     // In iOS 10, `UIApplicationOpenSettingsURLString` fails to open the Settings app if the
+     // opening app does not have Setting bundle.
+    NSString* mainBundlePath = [[NSBundle mainBundle] resourcePath];
+    NSString* settingsBundlePath = [mainBundlePath
+        stringByAppendingPathComponent:@"Settings.bundle"];
+    if (![NSBundle bundleWithPath:settingsBundlePath]) {
+      canOpenSettings = NO;
+    }
+  }
+  if (canOpenSettings) {
+    [alert addAction:[UIAlertAction actionWithTitle:[self cancelString]
+                                              style:UIAlertActionStyleCancel
+                                            handler:^(UIAlertAction *action) {
+      completion();
+    }]];
+    [alert addAction:[UIAlertAction actionWithTitle:[self settingsString]
+                                              style:UIAlertActionStyleDefault
+                                            handler:^(UIAlertAction *action) {
+      completion();
+      [[UIApplication sharedApplication]
+          openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
+    }]];
+  } else {
+    [alert addAction:[UIAlertAction actionWithTitle:[self okayString]
+                                              style:UIAlertActionStyleCancel
+                                            handler:^(UIAlertAction *action) {
+      completion();
+    }]];
+  }
+  return alert;
+};
+
+// Returns an alert controller for app verification required error.
+- (UIAlertController *)appVerificationRequiredAlertWithURL:(nullable NSURL *)url
+                                                completion:(void (^)())completion {
+  UIAlertController *alert;
+  if (url) {
+    // If the URL is provided, prompt user to open this URL or cancel.
+    alert = [UIAlertController alertControllerWithTitle:[self appVerificationTitleString]
+                                                message:[self appVerificationTextString]
+                                     preferredStyle:UIAlertControllerStyleAlert];
+    [alert addAction:[UIAlertAction actionWithTitle:[self cancelString]
+                                              style:UIAlertActionStyleCancel
+                                            handler:^(UIAlertAction *action) {
+      completion();
+    }]];
+    [alert addAction:[UIAlertAction actionWithTitle:[self appVerificationActionString]
+                                              style:UIAlertActionStyleDefault
+                                            handler:^(UIAlertAction *action) {
+      completion();
+      [[UIApplication sharedApplication] openURL:url];
+    }]];
+  } else {
+    // If the URL is not provided, simple let user acknowledge the issue. This is not supposed to
+    // happen but just to fail gracefully.
+    alert = [UIAlertController alertControllerWithTitle:[self unableToAccessString]
+                                                message:[self appVerificationTextString]
+                                         preferredStyle:UIAlertControllerStyleAlert];
+    [alert addAction:[UIAlertAction actionWithTitle:[self okayString]
+                                              style:UIAlertActionStyleDefault
+                                            handler:^(UIAlertAction *action) {
+      completion();
+    }]];
+  }
+  return alert;
+}
+
+#pragma mark - Localization
+
+// The English version of the strings are used as back-up in case the bundle resource is missing
+// from the third-party app. Please keep them in sync with the strings in the bundle.
+
+// Returns a localized string for unable to access the account.
+- (NSString *)unableToAccessString {
+  return [GIDSignInStrings localizedStringForKey:@"EmmErrorTitle"
+                                            text:@"Unable to sign in to account"];
+}
+
+// Returns a localized string for device passcode required error.
+- (NSString *)passcodeRequiredString {
+  NSString *defaultText =
+      @"Your administrator requires you to set a passcode on this device to access this account. "
+      "Please set a passcode and try again.";
+  return [GIDSignInStrings localizedStringForKey:@"EmmPasscodeRequired" text:defaultText];
+}
+
+// Returns a localized string for app verification error dialog title.
+- (NSString *)appVerificationTitleString {
+  return [GIDSignInStrings localizedStringForKey:@"EmmConnectTitle"
+                                            text:@"Connect with Device Policy App?"];
+}
+
+// Returns a localized string for app verification error dialog message.
+- (NSString *)appVerificationTextString {
+  NSString *defaultText = @"In order to protect your organization's data, "
+      "you must connect with the Device Policy app before logging in.";
+  return [GIDSignInStrings localizedStringForKey:@"EmmConnectText" text:defaultText];
+}
+
+// Returns a localized string for app verification error dialog action button label.
+- (NSString *)appVerificationActionString {
+  return [GIDSignInStrings localizedStringForKey:@"EmmConnectLabel" text:@"Connect"];
+}
+
+// Returns a localized string for general device non-compliance error.
+- (NSString *)deviceNotCompliantString {
+  NSString *defaultText =
+      @"The device is not compliant with the security policy set by your administrator.";
+  return [GIDSignInStrings localizedStringForKey:@"EmmGeneralError" text:defaultText];
+}
+
+// Returns a localized string for "Settings".
+- (NSString *)settingsString {
+  return [GIDSignInStrings localizedStringForKey:@"SettingsAppName" text:@"Settings"];
+}
+
+// Returns a localized string for "OK".
+- (NSString *)okayString {
+  return [GIDSignInStrings localizedStringForKey:@"OK" text:@"OK"];
+}
+
+// Returns a localized string for "Cancel".
+- (NSString *)cancelString {
+  return [GIDSignInStrings localizedStringForKey:@"Cancel" text:@"Cancel"];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 111 - 0
GoogleSignIn/Sources/GIDGoogleUser.m

@@ -0,0 +1,111 @@
+// Copyright 2021 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/GIDGoogleUser_Private.h"
+
+#import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
+#import "GoogleSignIn/Sources/GIDProfileData_Private.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+#else
+#import <AppAuth/AppAuth.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+// The ID Token claim key for the hosted domain value.
+static NSString *const kHostedDomainIDTokenClaimKey = @"hd";
+
+// Key constants used for encode and decode.
+static NSString *const kAuthenticationKey = @"authentication";
+static NSString *const kGrantedScopesKey = @"grantedScopes";
+static NSString *const kUserIDKey = @"userID";
+static NSString *const kServerAuthCodeKey = @"serverAuthCode";
+static NSString *const kProfileDataKey = @"profileData";
+static NSString *const kHostedDomainKey = @"hostedDomain";
+
+@implementation GIDGoogleUser
+
+- (instancetype)initWithAuthState:(OIDAuthState *)authState
+                      profileData:(GIDProfileData *)profileData {
+  self = [super init];
+  if (self) {
+    _authentication = [[GIDAuthentication alloc] initWithAuthState:authState];
+
+    NSArray *grantedScopes;
+    NSString *grantedScopeString = authState.lastTokenResponse.scope;
+    if (grantedScopeString) {
+      // If we have a 'scope' parameter from the backend, this is authoritative.
+      // Remove leading and trailing whitespace.
+      grantedScopeString = [grantedScopeString stringByTrimmingCharactersInSet:
+          [NSCharacterSet whitespaceCharacterSet]];
+      // Tokenize with space as a delimiter.
+      NSMutableArray *parsedScopes = [[grantedScopeString componentsSeparatedByString:@" "]
+          mutableCopy];
+      // Remove empty strings.
+      [parsedScopes removeObject:@""];
+      grantedScopes = [parsedScopes copy];
+    }
+    _grantedScopes = grantedScopes;
+
+    _serverAuthCode = [authState.lastTokenResponse.additionalParameters[@"server_code"] copy];
+    _profile = [profileData copy];
+
+    NSString *idToken = authState.lastTokenResponse.idToken;
+    if (idToken) {
+      OIDIDToken *idTokenDecoded = [[OIDIDToken alloc] initWithIDTokenString:idToken];
+      if (idTokenDecoded.subject) {
+        _userID = [idTokenDecoded.subject copy];
+      }
+      if (idTokenDecoded.claims[kHostedDomainIDTokenClaimKey]) {
+        _hostedDomain = [idTokenDecoded.claims[kHostedDomainIDTokenClaimKey] copy];
+      }
+    }
+  }
+  return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)decoder {
+  self = [super init];
+  if (self) {
+    _authentication = [decoder decodeObjectOfClass:[GIDAuthentication class]
+                                            forKey:kAuthenticationKey];
+    _grantedScopes = [decoder decodeObjectOfClass:[NSArray class] forKey:kGrantedScopesKey];
+    _userID = [decoder decodeObjectOfClass:[NSString class] forKey:kUserIDKey];
+    _serverAuthCode = [decoder decodeObjectOfClass:[NSString class] forKey:kServerAuthCodeKey];
+    _profile = [decoder decodeObjectOfClass:[GIDProfileData class] forKey:kProfileDataKey];
+    _hostedDomain = [decoder decodeObjectOfClass:[NSString class] forKey:kHostedDomainKey];
+  }
+  return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)encoder {
+  [encoder encodeObject:_authentication forKey:kAuthenticationKey];
+  [encoder encodeObject:_grantedScopes forKey:kGrantedScopesKey];
+  [encoder encodeObject:_userID forKey:kUserIDKey];
+  [encoder encodeObject:_serverAuthCode forKey:kServerAuthCodeKey];
+  [encoder encodeObject:_profile forKey:kProfileDataKey];
+  [encoder encodeObject:_hostedDomain forKey:kHostedDomainKey];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 32 - 0
GoogleSignIn/Sources/GIDGoogleUser_Private.h

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 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/Public/GoogleSignIn/GIDGoogleUser.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class OIDAuthState;
+
+// Internal methods for the class that are not part of the public API.
+@interface GIDGoogleUser ()
+
+// Create a object with an auth state, scopes, and profile data.
+- (instancetype)initWithAuthState:(OIDAuthState *)authState
+                      profileData:(GIDProfileData *)profileData;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 41 - 0
GoogleSignIn/Sources/GIDMDMPasscodeCache.h

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 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>
+
+@class GIDMDMPasscodeState;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The helper class to cache the passcode info and to actually detect the passcode state when cache
+ * expires.
+ */
+@interface GIDMDMPasscodeCache : NSObject
+
+/**
+ * Returns a shared instance of the cache.
+ */
++ (instancetype)sharedInstance;
+
+/**
+ * Retrieves the current passcode state.
+ */
+- (GIDMDMPasscodeState *)passcodeState;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 296 - 0
GoogleSignIn/Sources/GIDMDMPasscodeCache.m

@@ -0,0 +1,296 @@
+// Copyright 2021 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/GIDMDMPasscodeCache.h"
+
+#import <Foundation/Foundation.h>
+#import <LocalAuthentication/LocalAuthentication.h>
+#import <Security/Security.h>
+#import <UIKit/UIKit.h>
+
+#import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
+#import "GoogleSignIn/Sources/GIDMDMPasscodeState_Private.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** The JSON key for passcode info obtained by LocalAuthentication API. */
+static NSString *const kLocalAuthenticationKey = @"LocalAuthentication";
+
+/** The JSON key for passcode info obtained by Keychain API. */
+static NSString *const kKeychainKey = @"Keychain";
+
+/** The JSON key for API result. */
+static NSString *const kResultKey = @"result";
+
+/** The JSON key for error domain. */
+static NSString *const kErrorDomainKey = @"error_domain";
+
+/** The JSON key for error code. */
+static NSString *const kErrorCodeKey = @"error_code";
+
+/** Service name for the keychain item used to probe passcode state. */
+static NSString * const kPasscodeStatusService = @"com.google.MDM.PasscodeKeychainService";
+
+/** Account name for the keychain item used to probe passcode state. */
+static NSString * const kPasscodeStatusAccount = @"com.google.MDM.PasscodeKeychainAccount";
+
+/** The time for passcode state retrieved by Keychain API to be cached. */
+static const NSTimeInterval kKeychainInfoCacheTime = 5;
+
+/** The time to wait (in nanaoseconds) on obtaining keychain info. */
+static const int64_t kObtainKeychainInfoWaitTime = 3 * NSEC_PER_SEC;
+
+@implementation GIDMDMPasscodeCache {
+  /** Whether or not LocalAuthentication API is available. */
+  BOOL _hasLocalAuthentication;
+
+  /** The passcode information obtained by LocalAuthentication API. */
+  NSDictionary<NSString *, NSObject *> *_localAuthenticationInfo;
+
+  /** Whether the app has entered background since _localAuthenticationInfo was obtained. */
+  BOOL _hasEnteredBackground;
+
+  /** Whether or not Keychain API is available. */
+  BOOL _hasKeychain;
+
+  /** The passcode information obtained by LocalAuthentication API. */
+  NSDictionary<NSString *, NSObject *> *_keychainInfo;
+
+  /** The timestamp for _keychainInfo to expire. */
+  NSDate *_keychainExpireTime;
+
+  /** The cached passcode state. */
+  GIDMDMPasscodeState *_cachedState;
+}
+
+- (instancetype)init {
+  self = [super init];
+  if (self) {
+    _hasLocalAuthentication = [self hasLocalAuthentication];
+    _hasKeychain = [self hasKeychain];
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                            selector:@selector(applicationDidEnterBackground:)
+                                                name:UIApplicationDidEnterBackgroundNotification
+                                              object:nil];
+  }
+  return self;
+}
+
+- (void)dealloc {
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
++ (instancetype)sharedInstance {
+  static GIDMDMPasscodeCache *sharedInstance;
+  static dispatch_once_t onceToken;
+  dispatch_once(&onceToken, ^{
+    sharedInstance = [[GIDMDMPasscodeCache alloc] init];
+  });
+  return sharedInstance;
+}
+
+- (GIDMDMPasscodeState *)passcodeState {
+  // If the method is called by multiple threads at the same time, they need to execute sequentially
+  // to maintain internal data integrity.
+  @synchronized(self) {
+    BOOL refreshLocalAuthentication = _hasLocalAuthentication &&
+        (_localAuthenticationInfo == nil || _hasEnteredBackground);
+    BOOL refreshKeychain = _hasKeychain &&
+        (_keychainInfo == nil || [_keychainExpireTime timeIntervalSinceNow] < 0);
+
+    if (!refreshLocalAuthentication && !refreshKeychain && _cachedState) {
+      return _cachedState;
+    }
+
+    static dispatch_queue_t workQueue;
+    static dispatch_semaphore_t semaphore;
+    if (!workQueue) {
+      workQueue = dispatch_queue_create("com.google.MDM.PasscodeWorkQueue", DISPATCH_QUEUE_SERIAL);
+      semaphore = dispatch_semaphore_create(0);
+    }
+    if (refreshKeychain) {
+      _keychainInfo = nil;
+      dispatch_async(workQueue, ^() {
+        [self obtainKeychainInfo];
+        dispatch_semaphore_signal(semaphore);
+      });
+    }
+
+    if (refreshLocalAuthentication) {
+      [self obtainLocalAuthenticationInfo];
+    }
+
+    if (refreshKeychain) {
+      dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, kObtainKeychainInfoWaitTime);
+      dispatch_semaphore_wait(semaphore, timeout);
+    }
+    _cachedState = [[GIDMDMPasscodeState alloc] initWithStatus:[self status] info:[self info]];
+    return _cachedState;
+  }
+}
+
+#pragma mark - Private Methods
+
+/**
+ * Detects whether LocalAuthentication API is available for passscode detection purpose.
+ */
+- (BOOL)hasLocalAuthentication {
+  // While the LocalAuthentication framework itself is available at iOS 8+, the particular constant
+  // we need, kLAPolicyDeviceOwnerAuthentication, is only available at iOS 9+. Since the constant
+  // is defined as a macro, there is no good way to detect its availability at runtime, so we can
+  // only check OS version here.
+  NSProcessInfo *processInfo = [NSProcessInfo processInfo];
+  return [processInfo respondsToSelector:@selector(isOperatingSystemAtLeastVersion:)] &&
+      [processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 9}];
+}
+
+/**
+ * Detects whether Keychain API is available for passscode detection purpose.
+ */
+- (BOOL)hasKeychain {
+  // While the Keychain Source is available at iOS 4+, the particular constant we need,
+  // kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, is only available at iOS 8+.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wtautological-pointer-compare"
+  return &kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly != NULL;
+#pragma clang diagnostic pop
+}
+
+/**
+ * Handles the notification for the application entering background.
+ */
+- (void)applicationDidEnterBackground:(NSNotification *)notification {
+  _hasEnteredBackground = YES;
+}
+
+/**
+ * Obtains device passcode presence info with LocalAuthentication APIs.
+ */
+- (void)obtainLocalAuthenticationInfo {
+#if DEBUG
+  NSLog(@"Calling LocalAuthentication API for device passcode state...");
+#endif
+  _hasEnteredBackground = NO;
+  static LAContext *context;
+  @try {
+    if (!context) {
+      context = [[LAContext alloc] init];
+    }
+  } @catch (NSException *) {
+    // In theory there should be no exceptions but in practice there may be: b/23200390, b/23218643.
+    return;
+  }
+  int result;
+  NSError *error;
+  result = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&error] ? 1 : 0;
+  if (error) {
+    _localAuthenticationInfo = @{
+      kResultKey : @(result),
+      kErrorDomainKey : error.domain,
+      kErrorCodeKey : @(error.code),
+    };
+  } else {
+    _localAuthenticationInfo = @{
+      kResultKey : @(result),
+    };
+  }
+}
+
+/**
+ * Obtains device passcode presence info with Keychain APIs.
+ */
+- (void)obtainKeychainInfo {
+#if DEBUG
+  NSLog(@"Calling Keychain API for device passcode state...");
+#endif
+  _keychainExpireTime = [NSDate dateWithTimeIntervalSinceNow:kKeychainInfoCacheTime];
+  static NSDictionary *attributes;
+  static NSDictionary *query;
+  if (!attributes) {
+    NSData *secret = [@"Has passcode set?" dataUsingEncoding:NSUTF8StringEncoding];
+    attributes = @{
+      (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
+      (__bridge id)kSecAttrService : kPasscodeStatusService,
+      (__bridge id)kSecAttrAccount : kPasscodeStatusAccount,
+      (__bridge id)kSecValueData : secret,
+      (__bridge id)kSecAttrAccessible :
+          (__bridge id)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
+    };
+    query = @{
+      (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
+      (__bridge id)kSecAttrService: kPasscodeStatusService,
+      (__bridge id)kSecAttrAccount: kPasscodeStatusAccount
+    };
+  }
+  OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
+  if (status == errSecDuplicateItem) {
+    // If for some reason the item already exists, delete the item and try again.
+    SecItemDelete((__bridge CFDictionaryRef)query);
+    status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
+  };
+  if (status == errSecSuccess) {
+    SecItemDelete((__bridge CFDictionaryRef)query);
+  }
+  _keychainInfo = @{
+    kResultKey : @(status)
+  };
+}
+
+/**
+ * Computes the status string from the current data.
+ */
+- (NSString *)status {
+  // Prefer LocalAuthentication info if available.
+  if (_localAuthenticationInfo != nil) {
+    return ((NSNumber *)_localAuthenticationInfo[kResultKey]).boolValue ? @"YES" : @"NO";
+  }
+  if (_keychainInfo != nil){
+    switch ([(NSNumber *)_keychainInfo[kResultKey] intValue]) {
+      case errSecSuccess:
+        return @"YES";
+      case errSecDecode:  // iOS 8.0+
+      case errSecAuthFailed:  // iOS 9.1+
+      case errSecNotAvailable:  // iOS 11.0+
+        return @"NO";
+      default:
+        break;
+    }
+  }
+  return @"UNCHECKED";
+}
+
+/**
+ * Computes the encoded detailed information string from the current data.
+ */
+- (NSString *)info {
+  NSMutableDictionary<NSString *, NSDictionary<NSString *, NSObject *> *> *infoDict =
+      [NSMutableDictionary dictionaryWithCapacity:2];
+  if (_localAuthenticationInfo) {
+    infoDict[kLocalAuthenticationKey] = _localAuthenticationInfo;
+  }
+  if (_keychainInfo) {
+    infoDict[kKeychainKey] = _keychainInfo;
+  }
+  NSData *data = [NSJSONSerialization dataWithJSONObject:infoDict
+                                                 options:0
+                                                   error:NULL];
+  NSString *string = [data base64EncodedStringWithOptions:0];
+  string = [string stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
+  string = [string stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
+  return string ?: @"e30=";  // Use encoded "{}" in case of error.
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 50 - 0
GoogleSignIn/Sources/GIDMDMPasscodeState.h

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An object to obtain and describe the device passcode state.
+ */
+@interface GIDMDMPasscodeState : NSObject
+
+/**
+ * The device passcode status.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *status;
+
+/**
+ * The detailed device passcode information encoded as a string.
+ * See go/robust-ios-mdmlite for its format.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *info;
+
+/**
+ * This class should not be initialized from other code.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Creates a new instance for the class that represents the current passcode state.
+ */
++ (instancetype)passcodeState;
+
+@end
+
+NS_ASSUME_NONNULL_END
+

+ 50 - 0
GoogleSignIn/Sources/GIDMDMPasscodeState.m

@@ -0,0 +1,50 @@
+// Copyright 2021 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/GIDMDMPasscodeState.h"
+
+#import "GoogleSignIn/Sources/GIDMDMPasscodeState_Private.h"
+
+#import <Foundation/Foundation.h>
+
+#import "GoogleSignIn/Sources/GIDMDMPasscodeCache.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation GIDMDMPasscodeState
+
+- (instancetype)initWithStatus:(NSString *)status info:(NSString *)info {
+  self = [super init];
+  if (self) {
+    _status = [status copy];
+    _info = [info copy];
+  }
+  return self;
+}
+
++ (instancetype)passcodeState {
+#if DEBUG
+  NSDate *start = [NSDate date];
+#endif
+  GIDMDMPasscodeState *passcodeState = [[GIDMDMPasscodeCache sharedInstance] passcodeState];
+#if DEBUG
+  NSTimeInterval timeElapsed = [[NSDate date] timeIntervalSinceDate:start];
+  NSLog(@"Retrieved device passcode state in %dms.", (int)round(timeElapsed * 1000));
+#endif
+  return passcodeState;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 35 - 0
GoogleSignIn/Sources/GIDMDMPasscodeState_Private.h

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 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 "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Internal interface for the GIDMDMPasscodeState class.
+ */
+@interface GIDMDMPasscodeState ()
+
+/**
+ * Initializes the instance with the data.
+ */
+- (instancetype)initWithStatus:(NSString *)status info:(NSString *)info;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 129 - 0
GoogleSignIn/Sources/GIDProfileData.m

@@ -0,0 +1,129 @@
+// Copyright 2021 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/Public/GoogleSignIn/GIDProfileData.h"
+
+#import "GoogleSignIn/Sources/GIDProfileData_Private.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Key constants used for encode and decode.
+static NSString *const kEmailKey = @"email";
+static NSString *const kNameKey = @"name";
+static NSString *const kGivenNameKey = @"given_name";
+static NSString *const kFamilyNameKey = @"family_name";
+static NSString *const kImageURLKey = @"image_url";
+static NSString *const kOldImageURLStringKey = @"picture";
+
+@implementation GIDProfileData {
+  NSURL *_imageURL;
+}
+
+- (instancetype)initWithEmail:(NSString *)email
+                         name:(NSString *)name
+                    givenName:(NSString *)givenName
+                   familyName:(NSString *)familyName
+                     imageURL:(NSURL *)imageURL {
+  self = [super init];
+  if (self) {
+    _email = [email copy];
+    _name = [name copy];
+    _givenName = [givenName copy];
+    _familyName = [familyName copy];
+    _imageURL = [imageURL copy];
+  }
+  return self;
+}
+
+- (BOOL)hasImage {
+  return _imageURL != nil;
+}
+
+- (NSURL *)imageURLWithDimension:(NSUInteger)dimension {
+  if (!_imageURL) {
+    return nil;
+  }
+  if ([self isFIFEAvatarURL:_imageURL]) {
+    return [NSURL URLWithString:
+        [NSString stringWithFormat:@"%@=s%lu", _imageURL, (unsigned long)dimension]];
+  } else {
+    return [NSURL URLWithString:
+        [NSString stringWithFormat:@"%@?sz=%lu", _imageURL, (unsigned long)dimension]];
+  }
+}
+
+- (BOOL)isFIFEAvatarURL:(NSURL *)url {
+  static NSString *const AvatarURLPattern =
+      @"lh[3-6](-tt|-d[a-g,z]|-testonly)?\\.(google|googleusercontent)\\.[a-z]+\\/(a|a-)\\/";
+  NSError *error = NULL;
+  NSRegularExpression *regex =
+      [NSRegularExpression regularExpressionWithPattern:AvatarURLPattern
+                                                options:0
+                                                  error:&error];
+  if (!regex) {
+    return NO;
+  }
+
+  NSUInteger matches = [regex numberOfMatchesInString:url.absoluteString
+                                              options:0
+                                                range:NSMakeRange(0, url.absoluteString.length)];
+
+  if (matches) {
+    return YES;
+  }
+  return NO;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)decoder {
+  self = [super init];
+  if (self) {
+    _email = [decoder decodeObjectOfClass:[NSString class] forKey:kEmailKey];
+    _name = [decoder decodeObjectOfClass:[NSString class] forKey:kNameKey];
+    _givenName = [decoder decodeObjectOfClass:[NSString class] forKey:kGivenNameKey];
+    _familyName = [decoder decodeObjectOfClass:[NSString class] forKey:kFamilyNameKey];
+    _imageURL = [decoder decodeObjectOfClass:[NSURL class] forKey:kImageURLKey];
+
+    // Check to see if this is an old archive, if so, try decoding the old image URL string key.
+    if ([decoder containsValueForKey:kOldImageURLStringKey]) {
+      _imageURL = [NSURL URLWithString:[decoder decodeObjectOfClass:[NSString class]
+                                                             forKey:kOldImageURLStringKey]];
+    }
+  }
+  return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)encoder {
+  [encoder encodeObject:_email forKey:kEmailKey];
+  [encoder encodeObject:_name forKey:kNameKey];
+  [encoder encodeObject:_givenName forKey:kGivenNameKey];
+  [encoder encodeObject:_familyName forKey:kFamilyNameKey];
+  [encoder encodeObject:_imageURL forKey:kImageURLKey];
+}
+
+#pragma mark - NSCopying
+
+- (instancetype)copyWithZone:(nullable NSZone *)zone {
+  // Instances of this class are immutable so we'll return self per NSCopying docs guidance.
+  return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 33 - 0
GoogleSignIn/Sources/GIDProfileData_Private.h

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 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/Public/GoogleSignIn/GIDProfileData.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Private |GIDProfileData| methods that are used in this SDK.
+@interface GIDProfileData ()
+
+// Initialize with profile attributes.
+- (instancetype)initWithEmail:(NSString *)email
+                         name:(NSString *)name
+                    givenName:(NSString *)givenName
+                   familyName:(NSString *)familyName
+                     imageURL:(NSURL *)imageURL;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 31 - 0
GoogleSignIn/Sources/GIDScopes.h

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// The utility class to provide scope constants and check their existence.  Note that most methods
+// only work with limited client-side knowledge, a "scopesWith*" method could add the scope
+// unnecessarily.
+@interface GIDScopes : NSObject
+
+// Adds "email" and "profile" scopes to |scopes| if they are not already contained or implied.
++ (NSArray *)scopesWithBasicProfile:(NSArray *)scopes;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 65 - 0
GoogleSignIn/Sources/GIDScopes.m

@@ -0,0 +1,65 @@
+// Copyright 2021 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/GIDScopes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *const kEmailScope = @"email";
+static NSString *const kOldEmailScope = @"https://www.googleapis.com/auth/userinfo.email";
+static NSString *const kProfileScope = @"profile";
+static NSString *const kOldProfileScope = @"https://www.googleapis.com/auth/userinfo.profile";
+
+static BOOL hasProfile(NSString *scope) {
+  return [scope isEqualToString:kProfileScope] || [scope isEqualToString:kOldProfileScope];
+}
+
+static BOOL hasEmail(NSString *scope) {
+  return [scope isEqualToString:kEmailScope] || [scope isEqualToString:kOldEmailScope];
+}
+
+// Checks whether |scopes| contains or implies a particular scope, using
+// |hasScope| as the predicate.
+static BOOL hasScopeInArray(NSArray *scopes, BOOL (*hasScope)(NSString *)) {
+  for (NSString *scope in scopes) {
+    if (hasScope(scope)) {
+      return YES;
+    }
+  }
+  return NO;
+}
+
+// Adds |scopeToAdd| to |originalScopes| if it is not already contained
+// or implied, using |hasScope| as the predicate.
+static NSArray *addScopeTo(NSArray *originalScopes,
+                           BOOL (*hasScope)(NSString *),
+                           NSString *scopeToAdd) {
+  if (hasScopeInArray(originalScopes, hasScope)) {
+    return originalScopes;
+  }
+  NSMutableArray *result = [NSMutableArray arrayWithArray:originalScopes];
+  [result addObject:scopeToAdd];
+  return result;
+}
+
+@implementation GIDScopes
+
++ (NSArray *)scopesWithBasicProfile:(NSArray *)scopes {
+  scopes = addScopeTo(scopes, hasEmail, kEmailScope);
+  return addScopeTo(scopes, hasProfile, kProfileScope);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 789 - 0
GoogleSignIn/Sources/GIDSignIn.m

@@ -0,0 +1,789 @@
+// Copyright 2021 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/Public/GoogleSignIn/GIDSignIn.h"
+
+#import "GoogleSignIn/Sources/GIDSignIn_Private.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h"
+
+#import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
+#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
+#import "GoogleSignIn/Sources/GIDCallbackQueue.h"
+#import "GoogleSignIn/Sources/GIDScopes.h"
+#import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
+#import "GoogleSignIn/Sources/GIDAuthStateMigration.h"
+#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+
+#import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
+#import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
+#import "GoogleSignIn/Sources/GIDProfileData_Private.h"
+
+#ifdef SWIFT_PACKAGE
+@import AppAuth;
+@import GTMAppAuth;
+@import GTMSessionFetcherCore;
+#else
+#import <AppAuth/OIDAuthState.h>
+#import <AppAuth/OIDAuthorizationRequest.h>
+#import <AppAuth/OIDAuthorizationResponse.h>
+#import <AppAuth/OIDAuthorizationService.h>
+#import <AppAuth/OIDError.h>
+#import <AppAuth/OIDExternalUserAgentSession.h>
+#import <AppAuth/OIDIDToken.h>
+#import <AppAuth/OIDResponseTypes.h>
+#import <AppAuth/OIDServiceConfiguration.h>
+#import <AppAuth/OIDTokenRequest.h>
+#import <AppAuth/OIDTokenResponse.h>
+#import <AppAuth/OIDURLQueryComponent.h>
+#import <AppAuth/OIDAuthorizationService+IOS.h>
+#import <GTMAppAuth/GTMAppAuthFetcherAuthorization+Keychain.h>
+#import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+// The name of the query parameter used for logging the restart of auth from EMM callback.
+static NSString *const kEMMRestartAuthParameter = @"emmres";
+
+// The URL template for the authorization endpoint.
+static NSString *const kAuthorizationURLTemplate = @"https://%@/o/oauth2/v2/auth";
+
+// The URL template for the token endpoint.
+static NSString *const kTokenURLTemplate = @"https://%@/token";
+
+// The URL template for the URL to get user info.
+static NSString *const kUserInfoURLTemplate = @"https://%@/oauth2/v3/userinfo?access_token=%@";
+
+// The URL template for the URL to revoke the token.
+static NSString *const kRevokeTokenURLTemplate = @"https://%@/o/oauth2/revoke?token=%@";
+
+// Expected path in the URL scheme to be handled.
+static NSString *const kBrowserCallbackPath = @"/oauth2callback";
+
+// Expected path for EMM callback.
+static NSString *const kEMMCallbackPath = @"/emmcallback";
+
+// The EMM support version
+static NSString *const kEMMVersion = @"1";
+
+// The error code for Google Identity.
+NSString *const kGIDSignInErrorDomain = @"com.google.GIDSignIn";
+
+// Keychain constants for saving state in the authentication flow.
+static NSString *const kGTMAppAuthKeychainName = @"auth";
+
+// Basic profile (Fat ID Token / userinfo endpoint) keys
+static NSString *const kBasicProfileEmailKey = @"email";
+static NSString *const kBasicProfilePictureKey = @"picture";
+static NSString *const kBasicProfileNameKey = @"name";
+static NSString *const kBasicProfileGivenNameKey = @"given_name";
+static NSString *const kBasicProfileFamilyNameKey = @"family_name";
+
+// Parameters in the callback URL coming back from browser.
+static NSString *const kAuthorizationCodeKeyName = @"code";
+static NSString *const kOAuth2ErrorKeyName = @"error";
+static NSString *const kOAuth2AccessDenied = @"access_denied";
+static NSString *const kEMMPasscodeInfoRequiredKeyName = @"emm_passcode_info_required";
+
+// Error string for unavailable keychain.
+static NSString *const kKeychainError = @"keychain error";
+
+// Error string for user cancelations.
+static NSString *const kUserCanceledError = @"The user canceled the sign-in flow.";
+
+// User preference key to detect fresh install of the app.
+static NSString *const kAppHasRunBeforeKey = @"GID_AppHasRunBefore";
+
+// Maximum retry interval in seconds for the fetcher.
+static const NSTimeInterval kFetcherMaxRetryInterval = 15.0;
+
+// The delay before the new sign-in flow can be presented after the existing one is cancelled.
+static const NSTimeInterval kPresentationDelayAfterCancel = 1.0;
+
+// Extra parameters for the token exchange endpoint.
+static NSString *const kAudienceParameter = @"audience";
+// See b/11669751 .
+static NSString *const kOpenIDRealmParameter = @"openid.realm";
+
+// Minimum time to expiration for a restored access token.
+static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
+
+// The callback queue used for authentication flow.
+@interface GIDAuthFlow : GIDCallbackQueue
+
+@property(nonatomic, strong, nullable) OIDAuthState *authState;
+@property(nonatomic, strong, nullable) NSError *error;
+@property(nonatomic, copy, nullable) NSString *emmSupport;
+@property(nonatomic, nullable) GIDProfileData *profileData;
+
+@end
+
+@implementation GIDAuthFlow
+@end
+
+@implementation GIDSignIn {
+  // This value is used when sign-in flows are resumed via the handling of a URL. Its value is
+  // set when a sign-in flow is begun via |signInWithOptions:| when the options passed don't
+  // represent a sign in continuation.
+  GIDSignInInternalOptions *_currentOptions;
+  // Scheme information for this sign-in instance.
+  GIDSignInCallbackSchemes *_schemes;
+  // AppAuth configuration object.
+  OIDServiceConfiguration *_configuration;
+  // AppAuth external user-agent session state.
+  id<OIDExternalUserAgentSession> _currentAuthorizationFlow;
+}
+
+#pragma mark - Public methods
+
++ (GIDSignIn *)sharedInstance {
+  static dispatch_once_t once;
+  static GIDSignIn *sharedInstance;
+  dispatch_once(&once, ^{
+    sharedInstance = [[self alloc] initPrivate];
+  });
+  return sharedInstance;
+}
+
+// Invoked when the app delegate receives a callback at |application:openURL:options:| or
+// |application:openURL:sourceApplication:annotation|.
+- (BOOL)handleURL:(NSURL *)url {
+  // Check if the callback path matches the expected one for a URL from Safari/Chrome/SafariVC.
+  if ([url.path isEqual:kBrowserCallbackPath]) {
+    if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) {
+      _currentAuthorizationFlow = nil;
+      return YES;
+    }
+    return NO;
+  }
+  // Check if the callback path matches the expected one for a URL from Google Device Policy app.
+  if ([url.path isEqual:kEMMCallbackPath]) {
+    return [self handleDevicePolicyAppURL:url];
+  }
+  return NO;
+}
+
+- (BOOL)hasPreviousSignIn {
+  if ([_currentUser.authentication.authState isAuthorized]) {
+    return YES;
+  }
+  OIDAuthState *authState = [self loadAuthState];
+  return [authState isAuthorized];
+}
+
+- (void)restorePreviousSignIn {
+  [self signInWithOptions:[GIDSignInInternalOptions silentOptions]];
+}
+
+// Authenticates the user by first searching the keychain, then attempting to retrieve the refresh
+// token from a Google Sign In app, and finally through the standard OAuth 2.0 web flow.
+- (void)signIn {
+  [self signInWithOptions:[GIDSignInInternalOptions defaultOptions]];
+}
+
+- (void)signOut {
+  [self signOutWithUser:_currentUser];
+}
+
+- (void)disconnect {
+  [self disconnectWithUser:_currentUser];
+}
+
+#pragma mark - Custom getters and setters
+
+- (void)setClientID:(nullable NSString *)clientID {
+  if (![_clientID isEqualToString:clientID]) {
+    [self willChangeValueForKey:NSStringFromSelector(@selector(clientID))];
+    _clientID = [clientID copy];
+    _schemes = [[GIDSignInCallbackSchemes alloc] initWithClientIdentifier:_clientID];
+    [self didChangeValueForKey:NSStringFromSelector(@selector(clientID))];
+  }
+}
+
+- (void)setScopes:(nullable NSArray<NSString *> *)scopes {
+  scopes = [scopes sortedArrayUsingSelector:@selector(compare:)];
+  if (![_scopes isEqualToArray:scopes]) {
+    _scopes = [[NSArray alloc] initWithArray:scopes copyItems:YES];
+  }
+}
+
+- (void)setShouldFetchBasicProfile:(BOOL)shouldFetchBasicProfile {
+  shouldFetchBasicProfile = !!shouldFetchBasicProfile;
+  if (_shouldFetchBasicProfile != shouldFetchBasicProfile) {
+    _shouldFetchBasicProfile = shouldFetchBasicProfile;
+  }
+}
+
+- (void)setHostedDomain:(nullable NSString *)hostedDomain {
+  if (!(_hostedDomain == hostedDomain || [_hostedDomain isEqualToString:hostedDomain])) {
+    _hostedDomain = [hostedDomain copy];
+  }
+}
+
+#pragma mark - Private methods
+
+- (id)initPrivate {
+  self = [super init];
+  if (self) {
+    // Default scope settings.
+    _scopes = @[];
+    _shouldFetchBasicProfile = YES;
+
+    // Check to see if the 3P app is being run for the first time after a fresh install.
+    BOOL isFreshInstall = [self isFreshInstall];
+
+    // If this is a fresh install, ensure that any pre-existing keychain data is purged.
+    if (isFreshInstall) {
+      [self removeAllKeychainEntries];
+    }
+
+    NSString *authorizationEnpointURL = [NSString stringWithFormat:kAuthorizationURLTemplate,
+        [GIDSignInPreferences googleAuthorizationServer]];
+    NSString *tokenEndpointURL = [NSString stringWithFormat:kTokenURLTemplate,
+        [GIDSignInPreferences googleTokenServer]];
+    _configuration = [[OIDServiceConfiguration alloc]
+        initWithAuthorizationEndpoint:[NSURL URLWithString:authorizationEnpointURL]
+                        tokenEndpoint:[NSURL URLWithString:tokenEndpointURL]];
+
+    // Perform migration of auth state from old versions of the SDK if needed.
+    [GIDAuthStateMigration migrateIfNeededWithTokenURL:_configuration.tokenEndpoint
+                                          callbackPath:kBrowserCallbackPath
+                                          keychainName:kGTMAppAuthKeychainName
+                                        isFreshInstall:isFreshInstall];
+  }
+  return self;
+}
+
+// Does sanity check for parameters and then authenticates if necessary.
+- (void)signInWithOptions:(GIDSignInInternalOptions *)options {
+  // Options for continuation are not the options we want to cache. The purpose of caching the
+  // options in the first place is to provide continuation flows with a starting place from which to
+  // derive suitable options for the continuation!
+  if (!options.continuation) {
+    _currentOptions = options;
+  }
+
+  // Explicitly throw exception for missing client ID (and scopes) here. This must come before
+  // scheme check because schemes rely on reverse client IDs.
+  [self assertValidParameters];
+
+  if (options.interactive) {
+    [self assertValidPresentingViewController];
+  }
+
+  // If the application does not support the required URL schemes tell the developer so.
+  if (options.interactive) {
+    NSArray<NSString *> *unsupportedSchemes = [_schemes unsupportedSchemes];
+    if (unsupportedSchemes.count != 0) {
+      // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+      [NSException raise:NSInvalidArgumentException
+                  format:@"Your app is missing support for the following URL schemes: %@",
+                         [unsupportedSchemes componentsJoinedByString:@", "]];
+    }
+  }
+
+  // If this is a non-interactive flow, use cached authentication if possible.
+  if (!options.interactive && _currentUser.authentication) {
+    [_currentUser.authentication doWithFreshTokens:^(GIDAuthentication *unused, NSError *error) {
+      if (error) {
+        [self authenticateWithOptions:options];
+      } else {
+        [_delegate signIn:self didSignInForUser:_currentUser withError:nil];
+      }
+    }];
+  } else {
+    [self authenticateWithOptions:options];
+  }
+}
+
+# pragma mark - Authentication flow
+
+- (void)authenticateInteractivelyWithOptions:(GIDSignInInternalOptions *)options {
+  NSString *emmSupport = [[self class] isOperatingSystemAtLeast9] ? kEMMVersion : nil;
+  NSMutableDictionary<NSString *, NSString *> *additionalParameters = [@{} mutableCopy];
+  if (_serverClientID) {
+    additionalParameters[kAudienceParameter] = _serverClientID;
+  }
+  if (_loginHint) {
+    additionalParameters[@"login_hint"] = _loginHint;
+  }
+  if (_hostedDomain) {
+    additionalParameters[@"hd"] = _hostedDomain;
+  }
+  [additionalParameters addEntriesFromDictionary:
+      [GIDAuthentication parametersWithParameters:options.extraParams
+                                       emmSupport:emmSupport
+                           isPasscodeInfoRequired:NO]];
+  OIDAuthorizationRequest *request =
+      [[OIDAuthorizationRequest alloc] initWithConfiguration:_configuration
+                                                    clientId:_clientID
+                                                      scopes:[self adjustedScopes]
+                                                 redirectURL:[self redirectURI]
+                                                responseType:OIDResponseTypeCode
+                                        additionalParameters:additionalParameters];
+  _currentAuthorizationFlow = [OIDAuthorizationService
+      presentAuthorizationRequest:request
+         presentingViewController:_presentingViewController
+                         callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse,
+                                    NSError *_Nullable error) {
+    GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] init];
+    authFlow.emmSupport = emmSupport;
+
+    if (authorizationResponse) {
+      if (authorizationResponse.authorizationCode.length) {
+        authFlow.authState = [[OIDAuthState alloc]
+            initWithAuthorizationResponse:authorizationResponse];
+        // perform auth code exchange
+        [self maybeFetchToken:authFlow fallback:nil];
+      } else {
+        // There was a failure, convert to appropriate error code.
+        NSString *errorString;
+        GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown;
+        NSDictionary<NSString *, NSObject *> *params = authorizationResponse.additionalParameters;
+
+        if (authFlow.emmSupport) {
+          [authFlow wait];
+          BOOL isEMMError = [[GIDEMMErrorHandler sharedInstance]
+              handleErrorFromResponse:params
+                           completion:^{
+                             [authFlow next];
+                           }];
+          if (isEMMError) {
+            errorCode = kGIDSignInErrorCodeEMM;
+          }
+        }
+        errorString = (NSString *)params[kOAuth2ErrorKeyName];
+        if ([errorString isEqualToString:kOAuth2AccessDenied]) {
+          errorCode = kGIDSignInErrorCodeCanceled;
+        }
+
+        authFlow.error = [self errorWithString:errorString code:errorCode];
+      }
+    } else {
+      NSString *errorString = [error localizedDescription];
+      GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown;
+      if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow) {
+        // The user has canceled the flow at the iOS modal dialog.
+        errorString = kUserCanceledError;
+        errorCode = kGIDSignInErrorCodeCanceled;
+      }
+      authFlow.error = [self errorWithString:errorString code:errorCode];
+    }
+
+    [self addDecodeIdTokenCallback:authFlow];
+    [self addSaveAuthCallback:authFlow];
+    [self addCallDelegateCallback:authFlow];
+  }];
+}
+
+// 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];
+    return;
+  }
+
+  // Try retrieving an authorization object from the keychain.
+  OIDAuthState *authState = [self loadAuthState];
+
+  if (![authState isAuthorized]) {
+    // No valid auth in keychain, per documentation/spec, notify delegate of failure.
+    NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                         code:kGIDSignInErrorCodeHasNoAuthInKeychain
+                                     userInfo:nil];
+    [_delegate signIn:self didSignInForUser:nil withError:error];
+    return;
+  }
+
+  // Complete the auth flow using saved auth in keychain.
+  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] init];
+  authFlow.authState = authState;
+  [self maybeFetchToken:authFlow fallback:options.interactive ? ^() {
+    [self authenticateInteractivelyWithOptions:options];
+  } : nil];
+  [self addDecodeIdTokenCallback:authFlow];
+  [self addSaveAuthCallback:authFlow];
+  [self addCallDelegateCallback:authFlow];
+}
+
+// Fetches the access token if necessary as part of the auth flow. If |fallback|
+// is provided, call it instead of continuing the auth flow in case of error.
+- (void)maybeFetchToken:(GIDAuthFlow *)authFlow fallback:(nullable void (^)(void))fallback {
+  OIDAuthState *authState = authFlow.authState;
+  // Do nothing if we have an auth flow error or a restored access token that isn't near expiration.
+  if (authFlow.error ||
+      (authState.lastTokenResponse.accessToken &&
+        [authState.lastTokenResponse.accessTokenExpirationDate timeIntervalSinceNow] >
+        kMinimumRestoredAccessTokenTimeToExpire)) {
+    return;
+  }
+  NSMutableDictionary<NSString *, NSString *> *additionalParameters = [@{} mutableCopy];
+  if (_serverClientID) {
+    additionalParameters[kAudienceParameter] = _serverClientID;
+  }
+  if (_openIDRealm) {
+    additionalParameters[kOpenIDRealmParameter] = _openIDRealm;
+  }
+  NSDictionary<NSString *, NSObject *> *params =
+      authState.lastAuthorizationResponse.additionalParameters;
+  NSString *passcodeInfoRequired = (NSString *)params[kEMMPasscodeInfoRequiredKeyName];
+  [additionalParameters addEntriesFromDictionary:
+      [GIDAuthentication parametersWithParameters:@{}
+                                       emmSupport:authFlow.emmSupport
+                           isPasscodeInfoRequired:passcodeInfoRequired.length > 0]];
+  OIDTokenRequest *tokenRequest;
+  if (!authState.lastTokenResponse.accessToken &&
+      authState.lastAuthorizationResponse.authorizationCode) {
+    tokenRequest = [authState.lastAuthorizationResponse
+        tokenExchangeRequestWithAdditionalParameters:additionalParameters];
+  } else {
+    [additionalParameters
+        addEntriesFromDictionary:authState.lastTokenResponse.request.additionalParameters];
+    tokenRequest = [authState tokenRefreshRequestWithAdditionalParameters:additionalParameters];
+  }
+
+  [authFlow wait];
+  [OIDAuthorizationService
+      performTokenRequest:tokenRequest
+                 callback:^(OIDTokenResponse *_Nullable tokenResponse,
+                            NSError *_Nullable error) {
+    [authState updateWithTokenResponse:tokenResponse error:error];
+    authFlow.error = error;
+
+    if (!tokenResponse.accessToken || error) {
+      if (fallback) {
+        [authFlow reset];
+        fallback();
+        return;
+      }
+    }
+
+    if (authFlow.emmSupport) {
+      [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *error) {
+        authFlow.error = error;
+        [authFlow next];
+      }];
+    } else {
+      [authFlow next];
+    }
+  }];
+}
+
+// Adds a callback to the auth flow to save the auth object to |self| and the keychain as well.
+- (void)addSaveAuthCallback:(GIDAuthFlow *)authFlow {
+  __weak GIDAuthFlow *weakAuthFlow = authFlow;
+  [authFlow addCallback:^() {
+    GIDAuthFlow *handlerAuthFlow = weakAuthFlow;
+    OIDAuthState *authState = handlerAuthFlow.authState;
+    if (authState && !handlerAuthFlow.error) {
+      if (![self saveAuthState:authState]) {
+        handlerAuthFlow.error = [self errorWithString:kKeychainError
+                                                 code:kGIDSignInErrorCodeKeychain];
+        return;
+      }
+      [self willChangeValueForKey:NSStringFromSelector(@selector(currentUser))];
+      _currentUser = [[GIDGoogleUser alloc] initWithAuthState:authState
+                                                  profileData:handlerAuthFlow.profileData];
+      [self didChangeValueForKey:NSStringFromSelector(@selector(currentUser))];
+    }
+  }];
+}
+
+// Adds a callback to the auth flow to extract user data from the ID token where available and
+// make a userinfo request if necessary.
+- (void)addDecodeIdTokenCallback:(GIDAuthFlow *)authFlow {
+  __weak GIDAuthFlow *weakAuthFlow = authFlow;
+  [authFlow addCallback:^() {
+    GIDAuthFlow *handlerAuthFlow = weakAuthFlow;
+    OIDAuthState *authState = handlerAuthFlow.authState;
+    if (!authState || handlerAuthFlow.error) {
+      return;
+    }
+    OIDIDToken *idToken =
+        [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken];
+    if (idToken) {
+      if (_shouldFetchBasicProfile) {
+        // If the picture and name fields are present in the ID token, use them, otherwise make
+        // a userinfo request to fetch them.
+        if (idToken.claims[kBasicProfilePictureKey] &&
+            idToken.claims[kBasicProfileNameKey] &&
+            idToken.claims[kBasicProfileGivenNameKey] &&
+            idToken.claims[kBasicProfileFamilyNameKey]) {
+          handlerAuthFlow.profileData = [[GIDProfileData alloc]
+              initWithEmail:idToken.claims[kBasicProfileEmailKey]
+                       name:idToken.claims[kBasicProfileNameKey]
+                  givenName:idToken.claims[kBasicProfileGivenNameKey]
+                 familyName:idToken.claims[kBasicProfileFamilyNameKey]
+                   imageURL:[NSURL URLWithString:idToken.claims[kBasicProfilePictureKey]]];
+        } else {
+          [handlerAuthFlow wait];
+          NSURL *infoURL = [NSURL URLWithString:
+              [NSString stringWithFormat:kUserInfoURLTemplate,
+                  [GIDSignInPreferences googleUserInfoServer],
+                  authState.lastTokenResponse.accessToken]];
+          [self startFetchURL:infoURL
+                      fromAuthState:authState
+                        withComment:@"GIDSignIn: fetch basic profile info"
+              withCompletionHandler:^(NSData *data, NSError *error) {
+            if (data && !error) {
+              NSError *jsonDeserializationError;
+              NSDictionary<NSString *, NSString *> *profileDict =
+                  [NSJSONSerialization JSONObjectWithData:data
+                                                  options:NSJSONReadingMutableContainers
+                                                    error:&jsonDeserializationError];
+              if (profileDict) {
+                handlerAuthFlow.profileData = [[GIDProfileData alloc]
+                    initWithEmail:idToken.claims[kBasicProfileEmailKey]
+                             name:profileDict[kBasicProfileNameKey]
+                        givenName:profileDict[kBasicProfileGivenNameKey]
+                       familyName:profileDict[kBasicProfileFamilyNameKey]
+                         imageURL:[NSURL URLWithString:profileDict[kBasicProfilePictureKey]]];
+              }
+            }
+            if (error) {
+              handlerAuthFlow.error = error;
+            }
+            [handlerAuthFlow next];
+          }];
+        }
+      }
+    }
+  }];
+}
+
+// Adds a callback to the auth flow to call the sign-in delegate.
+- (void)addCallDelegateCallback:(GIDAuthFlow *)authFlow {
+  __weak GIDAuthFlow *weakAuthFlow = authFlow;
+  [authFlow addCallback:^() {
+    GIDAuthFlow *handlerAuthFlow = weakAuthFlow;
+    [_delegate signIn:self didSignInForUser:_currentUser withError:handlerAuthFlow.error];
+  }];
+}
+
+- (void)startFetchURL:(NSURL *)URL
+            fromAuthState:(OIDAuthState *)authState
+              withComment:(NSString *)comment
+    withCompletionHandler:(void (^)(NSData *, NSError *))handler {
+  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
+  GTMSessionFetcher *fetcher;
+  GTMAppAuthFetcherAuthorization *authorization =
+      [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
+  id<GTMSessionFetcherServiceProtocol> fetcherService = authorization.fetcherService;
+  if (fetcherService) {
+    fetcher = [fetcherService fetcherWithRequest:request];
+  } else {
+    fetcher = [GTMSessionFetcher fetcherWithRequest:request];
+  }
+  fetcher.retryEnabled = YES;
+  fetcher.maxRetryInterval = kFetcherMaxRetryInterval;
+  fetcher.comment = comment;
+  [fetcher beginFetchWithCompletionHandler:handler];
+}
+
+- (void)didDisconnectWithUser:(GIDGoogleUser *)user
+                        error:(nullable NSError *)error {
+  if ([_delegate respondsToSelector:@selector(signIn:didDisconnectWithUser:withError:)]) {
+    [_delegate signIn:self didDisconnectWithUser:user withError:error];
+  }
+}
+
+// Parse incoming URL from the Google Device Policy app.
+- (BOOL)handleDevicePolicyAppURL:(NSURL *)url {
+  OIDURLQueryComponent *queryComponent = [[OIDURLQueryComponent alloc] initWithURL:url];
+  NSDictionary<NSString *, NSObject<NSCopying> *> *params = queryComponent.dictionaryValue;
+  NSObject<NSCopying> *actionParam = params[@"action"];
+  NSString *actionString =
+      [actionParam isKindOfClass:[NSString class]] ? (NSString *)actionParam : nil;
+  if (![@"restart_auth" isEqualToString:actionString]) {
+    return NO;
+  }
+  if (!_presentingViewController) {
+    return NO;
+  }
+  if (!_currentAuthorizationFlow) {
+    return NO;
+  }
+  [_currentAuthorizationFlow cancel];
+  _currentAuthorizationFlow = nil;
+  NSDictionary<NSString *, NSString *> *extraParameters = @{ kEMMRestartAuthParameter : @"1" };
+  // In iOS 13 the presentation of ASWebAuthenticationSession needs an anchor window,
+  // so we need to wait until the previous presentation is completely gone to ensure the right
+  // anchor window is used here.
+  dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
+                 (int64_t)(kPresentationDelayAfterCancel * NSEC_PER_SEC)),
+                 dispatch_get_main_queue(), ^{
+    [self signInWithOptions:[_currentOptions optionsWithExtraParameters:extraParameters
+                                                        forContinuation:YES]];
+  });
+  return YES;
+}
+
+#pragma mark - Key-Value Observing
+
+// Override |NSObject(NSKeyValueObservingCustomization)| method in order to provide custom KVO
+// notifications for |clientID| and |currentUser| properties.
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+  if ([key isEqual:NSStringFromSelector(@selector(clientID))] ||
+      [key isEqual:NSStringFromSelector(@selector(currentUser))]) {
+    return NO;
+  }
+  return [super automaticallyNotifiesObserversForKey:key];
+}
+
+#pragma mark - Helpers
+
+- (NSError *)errorWithString:(NSString *)errorString code:(GIDSignInErrorCode)code {
+  if (errorString == nil) {
+    errorString = @"Unknown error";
+  }
+  NSDictionary<NSString *, NSString *> *errorDict = @{ NSLocalizedDescriptionKey : errorString };
+  return [NSError errorWithDomain:kGIDSignInErrorDomain
+                             code:code
+                         userInfo:errorDict];
+}
+
++ (BOOL)isOperatingSystemAtLeast9 {
+  NSProcessInfo *processInfo = [NSProcessInfo processInfo];
+  return [processInfo respondsToSelector:@selector(isOperatingSystemAtLeastVersion:)] &&
+      [processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 9}];
+}
+
+// Asserts the parameters being valid.
+- (void)assertValidParameters {
+  if (![_clientID length]) {
+    // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+    [NSException raise:NSInvalidArgumentException
+                format:@"You must specify |clientID| for |GIDSignIn|"];
+  }
+  if ([self adjustedScopes].count == 0) {
+    // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+    [NSException raise:NSInvalidArgumentException
+                format:@"You must specify |shouldFetchBasicProfile| or |scopes| for |GIDSignIn|"];
+  }
+}
+
+// Assert that the UI Delegate has been set.
+- (void)assertValidPresentingViewController {
+  if (!_presentingViewController) {
+    // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
+    [NSException raise:NSInvalidArgumentException format:@"presentingViewController must be set."];
+  }
+}
+
+// Checks whether or not this is the first time the app runs.
+- (BOOL)isFreshInstall {
+  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
+  if ([defaults boolForKey:kAppHasRunBeforeKey]) {
+    return NO;
+  }
+  [defaults setBool:YES forKey:kAppHasRunBeforeKey];
+  return YES;
+}
+
+- (void)removeAllKeychainEntries {
+  [GTMAppAuthFetcherAuthorization removeAuthorizationFromKeychainForName:kGTMAppAuthKeychainName];
+}
+
+// Clears the saved authentication object and other user information.
+- (void)clearAuthentication {
+  [self willChangeValueForKey:NSStringFromSelector(@selector(currentUser))];
+  _currentUser = nil;
+  [self didChangeValueForKey:NSStringFromSelector(@selector(currentUser))];
+}
+
+// Adds basic profile scopes to |scopes| if |shouldFetchBasicProfile| is set.
+- (NSArray *)adjustedScopes {
+  NSArray<NSString *> *adjustedScopes = _scopes;
+  if (_shouldFetchBasicProfile) {
+    adjustedScopes = [GIDScopes scopesWithBasicProfile:adjustedScopes];
+  }
+  return adjustedScopes;
+}
+
+- (NSURL *)redirectURI {
+  NSString *scheme = [_schemes clientIdentifierScheme];
+  return [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@", scheme, kBrowserCallbackPath]];
+}
+
+- (void)signOutWithUser:(GIDGoogleUser *)user {
+  // TODO(petea): Respond to user parameter.
+  // TODO(petea): Mark user as signed out rather than removing auth from keychain.
+  [self clearAuthentication];
+  [self removeAllKeychainEntries];
+}
+
+- (void)disconnectWithUser:(GIDGoogleUser *)user {
+  OIDAuthState *authState = user.authentication.authState;
+  if (!authState) {
+    // Even the user is not signed in right now, we still need to remove any token saved in the
+    // keychain.
+    authState = [self loadAuthState];
+  }
+  // Either access or refresh token would work, but we won't have access token if the auth is
+  // retrieved from keychain.
+  NSString *token = authState.lastTokenResponse.accessToken;
+  if (!token) {
+    token = authState.lastTokenResponse.refreshToken;
+  }
+  if (!token) {
+    [self removeAllKeychainEntries];
+    // Nothing to do here, consider the operation successful.
+    [self didDisconnectWithUser:user error:nil];
+    return;
+  }
+  NSString *revokeURLString = [NSString stringWithFormat:kRevokeTokenURLTemplate,
+      [GIDSignInPreferences googleAuthorizationServer], token];
+  // Append logging parameter
+  revokeURLString = [NSString stringWithFormat:@"%@&%@=%@",
+                     revokeURLString,
+                     kSDKVersionLoggingParameter,
+                     GIDVersion()];
+  NSURL *revokeURL = [NSURL URLWithString:revokeURLString];
+  [self startFetchURL:revokeURL
+              fromAuthState:authState
+                withComment:@"GIDSignIn: revoke tokens"
+      withCompletionHandler:^(NSData *data, NSError *error) {
+    // Revoking an already revoked token seems always successful, which saves the trouble here for
+    // us.
+    if (error) {
+      [self didDisconnectWithUser:user error:error];
+    } else {
+      [self signOutWithUser:user];
+      [self didDisconnectWithUser:user error:nil];
+    }
+  }];
+}
+
+- (BOOL)saveAuthState:(OIDAuthState *)authState {
+  GTMAppAuthFetcherAuthorization *authorization =
+      [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
+  return [GTMAppAuthFetcherAuthorization saveAuthorization:authorization
+                                         toKeychainForName:kGTMAppAuthKeychainName];
+}
+
+- (OIDAuthState *)loadAuthState {
+  GTMAppAuthFetcherAuthorization *authorization =
+      [GTMAppAuthFetcherAuthorization authorizationFromKeychainForName:kGTMAppAuthKeychainName];
+  return authorization.authState;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 660 - 0
GoogleSignIn/Sources/GIDSignInButton.m

@@ -0,0 +1,660 @@
+// Copyright 2021 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/GIDSignInButton_Private.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+
+#import "GoogleSignIn/Sources/GIDScopes.h"
+#import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
+#import "GoogleSignIn/Sources/GIDSignInStrings.h"
+#import "GoogleSignIn/Sources/GIDSignIn_Private.h"
+#import "GoogleSignIn/Sources/NSBundle+GID3PAdditions.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - Constants
+
+// Standard accessibility identifier.
+static NSString *const kAccessibilityIdentifier = @"GIDSignInButton";
+
+// The name of the font for button text.
+static NSString *const kFontNameRobotoBold = @"Roboto-Bold";
+
+// Button text font size.
+static const CGFloat kFontSize = 14;
+
+#pragma mark - Icon Constants
+
+// The name of the image for the Google "G"
+static NSString *const kGoogleImageName = @"google";
+
+// Keys used for NSCoding.
+static NSString *const kStyleKey = @"style";
+static NSString *const kColorSchemeKey = @"color_scheme";
+static NSString *const kButtonState = @"state";
+
+#pragma mark - Sizing Constants
+
+// The corner radius of the button
+static const int kCornerRadius = 2;
+
+// The standard height of the sign in button.
+static const int kButtonHeight = 48;
+
+// The width of the icon part of the button in points.
+static const int kIconWidth = 40;
+
+// Left and right text padding.
+static const int kTextPadding = 14;
+
+// The icon (UIImage)'s frame.
+static const CGRect kIconFrame = { {9, 10}, {29, 30} };
+
+#pragma mark - Appearance Constants
+
+static const CGFloat kBorderWidth = 4;
+
+static const CGFloat kHaloShadowAlpha = 12.0 / 100.0;
+static const CGFloat kHaloShadowBlur = 2;
+
+static const CGFloat kDropShadowAlpha = 24.0 / 100.0;
+static const CGFloat kDropShadowBlur = 2;
+static const CGFloat kDropShadowYOffset = 2;
+
+static const CGFloat kDisabledIconAlpha = 40.0 / 100.0;
+
+#pragma mark - Misc. Constants
+
+// The query parameter name to log the appearance of the button.
+static NSString *const kLoggingParameter = @"gpbtn";
+
+#pragma mark - Colors
+
+// All colors in hex RGBA format (0xRRGGBBAA)
+
+static const NSUInteger kColorGoogleBlue = 0x4285f4ff;
+static const NSUInteger kColorGoogleDarkBlue = 0x3367d6ff;
+
+static const NSUInteger kColorWhite = 0xffffffff;
+static const NSUInteger kColorLightestGrey = 0x00000014;
+static const NSUInteger kColorLightGrey = 0xeeeeeeff;
+static const NSUInteger kColorDisabledGrey = 0x00000066;
+static const NSUInteger kColorDarkestGrey = 0x00000089;
+
+static NSUInteger kColors[12] = {
+  // |Background|, |Foreground|,
+
+  kColorGoogleBlue, kColorWhite,              // Dark Google Normal
+  kColorLightestGrey, kColorDisabledGrey,     // Dark Google Disabled
+  kColorGoogleDarkBlue, kColorWhite,          // Dark Google Pressed
+
+  kColorWhite, kColorDarkestGrey,             // Light Google Normal
+  kColorLightestGrey, kColorDisabledGrey,     // Light Google Disabled
+  kColorLightGrey, kColorDarkestGrey,         // Light Google Pressed
+
+};
+
+// The state of the button:
+typedef NS_ENUM(NSUInteger, GIDSignInButtonState) {
+  kGIDSignInButtonStateNormal = 0,
+  kGIDSignInButtonStateDisabled = 1,
+  kGIDSignInButtonStatePressed = 2,
+};
+static NSUInteger const kNumGIDSignInButtonStates = 3;
+
+// Used to lookup specific colors from the kColors table:
+typedef NS_ENUM(NSUInteger, GIDSignInButtonStyleColor) {
+  kGIDSignInButtonStyleColorBackground = 0,
+  kGIDSignInButtonStyleColorForeground = 1,
+};
+static NSUInteger const kNumGIDSignInButtonStyleColors = 2;
+
+// This method just pulls the correct value out of the kColors table and returns it as a UIColor.
+static UIColor *colorForStyleState(GIDSignInButtonColorScheme style,
+                                        GIDSignInButtonState state,
+                                        GIDSignInButtonStyleColor color) {
+  NSUInteger stateWidth = kNumGIDSignInButtonStyleColors;
+  NSUInteger styleWidth = kNumGIDSignInButtonStates * stateWidth;
+  NSUInteger index = (style * styleWidth) + (state * stateWidth) + color;
+  NSUInteger colorValue = kColors[index];
+  return [UIColor colorWithRed:(CGFloat)(((colorValue & 0xff000000) >> 24) / 255.0f) \
+                         green:(CGFloat)(((colorValue & 0x00ff0000) >> 16) / 255.0f) \
+                          blue:(CGFloat)(((colorValue & 0x0000ff00) >> 8) / 255.0f) \
+                         alpha:(CGFloat)(((colorValue & 0x000000ff) >> 0) / 255.0f)];
+}
+
+#pragma mark - UIImage Category Forward Declaration
+
+@interface UIImage (GIDAdditions_Private)
+
+- (UIImage *)gid_imageWithBlendMode:(CGBlendMode)blendMode color:(UIColor *)color;
+
+@end
+
+#pragma mark - GIDSignInButton Private Properties
+
+@interface GIDSignInButton ()
+
+// The state (normal, pressed, disabled) of the button.
+@property(nonatomic, assign) GIDSignInButtonState buttonState;
+
+@end
+
+#pragma mark -
+
+@implementation GIDSignInButton {
+  UIImageView *_icon;
+}
+
+#pragma mark - Object lifecycle
+
+- (id)initWithFrame:(CGRect)frame {
+  self = [super initWithFrame:frame];
+  if (self) {
+    [self sharedInit];
+  }
+  return self;
+}
+
+- (void)sharedInit {
+  self.clipsToBounds = YES;
+  self.backgroundColor = [UIColor clearColor];
+
+  // Accessibility settings:
+  self.isAccessibilityElement = YES;
+  self.accessibilityTraits = UIAccessibilityTraitButton;
+  self.accessibilityIdentifier = kAccessibilityIdentifier;
+
+  // Default style settings.
+  _style = kGIDSignInButtonStyleStandard;
+  _colorScheme = kGIDSignInButtonColorSchemeLight;
+  _buttonState = kGIDSignInButtonStateNormal;
+
+  // Icon for branding image:
+  _icon = [[UIImageView alloc] initWithFrame:kIconFrame];
+  _icon.contentMode = UIViewContentModeCenter;
+  _icon.userInteractionEnabled = NO;
+  [self addSubview:_icon];
+
+  // Load font for "Sign in with Google" text
+  [NSBundle gid_registerFonts];
+
+  // Setup normal/highlighted state transitions:
+  [self addTarget:self
+                action:@selector(setNeedsDisplay)
+      forControlEvents:UIControlEventAllTouchEvents];
+  [self addTarget:self
+                action:@selector(switchToPressed)
+      forControlEvents:UIControlEventTouchDown |
+                       UIControlEventTouchDragInside |
+                       UIControlEventTouchDragEnter];
+  [self addTarget:self
+                action:@selector(switchToNormal)
+      forControlEvents:UIControlEventTouchDragExit |
+                       UIControlEventTouchDragOutside |
+                       UIControlEventTouchCancel];
+
+  // Setup convenience touch-up-inside handler:
+  [self addTarget:self action:@selector(pressed) forControlEvents:UIControlEventTouchUpInside];
+
+  // Update the icon, etc.
+  [self updateUI];
+}
+
+#pragma mark - NSCoding
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+  self = [super initWithCoder:aDecoder];
+  if (self) {
+    [self sharedInit];
+    if ([aDecoder containsValueForKey:kStyleKey]) {
+      _style = [aDecoder decodeIntegerForKey:kStyleKey];
+    }
+    if ([aDecoder containsValueForKey:kColorSchemeKey]) {
+      _colorScheme = [aDecoder decodeIntegerForKey:kColorSchemeKey];
+    }
+    if ([aDecoder containsValueForKey:kButtonState]) {
+      _buttonState = [aDecoder decodeIntegerForKey:kButtonState];
+    }
+  }
+  return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+  [super encodeWithCoder:aCoder];
+  [aCoder encodeInteger:_style forKey:kStyleKey];
+  [aCoder encodeInteger:_colorScheme forKey:kColorSchemeKey];
+  [aCoder encodeInteger:_buttonState forKey:kButtonState];
+}
+
+#pragma mark - UI
+
+- (void)updateUI {
+  // Reload the icon.
+  [self loadIcon];
+
+  // Set a useful accessibility label here even if we're not showing text.
+  // Get localized button text from bundle.
+  self.accessibilityLabel = [self buttonText];
+
+  // Force constrain frame sizes:
+  [self setFrame:self.frame];
+
+  [self setNeedsUpdateConstraints];
+  [self setNeedsDisplay];
+}
+
+- (void)loadIcon {
+  NSString *resourceName = kGoogleImageName;
+  NSBundle *gidBundle = [NSBundle gid_frameworkBundle];
+  NSString *resourcePath = [gidBundle pathForResource:resourceName ofType:@"png"];
+  UIImage *image = [UIImage imageWithContentsOfFile:resourcePath];
+
+  if (_buttonState == kGIDSignInButtonStateDisabled) {
+    _icon.image = [image gid_imageWithBlendMode:kCGBlendModeMultiply
+                                          color:[UIColor colorWithWhite:0
+                                                                  alpha:kDisabledIconAlpha]];
+  } else {
+    _icon.image = image;
+  }
+}
+
+#pragma mark - State Transitions
+
+- (void)switchToPressed {
+  [self setButtonState:kGIDSignInButtonStatePressed];
+}
+
+- (void)switchToNormal {
+  [self setButtonState:kGIDSignInButtonStateNormal];
+}
+
+- (void)switchToDisabled {
+  [self setButtonState:kGIDSignInButtonStateDisabled];
+}
+
+#pragma mark - Properties
+
+- (void)setStyle:(GIDSignInButtonStyle)style {
+  if (style == _style) {
+    return;
+  }
+  _style = style;
+  [self updateUI];
+}
+
+- (void)setColorScheme:(GIDSignInButtonColorScheme)colorScheme {
+  if (colorScheme == _colorScheme) {
+    return;
+  }
+  _colorScheme = colorScheme;
+  [self updateUI];
+}
+
+- (void)setEnabled:(BOOL)enabled {
+  if (enabled == self.enabled) {
+    return;
+  }
+  [super setEnabled:enabled];
+  if (enabled) {
+    [self switchToNormal];
+  } else {
+    [self switchToDisabled];
+  }
+  [self updateUI];
+}
+
+- (void)setButtonState:(GIDSignInButtonState)buttonState {
+  if (buttonState == _buttonState) {
+    return;
+  }
+  _buttonState = buttonState;
+  [self setNeedsDisplay];
+}
+
+- (void)setFrame:(CGRect)frame {
+  // Constrain the frame size to sizes we want.
+  frame.size = [self sizeThatFits:frame.size];
+  if (CGRectEqualToRect(frame, self.frame)) {
+    return;
+  }
+  [super setFrame:frame];
+  [self setNeedsUpdateConstraints];
+  [self setNeedsDisplay];
+}
+
+#pragma mark - UI Actions
+
+- (void)pressed {
+  [self switchToNormal];
+  NSString *appearanceCode = [NSString stringWithFormat:@"%ld.%ld",
+                                                        (long)_style,
+                                                        (long)_colorScheme];
+  NSDictionary *params = @{ kLoggingParameter : appearanceCode };
+  [[GIDSignIn sharedInstance]
+      signInWithOptions:[GIDSignInInternalOptions optionsWithExtraParams:params]];
+}
+
+#pragma mark - Helpers
+
+- (CGFloat)minWidth {
+  if (_style == kGIDSignInButtonStyleIconOnly) {
+    return kIconWidth + (kBorderWidth * 2);
+  }
+  NSString *text = [self buttonText];
+  CGSize textSize = [[self class] textSize:text withFont:[[self class] buttonTextFont]];
+  return ceil(kIconWidth + (kTextPadding * 2) + textSize.width + (kBorderWidth * 2));
+}
+
+- (BOOL)isConstraint:(NSLayoutConstraint *)constraintA
+    equalToConstraint:(NSLayoutConstraint *)constraintB {
+  return constraintA.priority == constraintB.priority &&
+      constraintA.firstItem == constraintB.firstItem &&
+      constraintA.firstAttribute == constraintB.firstAttribute &&
+      constraintA.relation == constraintB.relation &&
+      constraintA.secondItem == constraintB.secondItem &&
+      constraintA.secondAttribute == constraintB.secondAttribute &&
+      constraintA.multiplier == constraintB.multiplier &&
+      constraintA.constant == constraintB.constant;
+}
+
+#pragma mark - Overrides
+
+- (CGSize)sizeThatFits:(CGSize)size {
+  switch (_style) {
+    case kGIDSignInButtonStyleIconOnly:
+      return CGSizeMake([self minWidth], kButtonHeight);
+    case kGIDSignInButtonStyleStandard:
+    case kGIDSignInButtonStyleWide: {
+      return CGSizeMake(MAX(size.width, [self minWidth]), kButtonHeight);
+    }
+  }
+}
+
+- (void)updateConstraints {
+  NSLayoutRelation widthConstraintRelation;
+  // For icon style, we want to ensure a fixed width
+  if (_style == kGIDSignInButtonStyleIconOnly) {
+    widthConstraintRelation = NSLayoutRelationEqual;
+  } else {
+    widthConstraintRelation = NSLayoutRelationGreaterThanOrEqual;
+  }
+  // Define a width constraint ensuring that we don't go below our minimum width
+  NSLayoutConstraint *widthConstraint =
+      [NSLayoutConstraint constraintWithItem:self
+                                   attribute:NSLayoutAttributeWidth
+                                   relatedBy:widthConstraintRelation
+                                      toItem:nil
+                                   attribute:NSLayoutAttributeNotAnAttribute
+                                  multiplier:1.0
+                                    constant:[self minWidth]];
+  widthConstraint.identifier = @"buttonWidth - auto generated by GIDSignInButton";
+  // Define a height constraint using our constant height
+  NSLayoutConstraint *heightConstraint =
+      [NSLayoutConstraint constraintWithItem:self
+                                   attribute:NSLayoutAttributeHeight
+                                   relatedBy:NSLayoutRelationEqual
+                                      toItem:nil
+                                   attribute:NSLayoutAttributeNotAnAttribute
+                                  multiplier:1.0
+                                    constant:kButtonHeight];
+  heightConstraint.identifier = @"buttonHeight - auto generated by GIDSignInButton";
+  // By default, add our width and height constraints
+  BOOL addWidthConstraint = YES;
+  BOOL addHeightConstraint = YES;
+
+  for (NSLayoutConstraint *constraint in self.constraints) {
+    // If it is equivalent to our width or height constraint, don't add ours later
+    if ([self isConstraint:constraint equalToConstraint:widthConstraint]) {
+      addWidthConstraint = NO;
+      continue;
+    }
+    if ([self isConstraint:constraint equalToConstraint:heightConstraint]) {
+      addHeightConstraint = NO;
+      continue;
+    }
+    if (constraint.firstItem == self) {
+      // If it is a height constraint of any relation, remove it
+      if (constraint.firstAttribute == NSLayoutAttributeHeight) {
+        [self removeConstraint:constraint];
+      }
+      // If it is a width constraint of any relation, remove it if it will conflict with ours
+      if (constraint.firstAttribute == NSLayoutAttributeWidth &&
+          (constraint.constant < [self minWidth] || _style == kGIDSignInButtonStyleIconOnly)) {
+        [self removeConstraint:constraint];
+      }
+    }
+  }
+
+  if (addWidthConstraint) {
+    [self addConstraint:widthConstraint];
+  }
+  if (addHeightConstraint) {
+    [self addConstraint:heightConstraint];
+  }
+  [super updateConstraints];
+}
+
+#pragma mark - Rendering
+
+- (void)drawRect:(CGRect)rect {
+  [super drawRect:rect];
+
+  CGContextRef context = UIGraphicsGetCurrentContext();
+  CGContextRetain(context);
+
+  if (context == NULL) {
+    return;
+  }
+
+  // Draw the button background
+  [self drawButtonBackground:context];
+
+  // Draw the text
+  [self drawButtonText:context];
+
+  CGContextRelease(context);
+  context = NULL;
+}
+
+#pragma mark - Button Background Rendering
+
+- (void)drawButtonBackground:(CGContextRef)context {
+  CGContextSaveGState(context);
+
+  // Normalize the coordinate system of our graphics context
+  // (0,0) -----> +x
+  // |
+  // |
+  // \/ +y
+  CGContextScaleCTM(context, 1, -1);
+  CGContextTranslateCTM(context, 0, -self.bounds.size.height);
+
+  // Get the colors for the current state and configuration
+  UIColor *background = colorForStyleState(_colorScheme,
+                                           _buttonState,
+                                           kGIDSignInButtonStyleColorBackground);
+
+  // Create rounded rectangle for button background/outline
+  CGMutablePathRef path = CGPathCreateMutable();
+  CGPathAddRoundedRect(path,
+                       NULL,
+                       CGRectInset(self.bounds, kBorderWidth, kBorderWidth),
+                       kCornerRadius,
+                       kCornerRadius);
+
+  // Fill the background and apply halo shadow
+  CGContextSaveGState(context);
+  CGContextAddPath(context, path);
+  CGContextSetFillColorWithColor(context, background.CGColor);
+  // If we're not in the disabled state, we want a shadow
+  if (_buttonState != kGIDSignInButtonStateDisabled) {
+    // Draw halo shadow around button
+    CGContextSetShadowWithColor(context,
+                                CGSizeMake(0, 0),
+                                kHaloShadowBlur,
+                                [UIColor colorWithWhite:0 alpha:kHaloShadowAlpha].CGColor);
+  }
+  CGContextFillPath(context);
+  CGContextRestoreGState(context);
+
+  if (_buttonState != kGIDSignInButtonStateDisabled) {
+    // Fill the background again to apply drop shadow
+    CGContextSaveGState(context);
+    CGContextAddPath(context, path);
+    CGContextSetFillColorWithColor(context, background.CGColor);
+    CGContextSetShadowWithColor(context,
+                                CGSizeMake(0, kDropShadowYOffset),
+                                kDropShadowBlur,
+                                [UIColor colorWithWhite:0 alpha:kDropShadowAlpha].CGColor);
+    CGContextFillPath(context);
+    CGContextRestoreGState(context);
+  }
+
+  if (_colorScheme == kGIDSignInButtonColorSchemeDark &&
+      _buttonState != kGIDSignInButtonStateDisabled) {
+    // Create rounded rectangle container for the "G"
+    CGMutablePathRef gContainerPath = CGPathCreateMutable();
+    CGPathAddRoundedRect(gContainerPath,
+                         NULL,
+                         CGRectInset(CGRectMake(0, 0, kButtonHeight, kButtonHeight),
+                                     kBorderWidth + 1,
+                                     kBorderWidth + 1),
+                         kCornerRadius,
+                         kCornerRadius);
+    CGContextAddPath(context, gContainerPath);
+    CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
+    CGContextFillPath(context);
+    CGPathRelease(gContainerPath);
+  }
+
+  CGPathRelease(path);
+  CGContextRestoreGState(context);
+}
+
+#pragma mark - Text Rendering
+
+- (void)drawButtonText:(CGContextRef)context {
+  if (_style == kGIDSignInButtonStyleIconOnly) {
+    return;
+  }
+
+  NSString *text = self.accessibilityLabel;
+
+  UIColor *foregroundColor = colorForStyleState(_colorScheme,
+                                                _buttonState,
+                                                kGIDSignInButtonStyleColorForeground);
+  UIFont *font = [[self class] buttonTextFont];
+  CGSize textSize = [[self class] textSize:text withFont:font];
+
+  // Draw the button text at the right position with the right color.
+  CGFloat textLeft = kIconWidth + kTextPadding;
+  CGFloat textTop = round((self.bounds.size.height - textSize.height) / 2);
+
+  [text drawAtPoint:CGPointMake(textLeft, textTop)
+      withAttributes:@{ NSFontAttributeName : font,
+                        NSForegroundColorAttributeName : foregroundColor }];
+}
+
+#pragma mark - Button Text Selection / Localization
+
+- (NSString *)buttonText {
+  switch (_style) {
+    case kGIDSignInButtonStyleWide:
+      return [GIDSignInStrings signInWithGoogleString];
+    case kGIDSignInButtonStyleStandard:
+    case kGIDSignInButtonStyleIconOnly:
+      return [GIDSignInStrings signInString];
+  }
+}
+
++ (UIFont *)buttonTextFont {
+  UIFont *font = [UIFont fontWithName:kFontNameRobotoBold size:kFontSize];
+  if (!font) {
+    font = [UIFont boldSystemFontOfSize:kFontSize];
+  }
+  return font;
+}
+
++ (CGSize)textSize:(NSString *)text withFont:(UIFont *)font {
+  return [text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)
+                            options:0
+                         attributes:@{ NSFontAttributeName : font }
+                            context:nil].size;
+}
+
+@end
+
+#pragma mark - UIImage GIDAdditions_Private Category
+
+@implementation UIImage (GIDAdditions_Private)
+
+- (UIImage *)gid_imageWithBlendMode:(CGBlendMode)blendMode color:(UIColor *)color {
+  CGSize size = [self size];
+  CGRect rect = CGRectMake(0.0f, 0.0f, size.width, size.height);
+
+  UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0);
+  CGContextRef context = UIGraphicsGetCurrentContext();
+  CGContextSetShouldAntialias(context, true);
+  CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
+
+  CGContextScaleCTM(context, 1, -1);
+  CGContextTranslateCTM(context, 0, -rect.size.height);
+
+  CGContextClipToMask(context, rect, self.CGImage);
+  CGContextDrawImage(context, rect, self.CGImage);
+
+  CGContextSetBlendMode(context, blendMode);
+
+  CGFloat alpha = 1.0;
+  if (blendMode == kCGBlendModeMultiply) {
+    CGFloat red, green, blue;
+    BOOL success = [color getRed:&red green:&green blue:&blue alpha:&alpha];
+    if (success) {
+      color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
+    } else {
+      CGFloat grayscale;
+      success = [color getWhite:&grayscale alpha:&alpha];
+      if (success) {
+        color = [UIColor colorWithWhite:grayscale alpha:1.0];
+      }
+    }
+  }
+
+  CGContextSetFillColorWithColor(context, color.CGColor);
+  CGContextFillRect(context, rect);
+
+  if (blendMode == kCGBlendModeMultiply && alpha != 1.0) {
+    // Modulate by the alpha.
+    color = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:alpha];
+    CGContextSetBlendMode(context, kCGBlendModeDestinationIn);
+    CGContextSetFillColorWithColor(context, color.CGColor);
+    CGContextFillRect(context, rect);
+  }
+
+  UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
+  UIGraphicsEndImageContext();
+
+  if (self.capInsets.bottom > 0 || self.capInsets.top > 0 ||
+      self.capInsets.left > 0 || self.capInsets.left > 0) {
+    image = [image resizableImageWithCapInsets:self.capInsets];
+  }
+
+  return image;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 28 - 0
GoogleSignIn/Sources/GIDSignInButton_Private.h

@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 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/Public/GoogleSignIn/GIDSignInButton.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Private |GIDSignInButton| methods that are used internally in this SDK.
+@interface GIDSignInButton ()
+
+- (void)pressed;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 44 - 0
GoogleSignIn/Sources/GIDSignInCallbackSchemes.h

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// A utility class for dealing with callback schemes.
+@interface GIDSignInCallbackSchemes : NSObject
+
+// Please call the designated initializer.
+- (instancetype)init NS_UNAVAILABLE;
+
+// The designated initializer.
+- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier NS_DESIGNATED_INITIALIZER;
+
+// The canonical client identifier callback scheme. Requires clientId to be set on GIDSignIn.
+- (NSString *)clientIdentifierScheme;
+
+// An array of all schemes used for sign-in callbacks.
+- (NSArray *)allSchemes;
+
+// Returns a list of URL schemes the current app host should support for Google Sign-In to work.
+- (NSMutableArray *)unsupportedSchemes;
+
+// Indicates the scheme of an NSURL is a sign-in callback scheme.
+- (BOOL)URLSchemeIsCallbackScheme:(NSURL *)URL;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 88 - 0
GoogleSignIn/Sources/GIDSignInCallbackSchemes.m

@@ -0,0 +1,88 @@
+// Copyright 2021 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/GIDSignInCallbackSchemes.h"
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation GIDSignInCallbackSchemes {
+  NSString *_clientIdentifier;
+}
+
+/**
+ * @fn relevantURLSchemes
+ * @brief Extracts CFBundleURLSchemes from the host app's info.plist.
+ * @return An array of lowercase NSString *'s representing the URL schemes the host app has declared
+ *     support for.
+ * @remarks Branched from google3/googlemac/iPhone/Firebase/Source/GGLBundleUtil.m
+ */
++ (NSArray *)relevantURLSchemes {
+  NSMutableArray *result = [NSMutableArray array];
+  NSBundle *bundle = [NSBundle mainBundle];
+  NSArray *urlTypes = [bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"];
+  for (NSDictionary *urlType in urlTypes) {
+    NSArray *urlTypeSchemes = urlType[@"CFBundleURLSchemes"];
+    for (NSString *urlTypeScheme in urlTypeSchemes) {
+      [result addObject:urlTypeScheme.lowercaseString];
+    }
+  }
+  return result;
+}
+
+- (instancetype)initWithClientIdentifier:(NSString *)clientIdentifier {
+  self = [super init];
+  if (self) {
+    _clientIdentifier = [clientIdentifier copy];
+  }
+  return self;
+}
+
+- (NSString *)clientIdentifierScheme {
+  NSArray *clientIdentifierParts = [_clientIdentifier componentsSeparatedByString:@"."];
+  NSString *reversedClientIdentifier =
+      [[clientIdentifierParts reverseObjectEnumerator].allObjects componentsJoinedByString:@"."];
+  return reversedClientIdentifier.lowercaseString;
+}
+
+- (NSArray *)allSchemes {
+  NSMutableArray *schemes = [NSMutableArray array];
+  NSString *clientIdentifierScheme = [self clientIdentifierScheme];
+  if (clientIdentifierScheme) {
+    [schemes addObject:clientIdentifierScheme];
+  }
+  return schemes;
+}
+
+- (NSMutableArray *)unsupportedSchemes {
+  NSMutableArray *unsupportedSchemes = [NSMutableArray arrayWithArray:[self allSchemes]];
+  NSArray *supportedSchemes = [[self class] relevantURLSchemes];
+  [unsupportedSchemes removeObjectsInArray:supportedSchemes];
+  return unsupportedSchemes;
+}
+
+- (BOOL)URLSchemeIsCallbackScheme:(NSURL *)URL {
+  NSString *incomingURLScheme = URL.scheme.lowercaseString;
+  for (NSString *scheme in [self allSchemes]) {
+    if ([incomingURLScheme isEqual:scheme]) {
+      return YES;
+    }
+  }
+  return NO;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 49 - 0
GoogleSignIn/Sources/GIDSignInInternalOptions.h

@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// The options used internally for aspects of the sign-in flow.
+@interface GIDSignInInternalOptions : NSObject
+
+/// Whether interaction with user is allowed at all.
+@property(nonatomic, readonly) BOOL interactive;
+
+/// Whether the sign-in is a continuation of the previous one.
+@property(nonatomic, readonly) BOOL continuation;
+
+/// The extra parameters used in the sign-in URL.
+@property(nonatomic, readonly, nullable) NSDictionary *extraParams;
+
+/// Creates the default options.
++ (instancetype)defaultOptions;
+
+/// Creates the options to sign in silently.
++ (instancetype)silentOptions;
+
+/// Creates the options to sign in with extra parameters.
++ (instancetype)optionsWithExtraParams:(NSDictionary *)extraParams;
+
+/// Creates options with the same values as the receiver, except for the "extra parameters", and
+/// continuation flag, which are replaced by the arguments passed to this method.
+- (instancetype)optionsWithExtraParameters:(NSDictionary *)extraParams
+                           forContinuation:(BOOL)continuation;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 59 - 0
GoogleSignIn/Sources/GIDSignInInternalOptions.m

@@ -0,0 +1,59 @@
+// Copyright 2021 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/GIDSignInInternalOptions.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation GIDSignInInternalOptions
+
++ (instancetype)defaultOptions {
+  GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init];
+  if (options) {
+    options->_interactive = YES;
+    options->_continuation = NO;
+  }
+  return options;
+}
+
++ (instancetype)silentOptions {
+  GIDSignInInternalOptions *options = [self defaultOptions];
+  if (options) {
+    options->_interactive = NO;
+  }
+  return options;
+}
+
++ (instancetype)optionsWithExtraParams:(NSDictionary *)extraParams {
+  GIDSignInInternalOptions *options = [self defaultOptions];
+  if (options) {
+    options->_extraParams = [extraParams copy];
+  }
+  return options;
+}
+
+- (instancetype)optionsWithExtraParameters:(NSDictionary *)extraParams
+                           forContinuation:(BOOL)continuation {
+  GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init];
+  if (options) {
+    options->_interactive = _interactive;
+    options->_continuation = continuation;
+    options->_extraParams = [extraParams copy];
+  }
+  return options;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 33 - 0
GoogleSignIn/Sources/GIDSignInPreferences.h

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+extern NSString *const kSDKVersionLoggingParameter;
+
+NSString* GIDVersion(void);
+
+@interface GIDSignInPreferences : NSObject
+
++ (NSString *)googleAuthorizationServer;
++ (NSString *)googleTokenServer;
++ (NSString *)googleUserInfoServer;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 55 - 0
GoogleSignIn/Sources/GIDSignInPreferences.m

@@ -0,0 +1,55 @@
+// Copyright 2021 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/GIDSignInPreferences.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *const kLSOServer = @"accounts.google.com";
+static NSString *const kTokenServer = @"oauth2.googleapis.com";
+static NSString *const kUserInfoServer = @"www.googleapis.com";
+
+// This convolved list of C macros is needed because the Xcode project generated
+// by blaze build and the command itself have inconsistent escaping of the
+// quotation marks in -D flags, which makes passing '"' as part of the macro
+// value impossible.
+#define STR(x) STR_EXPAND(x)
+#define STR_EXPAND(x) #x
+
+// The name of the query parameter used for logging the SDK version.
+NSString *const kSDKVersionLoggingParameter = @"gpsdk";
+
+// The prefixed sdk version string to differentiate gid version values used with the legacy gpsdk
+// logging key.
+NSString* GIDVersion(void) {
+  return [NSString stringWithFormat:@"gid-%s",(const char* const)STR(GID_SDK_VERSION)];
+}
+
+@implementation GIDSignInPreferences
+
++ (NSString *)googleAuthorizationServer {
+  return kLSOServer;
+}
+
++ (NSString *)googleTokenServer {
+  return kTokenServer;
+}
+
++ (NSString *)googleUserInfoServer {
+  return kUserInfoServer;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 38 - 0
GoogleSignIn/Sources/GIDSignInStrings.h

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Provides localized strings.
+// TODO(xiangtian) At some point we should probably convert this so that it's auto-generated from
+// a script. This is a "better than what was there before, and what we need now, but probably not
+// ideal" solution.
+@interface GIDSignInStrings : NSObject
+
+// Returns the localized string for the key if available, or the supplied default text if not.
++ (nullable NSString *)localizedStringForKey:(NSString *)key text:(NSString *)text;
+
+// "Sign In"
++ (nullable NSString *)signInString;
+
+// "Sign in with Google"
++ (nullable NSString *)signInWithGoogleString;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 47 - 0
GoogleSignIn/Sources/GIDSignInStrings.m

@@ -0,0 +1,47 @@
+// Copyright 2021 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/GIDSignInStrings.h"
+
+#import "GoogleSignIn/Sources/NSBundle+GID3PAdditions.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// The table name for localized strings (i.e. file name before .strings suffix).
+static NSString * const kStringsTableName = @"GoogleSignIn";
+
+#pragma mark - Button Text Constants
+
+// Button texts used as both keys in localized strings files and default values.
+static NSString *const kStandardButtonText = @"Sign in";
+static NSString *const kWideButtonText = @"Sign in with Google";
+
+@implementation GIDSignInStrings
+
++ (nullable NSString *)localizedStringForKey:(NSString *)key text:(NSString *)text {
+  NSBundle *frameworkBundle = [NSBundle gid_frameworkBundle];
+  return [frameworkBundle localizedStringForKey:key value:text table:kStringsTableName];
+}
+
++ (nullable NSString *)signInString {
+  return [self localizedStringForKey:kStandardButtonText text:kStandardButtonText];
+}
+
++ (nullable NSString *)signInWithGoogleString {
+  return [self localizedStringForKey:kWideButtonText text:kWideButtonText];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 34 - 0
GoogleSignIn/Sources/GIDSignIn_Private.h

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 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/Public/GoogleSignIn/GIDSignIn.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class GIDSignInInternalOptions;
+
+// Private |GIDSignIn| methods that are used internally in this SDK and other Google SDKs.
+@interface GIDSignIn ()
+
+// Private initializer for |GIDSignIn|.
+- (instancetype)initPrivate;
+
+// Authenticates with extra options.
+- (void)signInWithOptions:(GIDSignInInternalOptions *)options;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 31 - 0
GoogleSignIn/Sources/NSBundle+GID3PAdditions.h

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface NSBundle (GID3PAdditions)
+
+// Gets the bundle for the SDK framework.
++ (nullable NSBundle *)gid_frameworkBundle;
+
+// Registers fonts needed for the SDK to work. Okay to call multiple times.
++ (void)gid_registerFonts;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 60 - 0
GoogleSignIn/Sources/NSBundle+GID3PAdditions.m

@@ -0,0 +1,60 @@
+// Copyright 2021 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/NSBundle+GID3PAdditions.h"
+
+#import <CoreText/CoreText.h>
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation NSBundle (GID3PAdditions)
+
++ (nullable NSBundle *)gid_frameworkBundle {
+  NSString* mainBundlePath = [[NSBundle mainBundle] resourcePath];
+  NSString* frameworkBundlePath = [mainBundlePath
+      stringByAppendingPathComponent:@"GoogleSignIn.bundle"];
+  return [NSBundle bundleWithPath:frameworkBundlePath];
+}
+
++ (void)gid_registerFonts {
+  static dispatch_once_t once;
+  dispatch_once(&once, ^{
+    NSArray* allFontNames = @[ @"Roboto-Bold" ];
+    NSBundle* bundle = [self gid_frameworkBundle];
+    for (NSString *fontName in allFontNames) {
+      // Check to see if the font is already here, and skip registration if so.
+      if ([UIFont fontWithName:fontName size:[UIFont systemFontSize]]) {  // size doesn't matter
+        continue;
+      }
+
+      // Load the font data file from the bundle.
+      NSString *path = [bundle pathForResource:fontName ofType:@"ttf"];
+      CGDataProviderRef provider = CGDataProviderCreateWithFilename([path UTF8String]);
+      CFErrorRef error;
+      CGFontRef newFont = CGFontCreateWithDataProvider(provider);
+      if (!newFont || !CTFontManagerRegisterGraphicsFont(newFont, &error)) {
+#ifdef DEBUG
+        NSLog(@"Unable to load font: %@", fontName);
+#endif
+      }
+      CGFontRelease(newFont);
+      CGDataProviderRelease(provider);
+    }
+  });
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 66 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h

@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 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>
+
+@protocol GTMFetcherAuthorizationProtocol;
+@class GIDAuthentication;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// The callback block that takes a `GIDAuthentication` or an error if the attempt to refresh tokens
+/// was unsuccessful.
+typedef void (^GIDAuthenticationAction)(GIDAuthentication *authentication,
+                                        NSError *_Nullable error);
+
+/// This class represents the OAuth 2.0 entities needed for sign-in.
+@interface GIDAuthentication : NSObject <NSSecureCoding>
+
+/// The client ID associated with the authentication.
+@property(nonatomic, readonly) NSString *clientID;
+
+/// The OAuth2 access token to access Google services.
+@property(nonatomic, readonly) NSString *accessToken;
+
+/// The estimated expiration date of the access token.
+@property(nonatomic, readonly) NSDate *accessTokenExpirationDate;
+
+/// The OAuth2 refresh token to exchange for new access tokens.
+@property(nonatomic, readonly) NSString *refreshToken;
+
+/// An OpenID Connect ID token that identifies the user. Send this token to your server to
+/// authenticate the user there. For more information on this topic, see
+/// https://developers.google.com/identity/sign-in/ios/backend-auth
+@property(nonatomic, readonly, nullable) NSString *idToken;
+
+/// The estimated expiration date of the ID token.
+@property(nonatomic, readonly, nullable) NSDate *idTokenExpirationDate;
+
+/// Gets a new authorizer for `GTLService`, `GTMSessionFetcher`, or `GTMHTTPFetcher`.
+///
+/// @return A new authorizer
+- (id<GTMFetcherAuthorizationProtocol>)fetcherAuthorizer;
+
+/// Get a valid access token and a valid ID token, refreshing them first if they have expired or are
+/// about to expire.
+///
+/// @param action A callback block that takes a `GIDAuthentication` or an error if the attempt to
+///               refresh tokens was unsuccessful.
+- (void)doWithFreshTokens:(GIDAuthenticationAction)action;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 77 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h

@@ -0,0 +1,77 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// This class represents the client configuration provided by the developer.
+@interface GIDConfiguration : NSObject <NSCopying, NSSecureCoding>
+
+/// The client ID of the app from the Google Cloud Console.
+@property(nonatomic, readonly) NSString *clientID;
+
+/// The client ID of the home web server.  This will be returned as the `audience` property of the
+/// OpenID Connect ID token.  For more info on the ID token:
+/// https://developers.google.com/identity/sign-in/ios/backend-auth
+@property(nonatomic, readonly, nullable) NSString *serverClientID;
+
+/// The login hint to the authorization server, for example the user's ID, or email address,
+/// to be prefilled if possible.
+@property(nonatomic, readonly, nullable) NSString *loginHint;
+
+/// The Google Apps domain to which users must belong to sign in.  To verify, check
+/// `GIDGoogleUser`'s `hostedDomain` property.
+@property(nonatomic, readonly, nullable) NSString *hostedDomain;
+
+/// The OpenID2 realm of the home web server. This allows Google to include the user's OpenID
+/// Identifier in the OpenID Connect ID token.
+@property(nonatomic, readonly, nullable) NSString *openIDRealm;
+
+/// Unavailable.  Please use `initWithClientID:` or one of the other initializers below.
+- (instancetype)init NS_UNAVAILABLE;
+
+/// Initialize a `GIDConfiguration` object with a client ID.
+///
+/// @param clientID The client ID of the app.
+/// @return An initilized `GIDConfiguration` instance.
+- (instancetype)initWithClientID:(NSString *)clientID;
+
+/// Initialize a `GIDConfiguration` object with a client ID and server client ID.
+///
+/// @param clientID The client ID of the app.
+/// @param serverClientID The server's client ID.
+/// @return An initilized `GIDConfiguration` instance.
+- (instancetype)initWithClientID:(NSString *)clientID
+                  serverClientID:(nullable NSString *)serverClientID;
+
+/// Initialize a `GIDConfiguration` object by specifying all available properties.
+///
+/// @param clientID The client ID of the app.
+/// @param serverClientID The server's client ID.
+/// @param loginHint The login hint to be used.
+/// @param hostedDomain The Google Apps domain to be used.
+/// @param openIDRealm The OpenID realm to be used.
+/// @return An initilized `GIDConfiguration` instance.
+- (instancetype)initWithClientID:(NSString *)clientID
+                  serverClientID:(nullable NSString *)serverClientID
+                       loginHint:(nullable NSString *)loginHint
+                    hostedDomain:(nullable NSString *)hostedDomain
+                     openIDRealm:(nullable NSString *)openIDRealm NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 49 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h

@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class GIDAuthentication;
+@class GIDProfileData;
+
+/// This class represents a user account.
+@interface GIDGoogleUser : NSObject <NSSecureCoding>
+
+/// The Google user ID.
+@property(nonatomic, readonly, nullable) NSString *userID;
+
+/// Representation of the Basic profile data. It is only available if
+/// `GIDSignIn.shouldFetchBasicProfile` is set and either `-[GIDSignIn signIn]` or
+/// `-[GIDSignIn restorePreviousSignIn]` has been completed successfully.
+@property(nonatomic, readonly, nullable) GIDProfileData *profile;
+
+/// The authentication object for the user.
+@property(nonatomic, readonly) GIDAuthentication *authentication;
+
+/// The API scopes granted to the app in an array of `NSString`.
+@property(nonatomic, readonly, nullable) NSArray *grantedScopes;
+
+/// For Google Apps hosted accounts, the domain of the user.
+@property(nonatomic, readonly, nullable) NSString *hostedDomain;
+
+/// An OAuth2 authorization code for the home server.
+@property(nonatomic, readonly, nullable) NSString *serverAuthCode;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 47 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// This class represents the basic profile information of a `GIDGoogleUser`.
+@interface GIDProfileData : NSObject <NSCopying, NSSecureCoding>
+
+/// The Google user's email.
+@property(nonatomic, readonly) NSString *email;
+
+/// The Google user's full name.
+@property(nonatomic, readonly) NSString *name;
+
+/// The Google user's given name.
+@property(nonatomic, readonly) NSString *givenName;
+
+/// The Google user's family name.
+@property(nonatomic, readonly) NSString *familyName;
+
+/// Whether or not the user has profile image.
+@property(nonatomic, readonly) BOOL hasImage;
+
+/// Gets the user's profile image URL for the given dimension in pixels for each side of the square.
+///
+/// @param dimension The desired height (and width) of the profile image.
+/// @return The URL of the user's profile image.
+- (NSURL *)imageURLWithDimension:(NSUInteger)dimension;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 171 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h

@@ -0,0 +1,171 @@
+/*
+ * Copyright 2021 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 <UIKit/UIKit.h>
+
+@class GIDGoogleUser;
+@class GIDSignIn;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// The error domain for `NSError`s returned by the Google Identity SDK.
+extern NSString *const kGIDSignInErrorDomain;
+
+/// A list of potential error codes returned from the Google Identity SDK.
+typedef NS_ENUM(NSInteger, GIDSignInErrorCode) {
+  /// Indicates an unknown error has occurred.
+  kGIDSignInErrorCodeUnknown = -1,
+  /// Indicates a problem reading or writing to the application keychain.
+  kGIDSignInErrorCodeKeychain = -2,
+  /// Indicates there are no valid auth tokens in the keychain. This error code will be returned by
+  /// `restorePreviousSignIn` if the user has not signed in before or if they have since signed out.
+  kGIDSignInErrorCodeHasNoAuthInKeychain = -4,
+  /// Indicates the user canceled the sign in request.
+  kGIDSignInErrorCodeCanceled = -5,
+  /// Indicates an Enterprise Mobility Management related error has occurred.
+  kGIDSignInErrorCodeEMM = -6,
+};
+
+/// A protocol implemented by the delegate of `GIDSignIn` to receive a refresh token or an error.
+@protocol GIDSignInDelegate <NSObject>
+
+/// The sign-in flow has finished and was successful if `error` is `nil`.
+- (void)signIn:(GIDSignIn *)signIn
+    didSignInForUser:(nullable GIDGoogleUser *)user
+           withError:(nullable NSError *)error;
+
+@optional
+
+/// Finished disconnecting `user` from the app successfully if `error` is `nil`.
+- (void)signIn:(GIDSignIn *)signIn
+    didDisconnectWithUser:(nullable GIDGoogleUser *)user
+                withError:(nullable NSError *)error;
+
+@end
+
+/// This class signs the user in with Google. It also provides single sign-on via a capable Google
+/// app if one is installed.
+///
+/// For reference, please see "Google Sign-In for iOS" at
+/// https://developers.google.com/identity/sign-in/ios
+///
+/// Here is sample code to use `GIDSignIn`:
+/// 1. Get a reference to the `GIDSignIn` shared instance:
+///    ```
+///    GIDSignIn *signIn = [GIDSignIn sharedInstance];
+///    ```
+/// 2. Call `[signIn setDelegate:self]`;
+/// 3. Set up delegate method `signIn:didSignInForUser:withError:`.
+/// 4. Call `handleURL` on the shared instance from `application:openUrl:...` in your app delegate.
+/// 5. Call `signIn` on the shared instance;
+@interface GIDSignIn : NSObject
+
+/// The authentication object for the current user, or `nil` if there is currently no logged in
+/// user.
+@property(nonatomic, readonly, nullable) GIDGoogleUser *currentUser;
+
+/// The object to be notified when authentication is finished.
+@property(nonatomic, weak, nullable) id<GIDSignInDelegate> delegate;
+
+/// The view controller used to present `SFSafariViewContoller` on iOS 9 and 10.
+@property(nonatomic, weak, nullable) UIViewController *presentingViewController;
+
+/// The client ID of the app from the Google APIs console.  Must set for sign-in to work.
+@property(nonatomic, copy, nullable) NSString *clientID;
+
+/// The API scopes requested by the app in an array of `NSString`s.  The default value is `@[]`.
+///
+/// This property is optional. If you set it, set it before calling `signIn`.
+@property(nonatomic, copy, nullable) NSArray<NSString *> *scopes;
+
+/// Whether or not to fetch basic profile data after signing in. The data is saved in the
+/// `GIDGoogleUser.profileData` object.
+///
+/// Setting the flag will add "email" and "profile" to scopes.
+/// Defaults to `YES`.
+@property(nonatomic, assign) BOOL shouldFetchBasicProfile;
+
+/// The login hint to the authorization server, for example the user's ID, or email address,
+/// to be prefilled if possible.
+///
+/// This property is optional. If you set it, set it before calling `signIn`.
+@property(nonatomic, copy, nullable) NSString *loginHint;
+
+/// The client ID of the home web server.  This will be returned as the `audience` property of the
+/// OpenID Connect ID token.  For more info on the ID token:
+/// https://developers.google.com/identity/sign-in/ios/backend-auth
+///
+/// This property is optional. If you set it, set it before calling `signIn`.
+@property(nonatomic, copy, nullable) NSString *serverClientID;
+
+/// The OpenID2 realm of the home web server. This allows Google to include the user's OpenID
+/// Identifier in the OpenID Connect ID token.
+///
+/// This property is optional. If you set it, set it before calling `signIn`.
+@property(nonatomic, copy, nullable) NSString *openIDRealm;
+
+/// The Google Apps domain to which users must belong to sign in.  To verify, check
+/// `GIDGoogleUser`'s `hostedDomain` property.
+///
+/// This property is optional. If you set it, set it before calling `signIn`.
+@property(nonatomic, copy, nullable) NSString *hostedDomain;
+
+/// Returns a shared `GIDSignIn` instance.
++ (GIDSignIn *)sharedInstance;
+
+/// Unavailable. Use `sharedInstance` to instantiate `GIDSignIn`.
++ (instancetype)new NS_UNAVAILABLE;
+
+/// Unavailable. Use `sharedInstance` to instantiate `GIDSignIn`.
+- (instancetype)init NS_UNAVAILABLE;
+
+/// This method should be called from your `UIApplicationDelegate`'s `application:openURL:options`
+/// and `application:openURL:sourceApplication:annotation` method(s).
+///
+/// @param url The URL that was passed to the app.
+/// @return `YES` if `GIDSignIn` handled this URL.
+- (BOOL)handleURL:(NSURL *)url;
+
+/// Checks if there is a previously authenticated user saved in keychain.
+///
+/// @return `YES` if there is a previously authenticated user saved in keychain.
+- (BOOL)hasPreviousSignIn;
+
+/// Attempts to restore a previously authenticated user without interaction.
+
+/// The delegate will be called at the end of this process indicating success or failure.  The
+/// current values of `GIDSignIn`'s configuration properties will not impact the restored user.
+- (void)restorePreviousSignIn;
+
+/// Starts an interactive sign-in flow using `GIDSignIn`'s configuration properties.
+///
+/// The delegate will be called at the end of this process.  Any saved sign-in state will be
+/// replaced by the result of this flow.  Note that this method should not be called when the app is
+/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the
+/// `restorePreviousSignIn` method to restore a previous sign-in.
+- (void)signIn;
+
+/// Marks current user as being in the signed out state.
+- (void)signOut;
+
+/// Disconnects the current user from the app and revokes previous authentication. If the operation
+/// succeeds, the OAuth 2.0 token is also removed from keychain.
+- (void)disconnect;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 63 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInButton.h

@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 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 <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// The layout styles supported by the `GIDSignInButton`.
+///
+/// The minimum size of the button depends on the language used for text.
+/// The following dimensions (in points) fit for all languages:
+/// - kGIDSignInButtonStyleStandard: 230 x 48
+/// - kGIDSignInButtonStyleWide:     312 x 48
+/// - kGIDSignInButtonStyleIconOnly: 48 x 48 (no text, fixed size)
+typedef NS_ENUM(NSInteger, GIDSignInButtonStyle) {
+  kGIDSignInButtonStyleStandard = 0,
+  kGIDSignInButtonStyleWide = 1,
+  kGIDSignInButtonStyleIconOnly = 2
+};
+
+/// The color schemes supported by the `GIDSignInButton`.
+typedef NS_ENUM(NSInteger, GIDSignInButtonColorScheme) {
+  kGIDSignInButtonColorSchemeDark = 0,
+  kGIDSignInButtonColorSchemeLight = 1
+};
+
+/// This class provides the "Sign in with Google" button.
+///
+/// You can instantiate this class programmatically or from a NIB file. You
+/// should set up the `GIDSignIn` shared instance with your client ID and any
+/// additional scopes, implement the delegate methods for `GIDSignIn`, and add
+/// this button to your view hierarchy.
+@interface GIDSignInButton : UIControl
+
+/// The layout style for the sign-in button.
+/// Possible values:
+/// - kGIDSignInButtonStyleStandard: 230 x 48 (default)
+/// - kGIDSignInButtonStyleWide:     312 x 48
+/// - kGIDSignInButtonStyleIconOnly: 48 x 48 (no text, fixed size)
+@property(nonatomic, assign) GIDSignInButtonStyle style;
+
+/// The color scheme for the sign-in button.
+/// Possible values:
+/// - kGIDSignInButtonColorSchemeDark
+/// - kGIDSignInButtonColorSchemeLight (default)
+@property(nonatomic, assign) GIDSignInButtonColorScheme colorScheme;
+
+@end
+
+NS_ASSUME_NONNULL_END

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

@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 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/Public/GoogleSignIn/GIDAuthentication.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDProfileData.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInButton.h"

+ 207 - 0
GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m

@@ -0,0 +1,207 @@
+// Copyright 2021 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/XCTest.h>
+
+#import "GoogleSignIn/Sources/GIDAuthStateMigration.h"
+#import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
+
+#import <AppAuth/AppAuth.h>
+#import <GTMAppAuth/GTMAppAuthFetcherAuthorization+Keychain.h>
+#import <GTMAppAuth/GTMKeychain.h>
+#import <GTMAppAuth/GTMOAuth2KeychainCompatibility.h>
+#import <OCMock/OCMock.h>
+
+static NSString *const kTokenURL = @"https://host.com/example/token/url";
+static NSString *const kCallbackPath = @"/callback/path";
+static NSString *const kKeychainName = @"keychain_name";
+static NSString *const kBundleID = @"com.google.GoogleSignInInternalSample.dev";
+static NSString *const kClientID =
+    @"223520599684-kg64hfn0h950oureqacja2fltg00msv3.apps.googleusercontent.com";
+static NSString *const kDotReversedClientID =
+    @"com.googleusercontent.apps.223520599684-kg64hfn0h950oureqacja2fltg00msv3";
+static NSString *const kSavedFingerprint = @"com.google.GoogleSignInInternalSample.dev-"
+    "223520599684-kg64hfn0h950oureqacja2fltg00msv3.apps.googleusercontent.com-email profile";
+static NSString *const kSavedFingerprint_HostedDomain =
+    @"com.google.GoogleSignInInternalSample.dev-"
+    "223520599684-kg64hfn0h950oureqacja2fltg00msv3.apps.googleusercontent.com-email profile-"
+    "hd=test.com";
+static NSString *const kGTMOAuth2PersistenceString = @"param1=value1&param2=value2";
+static NSString *const kAdditionalTokenRequestParametersPostfix = @"~~atrp";
+static NSString *const kAdditionalTokenRequestParameters = @"param3=value3&param4=value4";
+static NSString *const kFinalPersistenceString =
+    @"param1=value1&param2=value2&param3=value3&param4=value4";
+static NSString *const kRedirectURI =
+    @"com.googleusercontent.apps.223520599684-kg64hfn0h950oureqacja2fltg00msv3:/callback/path";
+
+static NSString *const kMigrationCheckPerformedKey = @"GID_MigrationCheckPerformed";
+static NSString *const kFingerprintService = @"fingerprint";
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GIDAuthStateMigration ()
+
++ (nullable GTMAppAuthFetcherAuthorization *)
+    extractAuthorizationWithTokenURL:(NSURL *)tokenURL callbackPath:(NSString *)callbackPath;
+
++ (nullable NSString *)passwordForService:(NSString *)service;
+
+@end
+
+@interface GIDAuthStateMigrationTest : XCTestCase
+@end
+
+@implementation GIDAuthStateMigrationTest {
+  id _mockUserDefaults;
+  id _mockGTMAppAuthFetcherAuthorization;
+  id _mockGIDAuthStateMigration;
+  id _mockGTMKeychain;
+  id _mockNSBundle;
+  id _mockGIDSignInCallbackSchemes;
+  id _mockGTMOAuth2KeychainCompatibility;
+}
+
+- (void)setUp {
+  [super setUp];
+
+  _mockUserDefaults = OCMStrictClassMock([NSUserDefaults class]);
+  _mockGTMAppAuthFetcherAuthorization = OCMStrictClassMock([GTMAppAuthFetcherAuthorization class]);
+  _mockGIDAuthStateMigration = OCMStrictClassMock([GIDAuthStateMigration class]);
+  _mockGTMKeychain = OCMStrictClassMock([GTMKeychain class]);
+  _mockNSBundle = OCMStrictClassMock([NSBundle class]);
+  _mockGIDSignInCallbackSchemes = OCMStrictClassMock([GIDSignInCallbackSchemes class]);
+  _mockGTMOAuth2KeychainCompatibility = OCMStrictClassMock([GTMOAuth2KeychainCompatibility class]);
+}
+
+- (void)tearDown {
+  [_mockUserDefaults verify];
+  [_mockUserDefaults stopMocking];
+  [_mockGTMAppAuthFetcherAuthorization verify];
+  [_mockGTMAppAuthFetcherAuthorization stopMocking];
+  [_mockGIDAuthStateMigration verify];
+  [_mockGIDAuthStateMigration stopMocking];
+  [_mockGTMKeychain verify];
+  [_mockGTMKeychain stopMocking];
+  [_mockNSBundle verify];
+  [_mockNSBundle stopMocking];
+  [_mockGIDSignInCallbackSchemes verify];
+  [_mockGIDSignInCallbackSchemes stopMocking];
+  [_mockGTMOAuth2KeychainCompatibility verify];
+  [_mockGTMOAuth2KeychainCompatibility stopMocking];
+
+  [super tearDown];
+}
+
+#pragma mark - Tests
+
+- (void)testMigrateIfNeeded_NoPreviousMigration {
+  [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
+  [[[_mockUserDefaults expect] andReturnValue:@NO]
+      boolForKey:kMigrationCheckPerformedKey];
+  [[[_mockGIDAuthStateMigration expect] andReturn:_mockGTMAppAuthFetcherAuthorization]
+      extractAuthorizationWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath];
+  [[[_mockGTMAppAuthFetcherAuthorization expect] andReturnValue:@YES]
+      saveAuthorization:_mockGTMAppAuthFetcherAuthorization toKeychainForName:kKeychainName];
+  [[_mockUserDefaults expect] setBool:YES forKey:kMigrationCheckPerformedKey];
+
+  [GIDAuthStateMigration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
+                                        callbackPath:kCallbackPath
+                                        keychainName:kKeychainName
+                                      isFreshInstall:NO];
+}
+
+- (void)testMigrateIfNeeded_HasPreviousMigration {
+  [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
+  [[[_mockUserDefaults expect] andReturnValue:@YES]
+      boolForKey:kMigrationCheckPerformedKey];
+
+  [GIDAuthStateMigration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
+                                        callbackPath:kCallbackPath
+                                        keychainName:kKeychainName
+                                      isFreshInstall:NO];
+}
+
+- (void)testMigrateIfNeeded_KeychainFailure {
+  [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
+  [[[_mockUserDefaults expect] andReturnValue:@NO]
+      boolForKey:kMigrationCheckPerformedKey];
+  [[[_mockGIDAuthStateMigration expect] andReturn:_mockGTMAppAuthFetcherAuthorization]
+      extractAuthorizationWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath];
+  [[[_mockGTMAppAuthFetcherAuthorization expect] andReturnValue:[NSNumber numberWithBool:NO]]
+      saveAuthorization:_mockGTMAppAuthFetcherAuthorization toKeychainForName:kKeychainName];
+
+  [GIDAuthStateMigration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
+                                        callbackPath:kCallbackPath
+                                        keychainName:kKeychainName
+                                      isFreshInstall:NO];
+}
+
+- (void)testMigrateIfNeeded_isFreshInstall {
+  [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
+  [[[_mockUserDefaults expect] andReturnValue:@NO]
+      boolForKey:kMigrationCheckPerformedKey];
+  [[_mockUserDefaults expect] setBool:YES forKey:kMigrationCheckPerformedKey];
+
+  [GIDAuthStateMigration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
+                                        callbackPath:kCallbackPath
+                                        keychainName:kKeychainName
+                                      isFreshInstall:YES];
+}
+
+- (void)testExtractAuthorization {
+  [self extractAuthorizationWithFingerprint:kSavedFingerprint];
+}
+
+- (void)testExtractAuthorization_HostedDomain {
+  [self extractAuthorizationWithFingerprint:kSavedFingerprint_HostedDomain];
+}
+
+#pragma mark - Helpers
+
+// Generate the service name for the stored additional token request parameters string.
+- (NSString *)additionalTokenRequestParametersKeyFromFingerprint:(NSString *)fingerprint {
+  return [NSString stringWithFormat:@"%@%@", fingerprint, kAdditionalTokenRequestParametersPostfix];
+}
+
+// The parameterized extractAuthorization test.
+- (void)extractAuthorizationWithFingerprint:(NSString *)fingerprint {
+  [[[_mockGIDAuthStateMigration expect] andReturn:fingerprint]
+      passwordForService:kFingerprintService];
+  [[[_mockGTMKeychain expect] andReturn:kGTMOAuth2PersistenceString]
+      passwordFromKeychainForName:fingerprint];
+  [[[_mockNSBundle expect] andReturn:_mockNSBundle] mainBundle];
+  [[[_mockNSBundle expect] andReturn:kBundleID] bundleIdentifier];
+  [[[_mockGIDSignInCallbackSchemes expect] andReturn:_mockGIDSignInCallbackSchemes] alloc];
+  (void)[[[_mockGIDSignInCallbackSchemes expect] andReturn:_mockGIDSignInCallbackSchemes]
+      initWithClientIdentifier:kClientID];
+  [[[_mockGIDSignInCallbackSchemes expect] andReturn:kDotReversedClientID] clientIdentifierScheme];
+  [[[_mockGIDAuthStateMigration expect] andReturn:kAdditionalTokenRequestParameters]
+      passwordForService:[self additionalTokenRequestParametersKeyFromFingerprint:fingerprint]];
+  [[[_mockGTMOAuth2KeychainCompatibility expect] andReturn:_mockGTMAppAuthFetcherAuthorization]
+      authorizeFromPersistenceString:kFinalPersistenceString
+                            tokenURL:[NSURL URLWithString:kTokenURL]
+                         redirectURI:kRedirectURI
+                            clientID:kClientID
+                        clientSecret:nil];
+
+  GTMAppAuthFetcherAuthorization *authorization =
+      [GIDAuthStateMigration extractAuthorizationWithTokenURL:[NSURL URLWithString:kTokenURL]
+                                                 callbackPath:kCallbackPath];
+
+  XCTAssertNotNil(authorization);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 25 - 0
GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.h

@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 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/Public/GoogleSignIn/GIDAuthentication.h"
+
+@interface GIDAuthentication (Testing)
+
+- (BOOL)isEqual:(id)object;
+- (BOOL)isEqualToAuthentication:(GIDAuthentication *)other;
+- (NSUInteger)hash;
+
+@end

+ 46 - 0
GoogleSignIn/Tests/Unit/GIDAuthentication+Testing.m

@@ -0,0 +1,46 @@
+// Copyright 2021 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/Tests/Unit/GIDAuthentication+Testing.h"
+
+@implementation GIDAuthentication (Testing)
+
+- (BOOL)isEqual:(id)object {
+  if (self == object) {
+    return YES;
+  }
+  if (![object isKindOfClass:[GIDAuthentication class]]) {
+    return NO;
+  }
+  return [self isEqualToAuthentication:(GIDAuthentication *)object];
+}
+
+- (BOOL)isEqualToAuthentication:(GIDAuthentication *)other {
+  return [self.clientID isEqual:other.clientID] &&
+      [self.accessToken isEqual:other.accessToken] &&
+      [self.accessTokenExpirationDate isEqual:other.accessTokenExpirationDate] &&
+      [self.refreshToken isEqual:other.refreshToken] &&
+      (self.idToken == other.idToken || [self.idToken isEqual:other.idToken]) &&
+      (self.idTokenExpirationDate == other.idTokenExpirationDate ||
+          [self.idTokenExpirationDate isEqual:other.idTokenExpirationDate]);
+}
+
+// Not the hash implemention you want to use on prod, but just to match |isEqual:| here.
+- (NSUInteger)hash {
+  return [self.clientID hash] ^ [self.accessToken hash] ^ [self.accessTokenExpirationDate hash] ^
+      [self.refreshToken hash] ^ [self.idToken hash] ^ [self.idTokenExpirationDate hash];
+}
+
+@end
+

+ 636 - 0
GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m

@@ -0,0 +1,636 @@
+// Copyright 2021 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/XCTest.h>
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+
+#import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
+#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+#import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
+#import "GoogleSignIn/Sources/GIDSignInPreferences.h"
+#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDTokenRequest+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
+
+#import <AppAuth/OIDAuthState.h>
+#import <AppAuth/OIDAuthorizationRequest.h>
+#import <AppAuth/OIDAuthorizationResponse.h>
+#import <AppAuth/OIDAuthorizationService.h>
+#import <AppAuth/OIDError.h>
+#import <AppAuth/OIDIDToken.h>
+#import <AppAuth/OIDServiceConfiguration.h>
+#import <AppAuth/OIDTokenRequest.h>
+#import <AppAuth/OIDTokenResponse.h>
+#import <GoogleUtilities/GULSwizzler.h>
+#import <GoogleUtilities/GULSwizzler+Unswizzle.h>
+#import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#import <OCMock/OCMock.h>
+
+static NSString *const kClientID = @"87654321.googleusercontent.com";
+static NSString *const kNewAccessToken = @"new_access_token";
+static NSString *const kUserEmail = @"foo@gmail.com";
+static NSTimeInterval const kExpireTime = 442886117;
+static NSTimeInterval const kNewExpireTime = 442886123;
+static NSTimeInterval const kNewExpireTime2 = 442886124;
+
+static NSTimeInterval const kTimeAccuracy = 10;
+
+// The system name in old iOS versions.
+static NSString *const kOldIOSName = @"iPhone OS";
+
+// The system name in new iOS versions.
+static NSString *const kNewIOSName = @"iOS";
+
+// List of observed properties of the class being tested.
+static NSString *const kObservedProperties[] = {
+  @"accessToken",
+  @"accessTokenExpirationDate",
+  @"idToken",
+  @"idTokenExpirationDate"
+};
+static const NSUInteger kNumberOfObservedProperties =
+    sizeof(kObservedProperties) / sizeof(*kObservedProperties);
+
+// Bit position for notification change type bitmask flags.
+// Must match the list of observed properties above.
+typedef NS_ENUM(NSUInteger, ChangeType) {
+  kChangeTypeAccessTokenPrior,
+  kChangeTypeAccessToken,
+  kChangeTypeAccessTokenExpirationDatePrior,
+  kChangeTypeAccessTokenExpirationDate,
+  kChangeTypeIDTokenPrior,
+  kChangeTypeIDToken,
+  kChangeTypeIDTokenExpirationDatePrior,
+  kChangeTypeIDTokenExpirationDate,
+  kChangeTypeEnd  // not a real change type but an end mark for calculating |kChangeAll|
+};
+
+static const NSUInteger kChangeNone = 0u;
+static const NSUInteger kChangeAll = (1u << kChangeTypeEnd) - 1u;
+
+#if __has_feature(c_static_assert) || __has_extension(c_static_assert)
+_Static_assert(kChangeTypeEnd == (sizeof(kObservedProperties) / sizeof(*kObservedProperties)) * 2,
+               "List of observed properties must match list of change notification enums");
+#endif
+
+@interface GIDAuthenticationTest : XCTestCase
+@end
+
+@implementation GIDAuthenticationTest {
+  // Whether the auth object has ID token or not.
+  BOOL _hasIDToken;
+
+  // Fake data used to generate the expiration date of the access token.
+  NSTimeInterval _accessTokenExpireTime;
+
+  // Fake data used to generate the expiration date of the ID token.
+  NSTimeInterval _idTokenExpireTime;
+
+  // Fake data used to generate the additional token request parameters.
+  NSDictionary *_additionalTokenRequestParameters;
+
+  // The saved token fetch handler.
+  OIDTokenCallback _tokenFetchHandler;
+
+  // The saved token request.
+  OIDTokenRequest *_tokenRequest;
+
+  // All GIDAuthentication objects that are observed.
+  NSMutableArray *_observedAuths;
+
+  // Bitmask flags for observed changes, as specified in |ChangeType|.
+  NSUInteger _changesObserved;
+
+  // The fake system name used for testing.
+  NSString *_fakeSystemName;
+}
+
+- (void)setUp {
+  _hasIDToken = YES;
+  _accessTokenExpireTime = kAccessTokenExpiresIn;
+  _idTokenExpireTime = kExpireTime;
+  _additionalTokenRequestParameters = nil;
+  _tokenFetchHandler = nil;
+  _tokenRequest = nil;
+  [GULSwizzler swizzleClass:[OIDAuthorizationService class]
+                   selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:)
+            isClassSelector:YES
+                  withBlock:^(id sender,
+                              OIDTokenRequest *request,
+                              OIDAuthorizationResponse *authorizationResponse,
+                              OIDTokenCallback callback) {
+    XCTAssertNotNil(authorizationResponse.request.clientID);
+    XCTAssertNotNil(authorizationResponse.request.configuration.tokenEndpoint);
+    XCTAssertNil(_tokenFetchHandler);  // only one on-going fetch allowed
+    _tokenFetchHandler = [callback copy];
+    _tokenRequest = [request copy];
+    return nil;
+  }];
+  _observedAuths = [[NSMutableArray alloc] init];
+  _changesObserved = 0;
+  _fakeSystemName = kNewIOSName;
+  [GULSwizzler swizzleClass:[UIDevice class]
+                   selector:@selector(systemName)
+            isClassSelector:NO
+                  withBlock:^(id sender) { return _fakeSystemName; }];
+}
+
+- (void)tearDown {
+  [GULSwizzler unswizzleClass:[OIDAuthorizationService class]
+                     selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:)
+              isClassSelector:YES];
+  [GULSwizzler unswizzleClass:[UIDevice class]
+                     selector:@selector(systemName)
+              isClassSelector:NO];
+  for (GIDAuthentication *auth in _observedAuths) {
+    for (unsigned int i = 0; i < kNumberOfObservedProperties; ++i) {
+      [auth removeObserver:self forKeyPath:kObservedProperties[i]];
+    }
+  }
+  _observedAuths = nil;
+}
+
+#pragma mark - Tests
+
+- (void)testInitWithAuthState {
+  OIDAuthState *authState = [OIDAuthState testInstance];
+  GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState];
+
+  XCTAssertEqualObjects(auth.clientID, authState.lastAuthorizationResponse.request.clientID);
+  XCTAssertEqualObjects(auth.accessToken, authState.lastTokenResponse.accessToken);
+  XCTAssertEqualObjects(auth.accessTokenExpirationDate,
+                        authState.lastTokenResponse.accessTokenExpirationDate);
+  XCTAssertEqualObjects(auth.refreshToken, authState.refreshToken);
+  XCTAssertEqualObjects(auth.idToken, authState.lastTokenResponse.idToken);
+  OIDIDToken *idToken = [[OIDIDToken alloc]
+      initWithIDTokenString:authState.lastTokenResponse.idToken];
+  XCTAssertEqualObjects(auth.idTokenExpirationDate, [idToken expiresAt]);
+}
+
+- (void)testInitWithAuthStateNoIDToken {
+  OIDAuthState *authState = [OIDAuthState testInstanceWithIDToken:nil];
+  GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState];
+
+  XCTAssertEqualObjects(auth.clientID, authState.lastAuthorizationResponse.request.clientID);
+  XCTAssertEqualObjects(auth.accessToken, authState.lastTokenResponse.accessToken);
+  XCTAssertEqualObjects(auth.accessTokenExpirationDate,
+                        authState.lastTokenResponse.accessTokenExpirationDate);
+  XCTAssertEqualObjects(auth.refreshToken, authState.refreshToken);
+  XCTAssertNil(auth.idToken);
+  XCTAssertNil(auth.idTokenExpirationDate);
+}
+
+- (void)testAuthState {
+  OIDAuthState *authState = [OIDAuthState testInstance];
+  GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState];
+  OIDAuthState *authStateReturned = auth.authState;
+
+  XCTAssertEqual(authState, authStateReturned);
+}
+
+- (void)testCoding {
+  GIDAuthentication *auth = [self auth];
+  NSData *data = [NSKeyedArchiver archivedDataWithRootObject:auth];
+  GIDAuthentication *newAuth = [NSKeyedUnarchiver unarchiveObjectWithData:data];
+  XCTAssertEqualObjects(auth, newAuth);
+  XCTAssertTrue([GIDAuthentication supportsSecureCoding]);
+}
+
+- (void)testFetcherAuthorizer {
+  // This is really hard to test without assuming how GTMAppAuthFetcherAuthorization works
+  // internally, so let's just take the shortcut here by asserting we get a
+  // GTMAppAuthFetcherAuthorization object.
+  GIDAuthentication *auth = [self auth];
+  id<GTMFetcherAuthorizationProtocol> fetcherAuthroizer = auth.fetcherAuthorizer;
+  XCTAssertTrue([fetcherAuthroizer isKindOfClass:[GTMAppAuthFetcherAuthorization class]]);
+  XCTAssertTrue([fetcherAuthroizer canAuthorize]);
+}
+
+- (void)testDoWithFreshTokensWithBothExpired {
+  // Both tokens expired 10 seconds ago.
+  [self setExpireTimeForAccessToken:-10 IDToken:-10];
+  [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
+}
+
+- (void)testDoWithFreshTokensWithAccessTokenExpired {
+  // Access token expired 10 seconds ago while ID token to expire in 10 minutes.
+  [self setExpireTimeForAccessToken:-10 IDToken:10 * 60];
+  [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
+}
+
+- (void)testDoWithFreshTokensWithIDTokenToExpire {
+  // Access token to expire in 10 minutes while ID token to expire in 10 seconds.
+  [self setExpireTimeForAccessToken:10 * 60 IDToken:10];
+  [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
+}
+
+- (void)testDoWithFreshTokensWithBothFresh {
+  // Both tokens to expire in 10 minutes.
+  [self setExpireTimeForAccessToken:10 * 60 IDToken:10 * 60];
+  [self verifyTokensNotRefreshedWithMethod:@selector(doWithFreshTokens:)];
+}
+
+- (void)testDoWithFreshTokensWithAccessTokenExpiredAndNoIDToken {
+  _hasIDToken = NO;
+  [self setExpireTimeForAccessToken:-10 IDToken:10 * 60];  // access token expired 10 seconds ago
+  [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
+}
+
+- (void)testDoWithFreshTokensWithAccessTokenFreshAndNoIDToken {
+  _hasIDToken = NO;
+  [self setExpireTimeForAccessToken:10 * 60 IDToken:-10];  // access token to expire in 10 minutes
+  [self verifyTokensNotRefreshedWithMethod:@selector(doWithFreshTokens:)];
+}
+
+- (void)testDoWithFreshTokensError {
+  [self setTokensExpireTime:-10];  // expired 10 seconds ago
+  GIDAuthentication *auth = [self observedAuth];
+  __block BOOL callbackCalled = NO;
+  [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
+    callbackCalled = YES;
+    XCTAssertNil(authentication);
+    XCTAssertNotNil(error);
+  }];
+  _tokenFetchHandler(nil, [self fakeError]);
+  XCTAssertTrue(callbackCalled);
+  [self assertOldTokensInAuth:auth];
+}
+
+- (void)testDoWithFreshTokensQueue {
+  GIDAuthentication *auth = [self observedAuth];
+  __block BOOL firstCallbackCalled = NO;
+  [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
+    firstCallbackCalled = YES;
+    [self assertNewTokensInAuth:authentication];
+    XCTAssertNil(error);
+  }];
+  __block BOOL secondCallbackCalled = NO;
+  [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
+    secondCallbackCalled = YES;
+    [self assertNewTokensInAuth:authentication];
+    XCTAssertNil(error);
+  }];
+  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
+  XCTAssertTrue(firstCallbackCalled);
+  XCTAssertTrue(secondCallbackCalled);
+  [self assertNewTokensInAuth:auth];
+}
+
+#pragma mark - EMM Support
+
+- (void)testEMMSupport {
+  _additionalTokenRequestParameters = @{
+    @"emm_support" : @"xyz",
+  };
+  GIDAuthentication *auth = [self auth];
+  [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
+                            NSError * _Nullable error) {}];
+  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
+  NSDictionary *expectedParameters = @{
+    @"emm_support" : @"xyz",
+    @"device_os" : [NSString stringWithFormat:@"%@ %@",
+        _fakeSystemName, [UIDevice currentDevice].systemVersion],
+    kSDKVersionLoggingParameter : GIDVersion(),
+  };
+  XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
+                        expectedParameters);
+}
+
+- (void)testSystemNameNormalization {
+  _fakeSystemName = kOldIOSName;
+  _additionalTokenRequestParameters = @{
+    @"emm_support" : @"xyz",
+  };
+  GIDAuthentication *auth = [self auth];
+  [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
+                            NSError * _Nullable error) {}];
+  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
+  NSDictionary *expectedParameters = @{
+    @"emm_support" : @"xyz",
+    @"device_os" : [NSString stringWithFormat:@"%@ %@",
+        kNewIOSName, [UIDevice currentDevice].systemVersion],
+    kSDKVersionLoggingParameter : GIDVersion(),
+  };
+  XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
+                        expectedParameters);
+}
+
+- (void)testEMMPasscodeInfo {
+  _additionalTokenRequestParameters = @{
+    @"emm_support" : @"xyz",
+    @"device_os" : @"old one",
+    @"emm_passcode_info" : @"something",
+  };
+  GIDAuthentication *auth = [self auth];
+  [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
+                            NSError * _Nullable error) {}];
+  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
+  NSDictionary *expectedParameters = @{
+    @"emm_support" : @"xyz",
+    @"device_os" : [NSString stringWithFormat:@"%@ %@",
+        _fakeSystemName, [UIDevice currentDevice].systemVersion],
+    @"emm_passcode_info" : [GIDMDMPasscodeState passcodeState].info,
+    kSDKVersionLoggingParameter : GIDVersion(),
+  };
+  XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
+                        expectedParameters);
+}
+
+- (void)testEMMError {
+  // Set expectations.
+  NSDictionary *errorJSON = @{ @"error" : @"EMM Specific Error" };
+  NSError *emmError = [NSError errorWithDomain:@"anydomain"
+                                          code:12345
+                                      userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
+  id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
+  [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
+  __block void (^completion)(void);
+  [[[mockEMMErrorHandler expect] andReturnValue:@YES]
+      handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
+    completion = arg;
+    return YES;
+  }]];
+
+  // Start testing.
+  _additionalTokenRequestParameters = @{
+    @"emm_support" : @"xyz",
+  };
+  GIDAuthentication *auth = [self auth];
+  __block BOOL callbackCalled = NO;
+  [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
+    callbackCalled = YES;
+    XCTAssertNil(authentication);
+    XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain);
+    XCTAssertEqual(error.code, kGIDSignInErrorCodeEMM);
+  }];
+  _tokenFetchHandler(nil, emmError);
+
+  // Verify and clean up.
+  [mockEMMErrorHandler verify];
+  [mockEMMErrorHandler stopMocking];
+  XCTAssertFalse(callbackCalled);
+  completion();
+  XCTAssertTrue(callbackCalled);
+  [self assertOldTokensInAuth:auth];
+}
+
+- (void)testNonEMMError {
+  // Set expectations.
+  NSDictionary *errorJSON = @{ @"error" : @"Not EMM Specific Error" };
+  NSError *emmError = [NSError errorWithDomain:@"anydomain"
+                                          code:12345
+                                      userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
+  id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
+  [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
+  __block void (^completion)(void);
+  [[[mockEMMErrorHandler expect] andReturnValue:@NO]
+      handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
+    completion = arg;
+    return YES;
+  }]];
+
+  // Start testing.
+  _additionalTokenRequestParameters = @{
+    @"emm_support" : @"xyz",
+  };
+  GIDAuthentication *auth = [self auth];
+  __block BOOL callbackCalled = NO;
+  [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
+    callbackCalled = YES;
+    XCTAssertNil(authentication);
+    XCTAssertEqualObjects(error.domain, @"anydomain");
+    XCTAssertEqual(error.code, 12345);
+  }];
+  _tokenFetchHandler(nil, emmError);
+
+  // Verify and clean up.
+  [mockEMMErrorHandler verify];
+  [mockEMMErrorHandler stopMocking];
+  XCTAssertFalse(callbackCalled);
+  completion();
+  XCTAssertTrue(callbackCalled);
+  [self assertOldTokensInAuth:auth];
+}
+
+- (void)testCodingPreserveEMMParameters {
+  _additionalTokenRequestParameters = @{
+    @"emm_support" : @"xyz",
+    @"device_os" : @"old one",
+    @"emm_passcode_info" : @"something",
+  };
+  NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[self auth]];
+  GIDAuthentication *auth = [NSKeyedUnarchiver unarchiveObjectWithData:data];
+  [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
+                            NSError * _Nullable error) {}];
+  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
+  NSDictionary *expectedParameters = @{
+    @"emm_support" : @"xyz",
+    @"device_os" : [NSString stringWithFormat:@"%@ %@",
+        [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion],
+    @"emm_passcode_info" : [GIDMDMPasscodeState passcodeState].info,
+    kSDKVersionLoggingParameter : GIDVersion(),
+  };
+  XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
+                        expectedParameters);
+}
+
+#pragma mark - NSKeyValueObserving
+
+- (void)observeValueForKeyPath:(NSString *)keyPath
+                      ofObject:(id)object
+                        change:(NSDictionary *)change
+                       context:(void *)context {
+  GIDAuthentication *auth = (GIDAuthentication *)object;
+  ChangeType changeType;
+  if ([keyPath isEqualToString:@"accessToken"]) {
+    if (change[NSKeyValueChangeNotificationIsPriorKey]) {
+      XCTAssertEqualObjects(auth.accessToken, kAccessToken);
+      changeType = kChangeTypeAccessTokenPrior;
+    } else {
+      XCTAssertEqualObjects(auth.accessToken, kNewAccessToken);
+      changeType = kChangeTypeAccessToken;
+    }
+  } else if ([keyPath isEqualToString:@"accessTokenExpirationDate"]) {
+    if (change[NSKeyValueChangeNotificationIsPriorKey]) {
+      [self assertDate:auth.accessTokenExpirationDate equalTime:_accessTokenExpireTime];
+      changeType = kChangeTypeAccessTokenExpirationDatePrior;
+    } else {
+      [self assertDate:auth.accessTokenExpirationDate equalTime:kNewExpireTime];
+      changeType = kChangeTypeAccessTokenExpirationDate;
+    }
+  } else if ([keyPath isEqualToString:@"idToken"]) {
+    if (change[NSKeyValueChangeNotificationIsPriorKey]) {
+      XCTAssertEqualObjects(auth.idToken, [self idToken]);
+      changeType = kChangeTypeIDTokenPrior;
+    } else {
+      XCTAssertEqualObjects(auth.idToken, [self idTokenNew]);
+      changeType = kChangeTypeIDToken;
+    }
+  } else if ([keyPath isEqualToString:@"idTokenExpirationDate"]) {
+    if (change[NSKeyValueChangeNotificationIsPriorKey]) {
+      if (_hasIDToken) {
+        [self assertDate:auth.idTokenExpirationDate equalTime:_idTokenExpireTime];
+      }
+      changeType = kChangeTypeIDTokenExpirationDatePrior;
+    } else {
+      if (_hasIDToken) {
+        [self assertDate:auth.idTokenExpirationDate equalTime:kNewExpireTime2];
+      }
+      changeType = kChangeTypeIDTokenExpirationDate;
+    }
+  } else {
+    XCTFail(@"unexpected keyPath");
+    return;  // so compiler knows |changeType| is always assigned
+  }
+  NSUInteger changeMask = 1u << changeType;
+  XCTAssertFalse(_changesObserved & changeMask);  // each change type should only fire once
+  _changesObserved |= changeMask;
+}
+
+#pragma mark - Helpers
+
+- (GIDAuthentication *)auth {
+  NSString *idToken = [self idToken];
+  NSNumber *accessTokenExpiresIn =
+      @(_accessTokenExpireTime - [[NSDate date] timeIntervalSinceReferenceDate]);
+  OIDTokenRequest *tokenRequest =
+      [OIDTokenRequest testInstanceWithAdditionalParameters:_additionalTokenRequestParameters];
+  OIDTokenResponse *tokenResponse =
+      [OIDTokenResponse testInstanceWithIDToken:idToken
+                                    accessToken:kAccessToken
+                                      expiresIn:accessTokenExpiresIn
+                                   tokenRequest:tokenRequest];
+  return [[GIDAuthentication alloc]
+      initWithAuthState:[OIDAuthState testInstanceWithTokenResponse:tokenResponse]];
+}
+
+- (NSString *)idTokenWithExpireTime:(NSTimeInterval)expireTime {
+  if (!_hasIDToken) {
+    return nil;
+  }
+  return [OIDTokenResponse idTokenWithSub:kUserID exp:@(expireTime + NSTimeIntervalSince1970)];
+}
+
+- (NSString *)idToken {
+  return [self idTokenWithExpireTime:_idTokenExpireTime];
+}
+
+- (NSString *)idTokenNew {
+  return [self idTokenWithExpireTime:kNewExpireTime2];
+}
+
+// Return the auth object that has certain property changes observed.
+- (GIDAuthentication *)observedAuth {
+  GIDAuthentication *auth = [self auth];
+  for (unsigned int i = 0; i < kNumberOfObservedProperties; ++i) {
+    [auth addObserver:self
+           forKeyPath:kObservedProperties[i]
+              options:NSKeyValueObservingOptionPrior
+              context:NULL];
+  }
+  [_observedAuths addObject:auth];
+  return auth;
+}
+
+- (OIDTokenResponse *)tokenResponseWithNewTokens {
+  NSNumber *expiresIn = @(kNewExpireTime - [NSDate timeIntervalSinceReferenceDate]);
+  return [OIDTokenResponse testInstanceWithIDToken:(_hasIDToken ? [self idTokenNew] : nil)
+                                       accessToken:kNewAccessToken
+                                         expiresIn:expiresIn
+                                      tokenRequest:_tokenRequest ?: nil];
+}
+
+- (NSError *)fakeError {
+  return [NSError errorWithDomain:@"fake.domain" code:-1 userInfo:nil];
+}
+
+- (void)assertDate:(NSDate *)date equalTime:(NSTimeInterval)time {
+  XCTAssertEqualWithAccuracy([date timeIntervalSinceReferenceDate], time, kTimeAccuracy);
+}
+
+- (void)assertOldAccessTokenInAuth:(GIDAuthentication *)auth {
+  XCTAssertEqualObjects(auth.accessToken, kAccessToken);
+  [self assertDate:auth.accessTokenExpirationDate equalTime:_accessTokenExpireTime];
+  XCTAssertEqual(_changesObserved, kChangeNone);
+}
+
+- (void)assertNewAccessTokenInAuth:(GIDAuthentication *)auth {
+  XCTAssertEqualObjects(auth.accessToken, kNewAccessToken);
+  [self assertDate:auth.accessTokenExpirationDate equalTime:kNewExpireTime];
+  XCTAssertEqual(_changesObserved, kChangeAll);
+}
+
+- (void)assertOldTokensInAuth:(GIDAuthentication *)auth {
+  [self assertOldAccessTokenInAuth:auth];
+  XCTAssertEqualObjects(auth.idToken, [self idToken]);
+  if (_hasIDToken) {
+    [self assertDate:auth.idTokenExpirationDate equalTime:_idTokenExpireTime];
+  }
+}
+
+- (void)assertNewTokensInAuth:(GIDAuthentication *)auth {
+  [self assertNewAccessTokenInAuth:auth];
+  XCTAssertEqualObjects(auth.idToken, [self idTokenNew]);
+  if (_hasIDToken) {
+    [self assertDate:auth.idTokenExpirationDate equalTime:kNewExpireTime2];
+  }
+}
+
+- (void)setTokensExpireTime:(NSTimeInterval)fromNow {
+  [self setExpireTimeForAccessToken:fromNow IDToken:fromNow];
+}
+
+- (void)setExpireTimeForAccessToken:(NSTimeInterval)accessExpire IDToken:(NSTimeInterval)idExpire {
+  _accessTokenExpireTime = [[NSDate date] timeIntervalSinceReferenceDate] + accessExpire;
+  _idTokenExpireTime = [[NSDate date] timeIntervalSinceReferenceDate] + idExpire;
+}
+
+- (void)verifyTokensRefreshedWithMethod:(SEL)sel {
+  GIDAuthentication *auth = [self observedAuth];
+  __block BOOL callbackCalled = NO;
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+  // We know the method doesn't return anything, so there is no risk of leaking.
+  [auth performSelector:sel withObject:^(GIDAuthentication *authentication, NSError *error) {
+#pragma clang diagnostic pop
+    callbackCalled = YES;
+    [self assertNewTokensInAuth:authentication];
+    XCTAssertNil(error);
+  }];
+  _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
+  XCTAssertTrue(callbackCalled);
+  [self assertNewTokensInAuth:auth];
+}
+
+- (void)verifyTokensNotRefreshedWithMethod:(SEL)sel {
+  GIDAuthentication *auth = [self observedAuth];
+  __block BOOL callbackCalled = NO;
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+  // We know the method doesn't return anything, so there is no risk of leaking.
+  [auth performSelector:sel withObject:^(GIDAuthentication *authentication, NSError *error) {
+#pragma clang diagnostic pop
+    callbackCalled = YES;
+    [self assertOldTokensInAuth:authentication];
+    XCTAssertNil(error);
+  }];
+  XCTAssertNil(_tokenFetchHandler);
+  XCTAssertTrue(callbackCalled);
+  [self assertOldTokensInAuth:auth];
+}
+
+@end

+ 33 - 0
GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.h

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 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/Public/GoogleSignIn/GIDConfiguration.h"
+
+#import "GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
+
+extern NSString *const kServerClientID;
+extern NSString *const kLoginHint;
+extern NSString *const kOpenIDRealm;
+
+@interface GIDConfiguration (Testing)
+
+- (BOOL)isEqual:(id)object;
+- (BOOL)isEqualToConfiguration:(GIDConfiguration *)other;
+
++ (instancetype)testInstance;
+
+@end

+ 60 - 0
GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.m

@@ -0,0 +1,60 @@
+// Copyright 2021 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/Tests/Unit/GIDConfiguration+Testing.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
+
+#import "GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
+
+NSString *const kServerClientID = @"fakeServerClientID";
+NSString *const kLoginHint = @"fakeLoginHint";
+NSString *const kOpenIDRealm = @"fakeOpenIDRealm";
+
+@implementation GIDConfiguration (Testing)
+
+// TODO(petea): consider moving -isEqual* to the base class and implementing -hash as well.
+- (BOOL)isEqual:(id)object {
+  if (self == object) {
+    return YES;
+  }
+  if (![object isKindOfClass:[GIDConfiguration class]]) {
+    return NO;
+  }
+  return [self isEqualToConfiguration:(GIDConfiguration *)object];
+}
+
+- (BOOL)isEqualToConfiguration:(GIDConfiguration *)other {
+  // Nullable properties get an extra check to cover the nil case.
+  return [self.clientID isEqual:other.clientID] &&
+      ([self.serverClientID isEqual:other.serverClientID] ||
+          self.serverClientID == other.serverClientID) &&
+      ([self.loginHint isEqual:other.loginHint] ||
+          self.loginHint == other.loginHint) &&
+      ([self.hostedDomain isEqual:other.hostedDomain] ||
+          self.hostedDomain == other.hostedDomain) &&
+      ([self.openIDRealm isEqual:other.openIDRealm] ||
+          self.openIDRealm == other.openIDRealm);
+}
+
++ (instancetype)testInstance {
+  return [[GIDConfiguration alloc] initWithClientID:OIDAuthorizationRequestTestingClientID
+                                     serverClientID:kServerClientID
+                                          loginHint:kLoginHint
+                                       hostedDomain:kHostedDomain
+                                        openIDRealm:kOpenIDRealm];
+}
+
+@end

+ 93 - 0
GoogleSignIn/Tests/Unit/GIDConfigurationTest.m

@@ -0,0 +1,93 @@
+// Copyright 2021 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/Public/GoogleSignIn/GIDConfiguration.h"
+
+#import <XCTest/XCTest.h>
+#import <objc/runtime.h>
+
+#import "GoogleSignIn/Tests/Unit/GIDConfiguration+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDAuthorizationRequest+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
+
+@interface GIDConfigurationTest : XCTestCase
+@end
+
+@implementation GIDConfigurationTest
+
+#pragma mark - Tests
+
+- (void)testClientIDInitializer {
+  GIDConfiguration *configuration =
+      [[GIDConfiguration alloc] initWithClientID:OIDAuthorizationRequestTestingClientID];
+  XCTAssertEqualObjects(configuration.clientID, OIDAuthorizationRequestTestingClientID);
+  XCTAssertNil(configuration.serverClientID);
+  XCTAssertNil(configuration.loginHint);
+  XCTAssertNil(configuration.hostedDomain);
+  XCTAssertNil(configuration.openIDRealm);
+}
+
+- (void)testClientIDServerIDInitializer {
+  GIDConfiguration *configuration =
+      [[GIDConfiguration alloc] initWithClientID:OIDAuthorizationRequestTestingClientID
+                                  serverClientID:kServerClientID];
+  XCTAssertEqualObjects(configuration.clientID, OIDAuthorizationRequestTestingClientID);
+  XCTAssertEqualObjects(configuration.serverClientID, kServerClientID);
+  XCTAssertNil(configuration.loginHint);
+  XCTAssertNil(configuration.hostedDomain);
+  XCTAssertNil(configuration.openIDRealm);
+}
+
+- (void)testFullInitializer {
+  GIDConfiguration *configuration = [GIDConfiguration testInstance];
+  XCTAssertEqualObjects(configuration.clientID, OIDAuthorizationRequestTestingClientID);
+  XCTAssertEqualObjects(configuration.serverClientID, kServerClientID);
+  XCTAssertEqualObjects(configuration.loginHint, kLoginHint);
+  XCTAssertEqualObjects(configuration.hostedDomain, kHostedDomain);
+  XCTAssertEqualObjects(configuration.openIDRealm, kOpenIDRealm);
+}
+
+- (void)testDescription {
+  GIDConfiguration *configuration = [GIDConfiguration testInstance];
+  NSString *propertyString = @"";
+  unsigned int outCount, c;
+  objc_property_t *properties = class_copyPropertyList([configuration class], &outCount);
+  NSString *propertyName;
+  for (c = 0; c < outCount; c++) {
+    propertyName = [NSString stringWithUTF8String:property_getName(properties[c])];
+    propertyString = [propertyString stringByAppendingFormat:@", %@: %@",
+        propertyName, [configuration valueForKey:propertyName]];
+  }
+  NSString *expectedDescription =
+      [NSString stringWithFormat:@"<GIDConfiguration: %p%@>", configuration, propertyString];
+
+  XCTAssertEqualObjects(configuration.description, expectedDescription);
+}
+
+- (void)testCopying {
+  GIDConfiguration *configuration = [GIDConfiguration testInstance];
+  GIDConfiguration *copiedConfiguration = [configuration copy];
+  // Should be the same object.
+  XCTAssertEqual(configuration, copiedConfiguration);
+}
+
+- (void)testCoding {
+  GIDConfiguration *configuration = [GIDConfiguration testInstance];
+  NSData *data = [NSKeyedArchiver archivedDataWithRootObject:configuration];
+  GIDConfiguration *newConfiguration = [NSKeyedUnarchiver unarchiveObjectWithData:data];
+  XCTAssertEqualObjects(configuration, newConfiguration);
+  XCTAssertTrue(GIDConfiguration.supportsSecureCoding);
+}
+
+@end

+ 459 - 0
GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m

@@ -0,0 +1,459 @@
+// Copyright 2021 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 <UIKit/UIKit.h>
+#import <XCTest/XCTest.h>
+
+#import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+#import "GoogleSignIn/Sources/GIDSignInStrings.h"
+
+#import <GoogleUtilities/GULSwizzler.h>
+#import <GoogleUtilities/GULSwizzler+Unswizzle.h>
+#import <OCMock/OCMock.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Addtional methods added to UIAlertAction for testing.
+@interface UIAlertAction (Testing)
+
+// Returns the handler block for this alert action.
+- (void (^)(UIAlertAction *))actionHandler;
+
+@end
+
+@implementation UIAlertAction (Testing)
+
+- (void (^)(UIAlertAction *))actionHandler {
+  return [self valueForKey:@"handler"];
+}
+
+@end
+
+// Unit test for GIDEMMErrorHandler.
+@interface GIDEMMErrorHandlerTest : XCTestCase
+@end
+
+@implementation GIDEMMErrorHandlerTest {
+  // Whether or not the current device runs on iOS 10.
+  BOOL _isIOS10;
+
+  // Whether key window has been set.
+  BOOL _keyWindowSet;
+
+  // The view controller that has been presented, if any.
+  UIViewController *_presentedViewController;
+}
+
+- (void)setUp {
+  [super setUp];
+  _isIOS10 = [UIDevice currentDevice].systemVersion.integerValue == 10;
+  _keyWindowSet = NO;
+  _presentedViewController = nil;
+  [GULSwizzler swizzleClass:[UIWindow class]
+                   selector:@selector(makeKeyAndVisible)
+            isClassSelector:NO
+                  withBlock:^() { _keyWindowSet = YES; }];
+  [GULSwizzler swizzleClass:[UIViewController class]
+                   selector:@selector(presentViewController:animated:completion:)
+            isClassSelector:NO
+                  withBlock:^(id obj, id arg1) { _presentedViewController = arg1; }];
+  [GULSwizzler swizzleClass:[GIDSignInStrings class]
+                   selector:@selector(localizedStringForKey:text:)
+            isClassSelector:YES
+                  withBlock:^(id obj, NSString *key, NSString *text) { return text; }];
+}
+
+- (void)tearDown {
+  [GULSwizzler unswizzleClass:[UIWindow class]
+                     selector:@selector(makeKeyAndVisible)
+              isClassSelector:NO];
+  [GULSwizzler unswizzleClass:[UIViewController class]
+                     selector:@selector(presentViewController:animated:completion:)
+              isClassSelector:NO];
+  [GULSwizzler unswizzleClass:[GIDSignInStrings class]
+                     selector:@selector(localizedStringForKey:text:)
+              isClassSelector:YES];
+  _presentedViewController = nil;
+  [super tearDown];
+}
+
+// Expects opening a particular URL string in performing an action.
+- (void)expectOpenURLString:(NSString *)urlString inAction:(void (^)())action {
+  // Swizzle and mock [UIApplication sharedApplication] since it is unavailable in unit tests.
+  id mockApplication = OCMStrictClassMock([UIApplication class]);
+  [GULSwizzler swizzleClass:[UIApplication class]
+                   selector:@selector(sharedApplication)
+            isClassSelector:YES
+                  withBlock:^() { return mockApplication; }];
+  [[mockApplication expect] openURL:[NSURL URLWithString:urlString]];
+  action();
+  [mockApplication verify];
+  [GULSwizzler unswizzleClass:[UIApplication class]
+                     selector:@selector(sharedApplication)
+              isClassSelector:YES];
+}
+
+// Verifies that the handler doesn't handle non-exist error.
+- (void)testNoError {
+  __block BOOL completionCalled = NO;
+  BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:@{ @"abc" : @123 }
+                                                                  completion:^() {
+    completionCalled = YES;
+  }];
+  XCTAssertFalse(result);
+  XCTAssertTrue(completionCalled);
+  XCTAssertFalse(_keyWindowSet);
+  XCTAssertNil(_presentedViewController);
+}
+
+// Verifies that the handler doesn't handle non-EMM error.
+- (void)testNoEMMError {
+  __block BOOL completionCalled = NO;
+  NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"invalid_token" };
+  BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
+                                                                  completion:^() {
+    completionCalled = YES;
+  }];
+  XCTAssertFalse(result);
+  XCTAssertTrue(completionCalled);
+  XCTAssertFalse(_keyWindowSet);
+  XCTAssertNil(_presentedViewController);
+}
+
+// Verifies that the handler handles general EMM error with user tapping 'OK'.
+- (void)testGeneralEMMErrorOK {
+  __block BOOL completionCalled = NO;
+  NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_something_wrong" };
+  BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
+                                                                  completion:^() {
+    completionCalled = YES;
+  }];
+  if (![UIAlertController class]) {
+    XCTAssertFalse(result);
+    XCTAssertTrue(completionCalled);
+    XCTAssertFalse(_keyWindowSet);
+    XCTAssertNil(_presentedViewController);
+    return;
+  }
+  XCTAssertTrue(result);
+  XCTAssertFalse(completionCalled);
+  XCTAssertFalse(_keyWindowSet);
+  XCTAssertNil(_presentedViewController);
+
+  // Should handle no more error while the previous one is being handled.
+  __block BOOL secondCompletionCalled = NO;
+  BOOL secondResult = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
+                                                                  completion:^() {
+    secondCompletionCalled = YES;
+  }];
+  XCTAssertFalse(secondResult);
+  XCTAssertTrue(secondCompletionCalled);
+  XCTAssertFalse(_keyWindowSet);
+  XCTAssertNil(_presentedViewController);
+
+  // Wait for the code under test to be executed on the main thread.
+  XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
+  dispatch_async(dispatch_get_main_queue(), ^() {
+    [expectation fulfill];
+  });
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+  XCTAssertFalse(completionCalled);
+  XCTAssertTrue(_keyWindowSet);
+  XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
+  UIAlertController *alert = (UIAlertController *)_presentedViewController;
+  XCTAssertNotNil(alert.title);
+  XCTAssertNotNil(alert.message);
+  XCTAssertEqual(alert.actions.count, 1);
+
+  // Pretend to touch the "OK" button.
+  UIAlertAction *action = alert.actions[0];
+  XCTAssertEqualObjects(action.title, @"OK");
+  action.actionHandler(action);
+  XCTAssertTrue(completionCalled);
+}
+
+// Verifies that the handler handles EMM screenlock required error with user tapping 'Cancel'.
+- (void)testScreenlockRequiredCancel {
+  if (_isIOS10) {
+    // The dialog is different on iOS 10.
+    return;
+  }
+  __block BOOL completionCalled = NO;
+  NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_passcode_required" };
+  BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
+                                                                  completion:^() {
+    completionCalled = YES;
+  }];
+  if (![UIAlertController class]) {
+    XCTAssertFalse(result);
+    XCTAssertTrue(completionCalled);
+    XCTAssertFalse(_keyWindowSet);
+    XCTAssertNil(_presentedViewController);
+    return;
+  }
+  XCTAssertTrue(result);
+  XCTAssertFalse(completionCalled);
+  XCTAssertFalse(_keyWindowSet);
+  XCTAssertNil(_presentedViewController);
+
+  // Wait for the code under test to be executed on the main thread.
+  XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
+  dispatch_async(dispatch_get_main_queue(), ^() {
+    [expectation fulfill];
+  });
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+  XCTAssertFalse(completionCalled);
+  XCTAssertTrue(_keyWindowSet);
+  XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
+  UIAlertController *alert = (UIAlertController *)_presentedViewController;
+  XCTAssertNotNil(alert.title);
+  XCTAssertNotNil(alert.message);
+  XCTAssertEqual(alert.actions.count, 2);
+
+  // Pretend to touch the "Cancel" button.
+  UIAlertAction *action = alert.actions[0];
+  XCTAssertEqualObjects(action.title, @"Cancel");
+  action.actionHandler(action);
+  XCTAssertTrue(completionCalled);
+}
+
+// Verifies that the handler handles EMM screenlock required error with user tapping 'Settings'.
+- (void)testScreenlockRequiredSettings {
+  if (_isIOS10) {
+    // The dialog is different on iOS 10.
+    return;
+  }
+  __block BOOL completionCalled = NO;
+  NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_passcode_required" };
+  BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
+                                                                  completion:^() {
+    completionCalled = YES;
+  }];
+  if (![UIAlertController class]) {
+    XCTAssertFalse(result);
+    XCTAssertTrue(completionCalled);
+    XCTAssertFalse(_keyWindowSet);
+    XCTAssertNil(_presentedViewController);
+    return;
+  }
+  XCTAssertTrue(result);
+  XCTAssertFalse(completionCalled);
+  XCTAssertFalse(_keyWindowSet);
+  XCTAssertNil(_presentedViewController);
+
+  // Wait for the code under test to be executed on the main thread.
+  XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
+  dispatch_async(dispatch_get_main_queue(), ^() {
+    [expectation fulfill];
+  });
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+  XCTAssertFalse(completionCalled);
+  XCTAssertTrue(_keyWindowSet);
+  XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
+  UIAlertController *alert = (UIAlertController *)_presentedViewController;
+  XCTAssertNotNil(alert.title);
+  XCTAssertNotNil(alert.message);
+  XCTAssertEqual(alert.actions.count, 2);
+
+  // Pretend to touch the "Settings" button.
+  UIAlertAction *action = alert.actions[1];
+  XCTAssertEqualObjects(action.title, @"Settings");
+  [self expectOpenURLString:UIApplicationOpenSettingsURLString inAction:^() {
+    action.actionHandler(action);
+  }];
+  XCTAssertTrue(completionCalled);
+}
+
+- (void)testScreenlockRequiredOkOnIOS10 {
+  if (!_isIOS10) {
+    // A more useful dialog is used for other iOS versions.
+    return;
+  }
+  __block BOOL completionCalled = NO;
+  NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_passcode_required" };
+  BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
+                                                                  completion:^() {
+    completionCalled = YES;
+  }];
+  if (![UIAlertController class]) {
+    XCTAssertFalse(result);
+    XCTAssertTrue(completionCalled);
+    XCTAssertFalse(_keyWindowSet);
+    XCTAssertNil(_presentedViewController);
+    return;
+  }
+  XCTAssertTrue(result);
+  XCTAssertFalse(completionCalled);
+  XCTAssertFalse(_keyWindowSet);
+  XCTAssertNil(_presentedViewController);
+
+  // Wait for the code under test to be executed on the main thread.
+  XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
+  dispatch_async(dispatch_get_main_queue(), ^() {
+    [expectation fulfill];
+  });
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+  XCTAssertFalse(completionCalled);
+  XCTAssertTrue(_keyWindowSet);
+  XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
+  UIAlertController *alert = (UIAlertController *)_presentedViewController;
+  XCTAssertNotNil(alert.title);
+  XCTAssertNotNil(alert.message);
+  XCTAssertEqual(alert.actions.count, 1);
+
+  // Pretend to touch the "OK" button.
+  UIAlertAction *action = alert.actions[0];
+  XCTAssertEqualObjects(action.title, @"OK");
+  action.actionHandler(action);
+  XCTAssertTrue(completionCalled);
+}
+
+// Verifies that the handler handles EMM app verification required error without a URL.
+- (void)testAppVerificationNoURL {
+  __block BOOL completionCalled = NO;
+  NSDictionary<NSString *, NSString *> *response = @{ @"error" : @"emm_app_verification_required" };
+  BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
+                                                                  completion:^() {
+    completionCalled = YES;
+  }];
+  if (![UIAlertController class]) {
+    XCTAssertFalse(result);
+    XCTAssertTrue(completionCalled);
+    XCTAssertFalse(_keyWindowSet);
+    XCTAssertNil(_presentedViewController);
+    return;
+  }
+  XCTAssertTrue(result);
+  XCTAssertFalse(completionCalled);
+  XCTAssertFalse(_keyWindowSet);
+  XCTAssertNil(_presentedViewController);
+
+  // Wait for the code under test to be executed on the main thread.
+  XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
+  dispatch_async(dispatch_get_main_queue(), ^() {
+    [expectation fulfill];
+  });
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+  XCTAssertFalse(completionCalled);
+  XCTAssertTrue(_keyWindowSet);
+  XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
+  UIAlertController *alert = (UIAlertController *)_presentedViewController;
+  XCTAssertNotNil(alert.title);
+  XCTAssertNotNil(alert.message);
+  XCTAssertEqual(alert.actions.count, 1);
+
+  // Pretend to touch the "OK" button.
+  UIAlertAction *action = alert.actions[0];
+  XCTAssertEqualObjects(action.title, @"OK");
+  action.actionHandler(action);
+  XCTAssertTrue(completionCalled);
+}
+
+// Verifies that the handler handles EMM app verification required error user tapping 'Cancel'.
+- (void)testAppVerificationCancel {
+  __block BOOL completionCalled = NO;
+  NSDictionary<NSString *, NSString *> *response =
+      @{ @"error" : @"emm_app_verification_required: https://host.domain/path" };
+  BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
+                                                                  completion:^() {
+    completionCalled = YES;
+  }];
+  if (![UIAlertController class]) {
+    XCTAssertFalse(result);
+    XCTAssertTrue(completionCalled);
+    XCTAssertFalse(_keyWindowSet);
+    XCTAssertNil(_presentedViewController);
+    return;
+  }
+  XCTAssertTrue(result);
+  XCTAssertFalse(completionCalled);
+  XCTAssertFalse(_keyWindowSet);
+  XCTAssertNil(_presentedViewController);
+
+  // Wait for the code under test to be executed on the main thread.
+  XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
+  dispatch_async(dispatch_get_main_queue(), ^() {
+    [expectation fulfill];
+  });
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+  XCTAssertFalse(completionCalled);
+  XCTAssertTrue(_keyWindowSet);
+  XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
+  UIAlertController *alert = (UIAlertController *)_presentedViewController;
+  XCTAssertNotNil(alert.title);
+  XCTAssertNotNil(alert.message);
+  XCTAssertEqual(alert.actions.count, 2);
+
+  // Pretend to touch the "Cancel" button.
+  UIAlertAction *action = alert.actions[0];
+  XCTAssertEqualObjects(action.title, @"Cancel");
+  action.actionHandler(action);
+  XCTAssertTrue(completionCalled);
+}
+
+// Verifies that the handler handles EMM app verification required error user tapping 'Connect'.
+- (void)testAppVerificationConnect {
+  __block BOOL completionCalled = NO;
+  NSDictionary<NSString *, NSString *> *response =
+      @{ @"error" : @"emm_app_verification_required: https://host.domain/path" };
+  BOOL result = [[GIDEMMErrorHandler sharedInstance] handleErrorFromResponse:response
+                                                                  completion:^() {
+    completionCalled = YES;
+  }];
+  if (![UIAlertController class]) {
+    XCTAssertFalse(result);
+    XCTAssertTrue(completionCalled);
+    XCTAssertFalse(_keyWindowSet);
+    XCTAssertNil(_presentedViewController);
+    return;
+  }
+  XCTAssertTrue(result);
+  XCTAssertFalse(completionCalled);
+  XCTAssertFalse(_keyWindowSet);
+  XCTAssertNil(_presentedViewController);
+
+  // Wait for the code under test to be executed on the main thread.
+  XCTestExpectation *expectation = [self expectationWithDescription:@"wait for main thread"];
+  dispatch_async(dispatch_get_main_queue(), ^() {
+    [expectation fulfill];
+  });
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+  XCTAssertFalse(completionCalled);
+  XCTAssertTrue(_keyWindowSet);
+  XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
+  UIAlertController *alert = (UIAlertController *)_presentedViewController;
+  XCTAssertNotNil(alert.title);
+  XCTAssertNotNil(alert.message);
+  XCTAssertEqual(alert.actions.count, 2);
+
+  // Pretend to touch the "Connect" button.
+  UIAlertAction *action = alert.actions[1];
+  XCTAssertEqualObjects(action.title, @"Connect");
+  [self expectOpenURLString:@"https://host.domain/path" inAction:^() {
+    action.actionHandler(action);
+  }];
+  XCTAssertTrue(completionCalled);
+}
+
+// Verifies that the handler can handle sequential errors independently.
+- (void)testSequentialErrors {
+  [self testGeneralEMMErrorOK];
+  _keyWindowSet = NO;
+  _presentedViewController = nil;
+  [self testScreenlockRequiredCancel];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 29 - 0
GoogleSignIn/Tests/Unit/GIDFakeFetcher.h

@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 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 <GTMSessionFetcher/GTMSessionFetcher.h>
+
+// A fake |GTMHTTPFetcher| for testing.
+@interface GIDFakeFetcher : GTMSessionFetcher
+
+// The URL of the fetching request.
+- (NSURL *)requestURL;
+
+// Emulates server returning with data and/or error.
+- (void)didFinishWithData:(NSData *)data error:(NSError *)error;
+
+- (instancetype)initWithRequest:(NSURLRequest *)request;
+@end

+ 54 - 0
GoogleSignIn/Tests/Unit/GIDFakeFetcher.m

@@ -0,0 +1,54 @@
+// Copyright 2021 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/Tests/Unit/GIDFakeFetcher.h"
+
+typedef void (^FetchCompletionHandler)(NSData *, NSError *);
+
+@implementation GIDFakeFetcher {
+  FetchCompletionHandler _handler;
+  NSURL *_requestURL;
+}
+
+- (instancetype)initWithRequest:(NSURLRequest *)request {
+  self = [super initWithRequest:request configuration:nil];
+  if (self) {
+    _requestURL = [[request URL] copy];
+  }
+  return self;
+}
+
+
+- (void)beginFetchWithDelegate:(id)delegate didFinishSelector:(SEL)finishedSEL {
+  [NSException raise:@"NotImplementedException" format:@"Implement this method if it is used"];
+}
+
+- (void)beginFetchWithCompletionHandler:(FetchCompletionHandler)handler {
+  if (_handler) {
+    [NSException raise:NSInvalidArgumentException format:@"Attempted start fetch again"];
+  }
+  _handler = [handler copy];
+}
+
+- (NSURL *)requestURL {
+  return _requestURL;
+}
+
+- (void)didFinishWithData:(NSData *)data error:(NSError *)error {
+  FetchCompletionHandler handler = _handler;
+  _handler = nil;
+  handler(data, error);
+}
+
+@end

+ 25 - 0
GoogleSignIn/Tests/Unit/GIDFakeFetcherService.h

@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 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 <GTMSessionFetcher/GTMSessionFetcher.h>
+
+// A fake |GTMHTTPFetcherService| for testing.
+@interface GIDFakeFetcherService : NSObject<GTMSessionFetcherServiceProtocol>
+
+// Returns the list of |GPPFakeFetcher| objects that have been created.
+- (NSArray *)fetchers;
+
+@end

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff