FIRAppDistribution.m 12 KB

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