FIRAppDistributionUIService.m 8.4 KB

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