GIDEMMErrorHandler.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import <TargetConditionals.h>
  15. #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
  16. #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
  17. #import <UIKit/UIKit.h>
  18. #import "GoogleSignIn/Sources/GIDSignInStrings.h"
  19. NS_ASSUME_NONNULL_BEGIN
  20. // The error key in the server response.
  21. static NSString *const kErrorKey = @"error";
  22. // Error strings in the server response.
  23. static NSString *const kGeneralErrorPrefix = @"emm_";
  24. static NSString *const kScreenlockRequiredError = @"emm_passcode_required";
  25. static NSString *const kAppVerificationRequiredErrorPrefix = @"emm_app_verification_required";
  26. // Optional separator between error prefix and the payload.
  27. static NSString *const kErrorPayloadSeparator = @":";
  28. // A list for recognized error codes.
  29. typedef enum {
  30. ErrorCodeNone = 0,
  31. ErrorCodeDeviceNotCompliant,
  32. ErrorCodeScreenlockRequired,
  33. ErrorCodeAppVerificationRequired,
  34. } ErrorCode;
  35. @implementation GIDEMMErrorHandler {
  36. // Whether or not a dialog is pending user interaction.
  37. BOOL _pendingDialog;
  38. }
  39. + (instancetype)sharedInstance {
  40. static dispatch_once_t once;
  41. static GIDEMMErrorHandler *sharedInstance;
  42. dispatch_once(&once, ^{
  43. sharedInstance = [[self alloc] init];
  44. });
  45. return sharedInstance;
  46. }
  47. - (BOOL)handleErrorFromResponse:(NSDictionary<NSString *, id> *)response
  48. completion:(void (^)(void))completion {
  49. ErrorCode errorCode = ErrorCodeNone;
  50. NSURL *appVerificationURL;
  51. @synchronized(self) { // for accessing _pendingDialog
  52. if (!_pendingDialog && [UIAlertController class] &&
  53. [response isKindOfClass:[NSDictionary class]]) {
  54. id errorValue = response[kErrorKey];
  55. if ([errorValue isEqual:kScreenlockRequiredError]) {
  56. errorCode = ErrorCodeScreenlockRequired;
  57. } else if ([errorValue hasPrefix:kAppVerificationRequiredErrorPrefix]) {
  58. errorCode = ErrorCodeAppVerificationRequired;
  59. NSString *appVerificationString =
  60. [errorValue substringFromIndex:kAppVerificationRequiredErrorPrefix.length];
  61. if ([appVerificationString hasPrefix:kErrorPayloadSeparator]) {
  62. appVerificationString =
  63. [appVerificationString substringFromIndex:kErrorPayloadSeparator.length];
  64. }
  65. appVerificationString = [appVerificationString
  66. stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
  67. if (appVerificationString.length) {
  68. appVerificationURL = [NSURL URLWithString:appVerificationString];
  69. }
  70. } else if ([errorValue hasPrefix:kGeneralErrorPrefix]) {
  71. errorCode = ErrorCodeDeviceNotCompliant;
  72. }
  73. if (errorCode) {
  74. _pendingDialog = YES;
  75. }
  76. }
  77. }
  78. if (!errorCode) {
  79. completion();
  80. return NO;
  81. }
  82. // All UI must happen in the main thread.
  83. dispatch_async(dispatch_get_main_queue(), ^() {
  84. UIWindow *keyWindow = [self keyWindow];
  85. if (!keyWindow) {
  86. // Shouldn't happen, just in case.
  87. completion();
  88. return;
  89. }
  90. UIWindow *alertWindow;
  91. if (@available(iOS 13, *)) {
  92. if (keyWindow.windowScene) {
  93. alertWindow = [[UIWindow alloc] initWithWindowScene:keyWindow.windowScene];
  94. }
  95. }
  96. if (!alertWindow) {
  97. CGRect keyWindowBounds = CGRectIsEmpty(keyWindow.bounds) ?
  98. keyWindow.bounds : [UIScreen mainScreen].bounds;
  99. alertWindow = [[UIWindow alloc] initWithFrame:keyWindowBounds];
  100. }
  101. alertWindow.backgroundColor = [UIColor clearColor];
  102. alertWindow.rootViewController = [[UIViewController alloc] init];
  103. alertWindow.rootViewController.view.backgroundColor = [UIColor clearColor];
  104. alertWindow.windowLevel = UIWindowLevelAlert;
  105. [alertWindow makeKeyAndVisible];
  106. void (^finish)(void) = ^{
  107. alertWindow.hidden = YES;
  108. alertWindow.rootViewController = nil;
  109. [keyWindow makeKeyAndVisible];
  110. self->_pendingDialog = NO;
  111. completion();
  112. };
  113. UIAlertController *alert;
  114. switch (errorCode) {
  115. case ErrorCodeNone:
  116. break;
  117. case ErrorCodeScreenlockRequired:
  118. alert = [self passcodeRequiredAlertWithCompletion:finish];
  119. break;
  120. case ErrorCodeAppVerificationRequired:
  121. alert = [self appVerificationRequiredAlertWithURL:appVerificationURL completion:finish];
  122. break;
  123. case ErrorCodeDeviceNotCompliant:
  124. alert = [self deviceNotCompliantAlertWithCompletion:finish];
  125. break;
  126. }
  127. if (alert) {
  128. [alertWindow.rootViewController presentViewController:alert animated:YES completion:nil];
  129. } else {
  130. // Should not happen but just in case.
  131. finish();
  132. }
  133. });
  134. return YES;
  135. }
  136. // This method is exposed to the unit test.
  137. - (nullable UIWindow *)keyWindow {
  138. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
  139. if (@available(iOS 15, *)) {
  140. printf("keywindow - 15\n");
  141. for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
  142. if ([scene isKindOfClass:[UIWindowScene class]] &&
  143. scene.activationState == UISceneActivationStateForegroundActive) {
  144. return ((UIWindowScene *)scene).keyWindow;
  145. }
  146. }
  147. } else
  148. #endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
  149. {
  150. #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_15_0
  151. if (@available(iOS 13, *)) {
  152. printf("keywindow - 13\n");
  153. for (UIWindow *window in UIApplication.sharedApplication.windows) {
  154. printf("keywindow - checking window\n");
  155. if (window.keyWindow) {
  156. printf("keywindow - window found\n");
  157. return window;
  158. }
  159. }
  160. } else {
  161. #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_13_0
  162. printf("keywindow - fallback\n");
  163. return UIApplication.sharedApplication.keyWindow;
  164. #endif // __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_13_0
  165. }
  166. #endif // __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_15_0
  167. }
  168. return nil;
  169. }
  170. #pragma mark - Alerts
  171. // Returns an alert controller for device not compliant error.
  172. - (UIAlertController *)deviceNotCompliantAlertWithCompletion:(void (^)(void))completion {
  173. UIAlertController *alert =
  174. [UIAlertController alertControllerWithTitle:[self unableToAccessString]
  175. message:[self deviceNotCompliantString]
  176. preferredStyle:UIAlertControllerStyleAlert];
  177. [alert addAction:[UIAlertAction actionWithTitle:[self okayString]
  178. style:UIAlertActionStyleDefault
  179. handler:^(UIAlertAction *action) {
  180. completion();
  181. }]];
  182. return alert;
  183. };
  184. // Returns an alert controller for passcode required error.
  185. - (UIAlertController *)passcodeRequiredAlertWithCompletion:(void (^)(void))completion {
  186. UIAlertController *alert =
  187. [UIAlertController alertControllerWithTitle:[self unableToAccessString]
  188. message:[self passcodeRequiredString]
  189. preferredStyle:UIAlertControllerStyleAlert];
  190. BOOL canOpenSettings = YES;
  191. if ([[UIDevice currentDevice].systemVersion hasPrefix:@"10."]) {
  192. // In iOS 10, `UIApplicationOpenSettingsURLString` fails to open the Settings app if the
  193. // opening app does not have Setting bundle.
  194. NSString* mainBundlePath = [[NSBundle mainBundle] resourcePath];
  195. NSString* settingsBundlePath = [mainBundlePath
  196. stringByAppendingPathComponent:@"Settings.bundle"];
  197. if (![NSBundle bundleWithPath:settingsBundlePath]) {
  198. canOpenSettings = NO;
  199. }
  200. }
  201. if (canOpenSettings) {
  202. [alert addAction:[UIAlertAction actionWithTitle:[self cancelString]
  203. style:UIAlertActionStyleCancel
  204. handler:^(UIAlertAction *action) {
  205. completion();
  206. }]];
  207. [alert addAction:[UIAlertAction actionWithTitle:[self settingsString]
  208. style:UIAlertActionStyleDefault
  209. handler:^(UIAlertAction *action) {
  210. completion();
  211. [self openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
  212. }]];
  213. } else {
  214. [alert addAction:[UIAlertAction actionWithTitle:[self okayString]
  215. style:UIAlertActionStyleCancel
  216. handler:^(UIAlertAction *action) {
  217. completion();
  218. }]];
  219. }
  220. return alert;
  221. };
  222. // Returns an alert controller for app verification required error.
  223. - (UIAlertController *)appVerificationRequiredAlertWithURL:(nullable NSURL *)url
  224. completion:(void (^)(void))completion {
  225. UIAlertController *alert;
  226. if (url) {
  227. // If the URL is provided, prompt user to open this URL or cancel.
  228. alert = [UIAlertController alertControllerWithTitle:[self appVerificationTitleString]
  229. message:[self appVerificationTextString]
  230. preferredStyle:UIAlertControllerStyleAlert];
  231. [alert addAction:[UIAlertAction actionWithTitle:[self cancelString]
  232. style:UIAlertActionStyleCancel
  233. handler:^(UIAlertAction *action) {
  234. completion();
  235. }]];
  236. [alert addAction:[UIAlertAction actionWithTitle:[self appVerificationActionString]
  237. style:UIAlertActionStyleDefault
  238. handler:^(UIAlertAction *action) {
  239. completion();
  240. [self openURL:url];
  241. }]];
  242. } else {
  243. // If the URL is not provided, simple let user acknowledge the issue. This is not supposed to
  244. // happen but just to fail gracefully.
  245. alert = [UIAlertController alertControllerWithTitle:[self unableToAccessString]
  246. message:[self appVerificationTextString]
  247. preferredStyle:UIAlertControllerStyleAlert];
  248. [alert addAction:[UIAlertAction actionWithTitle:[self okayString]
  249. style:UIAlertActionStyleDefault
  250. handler:^(UIAlertAction *action) {
  251. completion();
  252. }]];
  253. }
  254. return alert;
  255. }
  256. - (void)openURL:(NSURL *)url {
  257. if (@available(iOS 10, *)) {
  258. [UIApplication.sharedApplication openURL:url options:@{} completionHandler:nil];
  259. } else {
  260. #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_10_0
  261. [UIApplication.sharedApplication openURL:url];
  262. #endif // __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_10_0
  263. }
  264. }
  265. #pragma mark - Localization
  266. // The English version of the strings are used as back-up in case the bundle resource is missing
  267. // from the third-party app. Please keep them in sync with the strings in the bundle.
  268. // Returns a localized string for unable to access the account.
  269. - (NSString *)unableToAccessString {
  270. return [GIDSignInStrings localizedStringForKey:@"EmmErrorTitle"
  271. text:@"Unable to sign in to account"];
  272. }
  273. // Returns a localized string for device passcode required error.
  274. - (NSString *)passcodeRequiredString {
  275. NSString *defaultText =
  276. @"Your administrator requires you to set a passcode on this device to access this account. "
  277. "Please set a passcode and try again.";
  278. return [GIDSignInStrings localizedStringForKey:@"EmmPasscodeRequired" text:defaultText];
  279. }
  280. // Returns a localized string for app verification error dialog title.
  281. - (NSString *)appVerificationTitleString {
  282. return [GIDSignInStrings localizedStringForKey:@"EmmConnectTitle"
  283. text:@"Connect with Device Policy App?"];
  284. }
  285. // Returns a localized string for app verification error dialog message.
  286. - (NSString *)appVerificationTextString {
  287. NSString *defaultText = @"In order to protect your organization's data, "
  288. "you must connect with the Device Policy app before logging in.";
  289. return [GIDSignInStrings localizedStringForKey:@"EmmConnectText" text:defaultText];
  290. }
  291. // Returns a localized string for app verification error dialog action button label.
  292. - (NSString *)appVerificationActionString {
  293. return [GIDSignInStrings localizedStringForKey:@"EmmConnectLabel" text:@"Connect"];
  294. }
  295. // Returns a localized string for general device non-compliance error.
  296. - (NSString *)deviceNotCompliantString {
  297. NSString *defaultText =
  298. @"The device is not compliant with the security policy set by your administrator.";
  299. return [GIDSignInStrings localizedStringForKey:@"EmmGeneralError" text:defaultText];
  300. }
  301. // Returns a localized string for "Settings".
  302. - (NSString *)settingsString {
  303. return [GIDSignInStrings localizedStringForKey:@"SettingsAppName" text:@"Settings"];
  304. }
  305. // Returns a localized string for "OK".
  306. - (NSString *)okayString {
  307. return [GIDSignInStrings localizedStringForKey:@"OK" text:@"OK"];
  308. }
  309. // Returns a localized string for "Cancel".
  310. - (NSString *)cancelString {
  311. return [GIDSignInStrings localizedStringForKey:@"Cancel" text:@"Cancel"];
  312. }
  313. @end
  314. NS_ASSUME_NONNULL_END
  315. #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST