FIRAppDistributionUIService.m 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. // Copyright 2019 Google
  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 "FirebaseAppDistribution/Sources/FIRAppDistributionUIService.h"
  15. #import "FirebaseAppDistribution/Sources/FIRFADLogger.h"
  16. #import "FirebaseAppDistribution/Sources/Public/FirebaseAppDistribution/FIRAppDistribution.h"
  17. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  18. #import <AuthenticationServices/AuthenticationServices.h>
  19. #import <SafariServices/SafariServices.h>
  20. #import <UIKit/UIKit.h>
  21. @implementation FIRAppDistributionUIService
  22. API_AVAILABLE(ios(12.0))
  23. ASWebAuthenticationSession *_webAuthenticationVC;
  24. // TODO: Remove this when the deployment target becomes iOS 12+
  25. SFAuthenticationSession *_safariAuthenticationVC;
  26. - (instancetype)init {
  27. self = [super init];
  28. self.safariHostingViewController = [[UIViewController alloc] init];
  29. return self;
  30. }
  31. + (instancetype)sharedInstance {
  32. static dispatch_once_t once;
  33. static FIRAppDistributionUIService *sharedInstance;
  34. dispatch_once(&once, ^{
  35. sharedInstance = [[FIRAppDistributionUIService alloc] init];
  36. });
  37. return sharedInstance;
  38. }
  39. + (NSString *)encodedAppId {
  40. return [[[FIRApp defaultApp] options].googleAppID stringByReplacingOccurrencesOfString:@":"
  41. withString:@"-"];
  42. }
  43. + (NSError *)getAppDistributionError:(FIRAppDistributionError)appDistributionErrorCode {
  44. NSString *message = appDistributionErrorCode == FIRAppDistributionErrorAuthenticationCancelled
  45. ? @"User cancelled sign-in flow"
  46. : @"Failed to authenticate the user";
  47. NSDictionary *userInfo = @{FIRAppDistributionErrorDetailsKey : message};
  48. return [NSError errorWithDomain:FIRAppDistributionErrorDomain
  49. code:appDistributionErrorCode
  50. userInfo:userInfo];
  51. }
  52. + (NSError *_Nullable)mapErrorToAppDistributionError:(NSError *_Nullable)error {
  53. if (!error) {
  54. return nil;
  55. }
  56. if (@available(iOS 12.0, *)) {
  57. if ([error code] == ASWebAuthenticationSessionErrorCodeCanceledLogin) {
  58. return [self getAppDistributionError:FIRAppDistributionErrorAuthenticationCancelled];
  59. }
  60. } else {
  61. if ([error code] == SFAuthenticationErrorCanceledLogin) {
  62. return [self getAppDistributionError:FIRAppDistributionErrorAuthenticationCancelled];
  63. }
  64. }
  65. return [self getAppDistributionError:FIRAppDistributionErrorAuthenticationFailure];
  66. }
  67. - (void)appDistributionRegistrationFlow:(NSURL *)URL
  68. withCompletion:(void (^)(NSError *_Nullable error))completion {
  69. NSString *callbackURL =
  70. [NSString stringWithFormat:@"appdistribution-%@", [[self class] encodedAppId]];
  71. FIRFADInfoLog(@"Registration URL: %@", URL);
  72. FIRFADInfoLog(@"Callback URL: %@", callbackURL);
  73. if (@available(iOS 12.0, *)) {
  74. ASWebAuthenticationSession *authenticationVC = [[ASWebAuthenticationSession alloc]
  75. initWithURL:URL
  76. callbackURLScheme:callbackURL
  77. completionHandler:^(NSURL *_Nullable callbackURL, NSError *_Nullable error) {
  78. [self resetUIState];
  79. [self logRegistrationCompletion:error authType:[ASWebAuthenticationSession description]];
  80. NSError *_Nullable appDistributionError =
  81. [[self class] mapErrorToAppDistributionError:error];
  82. completion(appDistributionError);
  83. }];
  84. if (@available(iOS 13.0, *)) {
  85. authenticationVC.presentationContextProvider = self;
  86. }
  87. _webAuthenticationVC = authenticationVC;
  88. [authenticationVC start];
  89. } else {
  90. _safariAuthenticationVC = [[SFAuthenticationSession alloc]
  91. initWithURL:URL
  92. callbackURLScheme:callbackURL
  93. completionHandler:^(NSURL *_Nullable callbackURL, NSError *_Nullable error) {
  94. [self resetUIState];
  95. [self logRegistrationCompletion:error authType:[SFAuthenticationSession description]];
  96. NSError *_Nullable appDistributionError =
  97. [[self class] mapErrorToAppDistributionError:error];
  98. completion(appDistributionError);
  99. }];
  100. [_safariAuthenticationVC start];
  101. }
  102. }
  103. - (void)showUIAlert:(UIAlertController *)alertController {
  104. [self initializeUIState];
  105. [self.window.rootViewController presentViewController:alertController
  106. animated:YES
  107. completion:nil];
  108. }
  109. - (void)showUIAlertWithCompletion:(FIRFADUIActionCompletion)completion {
  110. UIAlertController *alert = [UIAlertController
  111. alertControllerWithTitle:NSLocalizedString(
  112. @"Enable new build alerts",
  113. @"Title for App Distribution New Build Alerts UIAlert.")
  114. message:NSLocalizedString(
  115. @"Get in-app alerts when new builds are ready to test.",
  116. @"Description for enabling new build alerts will do.")
  117. preferredStyle:UIAlertControllerStyleAlert];
  118. UIAlertAction *yesButton = [UIAlertAction
  119. actionWithTitle:NSLocalizedString(@"Turn on", @"Button for turning on new build alerts.")
  120. style:UIAlertActionStyleDefault
  121. handler:^(UIAlertAction *action) {
  122. completion(YES);
  123. }];
  124. UIAlertAction *noButton = [UIAlertAction
  125. actionWithTitle:NSLocalizedString(@"Not now",
  126. @"Button for dismissing the new build alerts UIAlert")
  127. style:UIAlertActionStyleDefault
  128. handler:^(UIAlertAction *action) {
  129. [self resetUIState];
  130. completion(NO);
  131. }];
  132. [alert addAction:noButton];
  133. [alert addAction:yesButton];
  134. // Create an empty window + viewController to host the Safari UI.
  135. [self showUIAlert:alert];
  136. }
  137. - (BOOL)application:(UIApplication *)application
  138. openURL:(NSURL *)URL
  139. options:(NSDictionary<NSString *, id> *)options {
  140. if (self.registrationFlowCompletion) {
  141. FIRFADDebugLog(@"Continuing registration flow: %@", [self registrationFlowCompletion]);
  142. [self resetUIState];
  143. [self logRegistrationCompletion:nil authType:[SFSafariViewController description]];
  144. self.registrationFlowCompletion(nil);
  145. }
  146. return NO;
  147. }
  148. - (void)logRegistrationCompletion:(NSError *)error authType:(NSString *)authType {
  149. if (error) {
  150. FIRFADErrorLog(@"Failed to complete App Distribution registration flow. Auth type - %@, Error "
  151. @"- %@: %ld. Details - %@",
  152. authType, [error domain], (long)[error code], [error localizedDescription]);
  153. } else {
  154. FIRFADInfoLog(@"App Distribution Registration complete. Auth type - %@", authType);
  155. }
  156. }
  157. - (void)initializeUIState {
  158. if (self.window) {
  159. return;
  160. }
  161. if (@available(iOS 13.0, *)) {
  162. UIWindowScene *foregroundedScene = nil;
  163. for (UIWindowScene *connectedScene in [UIApplication sharedApplication].connectedScenes) {
  164. if (connectedScene.activationState == UISceneActivationStateForegroundActive) {
  165. foregroundedScene = connectedScene;
  166. break;
  167. }
  168. }
  169. if (foregroundedScene) {
  170. self.window = [[UIWindow alloc] initWithWindowScene:foregroundedScene];
  171. } else if ([UIApplication sharedApplication].connectedScenes.count == 1) {
  172. // There are situations where a scene isn't considered foreground in viewDidAppear
  173. // and this fixes the issue in single scene apps.
  174. // https://github.com/firebase/firebase-ios-sdk/issues/8096
  175. UIWindowScene *scene =
  176. (UIWindowScene *)[UIApplication sharedApplication].connectedScenes.anyObject;
  177. self.window = [[UIWindow alloc] initWithWindowScene:scene];
  178. } else {
  179. // TODO: Consider using UISceneDidActivateNotification.
  180. FIRFADInfoLog(@"No foreground scene found.");
  181. self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  182. }
  183. } else {
  184. self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  185. }
  186. self.window.rootViewController = self.safariHostingViewController;
  187. // Place it at the highest level within the stack.
  188. self.window.windowLevel = +CGFLOAT_MAX;
  189. // Run it.
  190. [self.window makeKeyAndVisible];
  191. }
  192. - (void)resetUIState {
  193. if (self.window) {
  194. self.window.rootViewController = nil;
  195. self.window.hidden = YES;
  196. self.window = nil;
  197. }
  198. self.registrationFlowCompletion = nil;
  199. if (@available(iOS 12.0, *)) {
  200. _webAuthenticationVC = nil;
  201. }
  202. // TODO: Remove this when the deployment target becomes iOS 12+
  203. // `_safariAuthenticationVC` is cleared unconditionally just in case;
  204. // It’s supposed to be assigned only when run on iOS 11.
  205. _safariAuthenticationVC = nil;
  206. }
  207. - (void)safariViewControllerDidFinish:(SFSafariViewController *)controller {
  208. NSError *error =
  209. [[self class] getAppDistributionError:FIRAppDistributionErrorAuthenticationCancelled];
  210. [self logRegistrationCompletion:error authType:[SFSafariViewController description]];
  211. if (self.registrationFlowCompletion) {
  212. self.registrationFlowCompletion(error);
  213. }
  214. [self resetUIState];
  215. }
  216. - (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:
  217. (ASWebAuthenticationSession *)session API_AVAILABLE(ios(13.0)) {
  218. return self.safariHostingViewController.view.window;
  219. }
  220. @end