FIRAppDistribution.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. // Copyright 2020 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 "FIRAppDistribution.h"
  15. #import "FIRAppDistribution+Private.h"
  16. #import "FIRAppDistributionAppDelegateInterceptor.h"
  17. #import <FirebaseCore/FIRAppInternal.h>
  18. #import <FirebaseCore/FIRComponent.h>
  19. #import <FirebaseCore/FIRComponentContainer.h>
  20. #import <FirebaseCore/FIROptions.h>
  21. #import <AppAuth/AppAuth.h>
  22. #import <GoogleUtilities/GULAppDelegateSwizzler.h>
  23. #import <UIKit/UIKit.h>
  24. #import "FIRAppDistributionAppDelegateInterceptor.h"
  25. /// Empty protocol to register with FirebaseCore's component system.
  26. @protocol FIRAppDistributionInstanceProvider <NSObject>
  27. @end
  28. @interface FIRAppDistributionAuthImp: NSObject<FIRAppDistributionAuthProtocol>
  29. @end
  30. @implementation FIRAppDistributionAuthImp
  31. - (void)discoverService:(NSURL *)issuerURL completion:(OIDDiscoveryCallback)completion {
  32. [OIDAuthorizationService discoverServiceConfigurationForIssuer:issuerURL completion:completion];
  33. }
  34. @end
  35. @interface FIRAppDistribution () <FIRLibrary, FIRAppDistributionInstanceProvider>
  36. @property (nonatomic, strong) id<FIRAppDistributionAuthProtocol> appDistributionAuth;
  37. @end
  38. @implementation FIRAppDistribution
  39. // The OAuth scope needed to authorize the App Distribution Tester API
  40. NSString *const kOIDScopeTesterAPI = @"https://www.googleapis.com/auth/cloud-platform";
  41. // The App Distribution Tester API endpoint used to retrieve releases
  42. NSString *const kReleasesEndpointURL =
  43. @"https://firebaseapptesters.googleapis.com/v1alpha/devices/-/testerApps/%@/releases";
  44. NSString *const kTesterAPIClientID =
  45. @"319754533822-osu3v3hcci24umq6diathdm0dipds1fb.apps.googleusercontent.com";
  46. NSString *const kIssuerURL = @"https://accounts.google.com";
  47. // explicit @synthesize is needed since the property is readonly
  48. @synthesize isTesterSignedIn = _isTesterSignedIn;
  49. #pragma mark - Singleton Support
  50. - (instancetype)initWithApp:(FIRApp *)app appInfo:(NSDictionary *)appInfo {
  51. return [self initWithApp:app appInfo:appInfo authHandler:[[FIRAppDistributionAuthImp alloc] init]];
  52. }
  53. - (instancetype)initWithApp:(FIRApp *)app appInfo:(NSDictionary *)appInfo authHandler:(id<FIRAppDistributionAuthProtocol>) auth {
  54. self = [super init];
  55. if (self) {
  56. _appDistributionAuth = auth;
  57. _safariHostingViewController = [[UIViewController alloc] init];
  58. [GULAppDelegateSwizzler proxyOriginalDelegate];
  59. FIRAppDistributionAppDelegatorInterceptor *interceptor =
  60. [FIRAppDistributionAppDelegatorInterceptor sharedInstance];
  61. [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor];
  62. }
  63. // TODO: Lookup keychain to load auth state on init
  64. _isTesterSignedIn = _authState ? YES : NO;
  65. return self;
  66. }
  67. + (void)load {
  68. NSString *version =
  69. [NSString stringWithUTF8String:(const char *const)STR_EXPAND(FIRAppDistribution_VERSION)];
  70. [FIRApp registerInternalLibrary:(Class<FIRLibrary>)self
  71. withName:@"firebase-appdistribution"
  72. withVersion:version];
  73. }
  74. + (NSArray<FIRComponent *> *)componentsToRegister {
  75. FIRComponentCreationBlock creationBlock =
  76. ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
  77. if (!container.app.isDefaultApp) {
  78. // TODO: Implement error handling
  79. @throw([NSException exceptionWithName:@"NotImplementedException"
  80. reason:@"This code path is not implemented yet"
  81. userInfo:nil]);
  82. return nil;
  83. }
  84. *isCacheable = YES;
  85. return [[FIRAppDistribution alloc] initWithApp:container.app
  86. appInfo:NSBundle.mainBundle.infoDictionary];
  87. };
  88. FIRComponent *component =
  89. [FIRComponent componentWithProtocol:@protocol(FIRAppDistributionInstanceProvider)
  90. instantiationTiming:FIRInstantiationTimingEagerInDefaultApp
  91. dependencies:@[]
  92. creationBlock:creationBlock];
  93. return @[ component ];
  94. }
  95. + (instancetype)appDistribution {
  96. // The container will return the same instance since isCacheable is set
  97. FIRApp *defaultApp = [FIRApp defaultApp]; // Missing configure will be logged here.
  98. // Get the instance from the `FIRApp`'s container. This will create a new instance the
  99. // first time it is called, and since `isCacheable` is set in the component creation
  100. // block, it will return the existing instance on subsequent calls.
  101. id<FIRAppDistributionInstanceProvider> instance =
  102. FIR_COMPONENT(FIRAppDistributionInstanceProvider, defaultApp.container);
  103. // In the component creation block, we return an instance of `FIRAppDistribution`. Cast it and
  104. // return it.
  105. return (FIRAppDistribution *)instance;
  106. }
  107. - (void)signInTesterWithCompletion:(FIRAppDistributionSignInTesterCompletion)completion {
  108. NSURL *issuer = [NSURL URLWithString:kIssuerURL];
  109. [self.appDistributionAuth discoverService:issuer
  110. completion:^(OIDServiceConfiguration *_Nullable configuration,
  111. NSError *_Nullable error) {
  112. [self handleOauthDiscoveryCompletion:configuration
  113. error:error
  114. appDistributionSignInCompletion:completion];
  115. }];
  116. }
  117. - (void)signOutTester {
  118. self.authState = nil;
  119. _isTesterSignedIn = false;
  120. }
  121. - (void)fetchReleases:(FIRAppDistributionUpdateCheckCompletion)completion {
  122. NSURLSession *URLSession = [NSURLSession sharedSession];
  123. NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
  124. NSString *URLString =
  125. [NSString stringWithFormat:kReleasesEndpointURL, [[FIRApp defaultApp] options].googleAppID];
  126. [request setURL:[NSURL URLWithString:URLString]];
  127. [request setHTTPMethod:@"GET"];
  128. [request setValue:[NSString
  129. stringWithFormat:@"Bearer %@", self.authState.lastTokenResponse.accessToken]
  130. forHTTPHeaderField:@"Authorization"];
  131. NSURLSessionDataTask *listReleasesDataTask = [URLSession
  132. dataTaskWithRequest:request
  133. completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
  134. if (error) {
  135. // TODO: Reformat error into error code
  136. completion(nil, error);
  137. return;
  138. }
  139. NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
  140. if (HTTPResponse.statusCode == 200) {
  141. [self handleReleasesAPIResponseWithData:data completion:completion];
  142. } else {
  143. // TODO: Handle non-200 http response
  144. @throw([NSException exceptionWithName:@"NotImplementedException"
  145. reason:@"This code path is not implemented yet"
  146. userInfo:nil]);
  147. }
  148. }];
  149. [listReleasesDataTask resume];
  150. }
  151. // OIDServiceConfiguration *_Nullable configuration,
  152. // NSError *_Nullable error
  153. - (void)handleOauthDiscoveryCompletion:(OIDServiceConfiguration *_Nullable)configuration
  154. error:(NSError *_Nullable)error
  155. appDistributionSignInCompletion:(FIRAppDistributionSignInTesterCompletion)completion {
  156. if (!configuration) {
  157. // TODO: Handle when we cannot get configuration
  158. @throw([NSException exceptionWithName:@"NotImplementedException"
  159. reason:@"This code path is not implemented yet"
  160. userInfo:nil]);
  161. return;
  162. }
  163. NSString *redirectUrl = [@"dev.firebase.appdistribution."
  164. stringByAppendingString:[[[NSBundle mainBundle] bundleIdentifier]
  165. stringByAppendingString:@":/launch"]];
  166. OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc]
  167. initWithConfiguration:configuration
  168. clientId:kTesterAPIClientID
  169. scopes:@[ OIDScopeOpenID, OIDScopeProfile, kOIDScopeTesterAPI ]
  170. redirectURL:[NSURL URLWithString:redirectUrl]
  171. responseType:OIDResponseTypeCode
  172. additionalParameters:nil];
  173. [self createUIWindowForLogin];
  174. // performs authentication request
  175. [FIRAppDistributionAppDelegatorInterceptor sharedInstance].currentAuthorizationFlow =
  176. [OIDAuthState authStateByPresentingAuthorizationRequest:request
  177. presentingViewController:self.safariHostingViewController
  178. callback:^(OIDAuthState *_Nullable authState,
  179. NSError *_Nullable error) {
  180. self.authState = authState;
  181. self->_isTesterSignedIn =
  182. self.authState ? YES : NO;
  183. completion(error);
  184. }];
  185. }
  186. - (UIWindow *)createUIWindowForLogin {
  187. // Create an empty window + viewController to host the Safari UI.
  188. UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  189. window.rootViewController = self.safariHostingViewController;
  190. // Place it at the highest level within the stack.
  191. window.windowLevel = +CGFLOAT_MAX;
  192. // Run it.
  193. [window makeKeyAndVisible];
  194. return window;
  195. }
  196. - (void)handleReleasesAPIResponseWithData:data
  197. completion:(FIRAppDistributionUpdateCheckCompletion)completion {
  198. // TODO Implement parsing of releases API response
  199. completion(nil, nil);
  200. }
  201. - (void)checkForUpdateWithCompletion:(FIRAppDistributionUpdateCheckCompletion)completion {
  202. if (self.isTesterSignedIn) {
  203. [self fetchReleases:completion];
  204. } else {
  205. UIAlertController *alert = [UIAlertController
  206. alertControllerWithTitle:@"Enable in-app alerts"
  207. message:@"Sign in with your Firebase App Distribution Google account to "
  208. @"turn on in-app alerts for new test releases."
  209. preferredStyle:UIAlertControllerStyleAlert];
  210. UIAlertAction *yesButton =
  211. [UIAlertAction actionWithTitle:@"Turn on"
  212. style:UIAlertActionStyleDefault
  213. handler:^(UIAlertAction *action) {
  214. [self signInTesterWithCompletion:^(NSError *_Nullable error) {
  215. self.window.hidden = YES;
  216. self.window = nil;
  217. if (error) {
  218. completion(nil, error);
  219. return;
  220. }
  221. [self fetchReleases:completion];
  222. }];
  223. }];
  224. UIAlertAction *noButton = [UIAlertAction actionWithTitle:@"Not now"
  225. style:UIAlertActionStyleDefault
  226. handler:^(UIAlertAction *action) {
  227. // precaution to ensure window gets destroyed
  228. self.window.hidden = YES;
  229. self.window = nil;
  230. completion(nil, nil);
  231. }];
  232. [alert addAction:noButton];
  233. [alert addAction:yesButton];
  234. // Create an empty window + viewController to host the Safari UI.
  235. self.window = [self createUIWindowForLogin];
  236. [self.window.rootViewController presentViewController:alert animated:YES completion:nil];
  237. }
  238. }
  239. @end