FIRAppDistribution.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FIRAppDistribution.h"
  17. #import "FIRAppDistribution+Private.h"
  18. #import "FIRAppDistributionMachO.h"
  19. #import <FirebaseCore/FIRAppInternal.h>
  20. #import <FirebaseCore/FIRComponent.h>
  21. #import <FirebaseCore/FIRComponentContainer.h>
  22. #import <FirebaseCore/FIROptions.h>
  23. #import <UIKit/UIKit.h>
  24. #import <AppAuth/AppAuth.h>
  25. #import <FIRAppDistributionAppDelegateInterceptor.h>
  26. #import <GoogleUtilities/GULAppDelegateSwizzler.h>
  27. /// Empty protocol to register with FirebaseCore's component system.
  28. @protocol FIRAppDistributionInstanceProvider <NSObject>
  29. @end
  30. @interface FIRAppDistribution () <FIRLibrary, FIRAppDistributionInstanceProvider>
  31. @end
  32. @implementation FIRAppDistribution
  33. // The OAuth scope needed to authorize the App Distribution Tester API
  34. NSString *const OIDScopeTesterAPI = @"https://www.googleapis.com/auth/cloud-platform";
  35. // The App Distribution Tester API endpoint used to retrieve releases
  36. NSString *const ReleasesEndpointURL = @"https://firebaseapptesters.googleapis.com/v1alpha/devices/-/testerApps/%@/releases";
  37. NSString *const TesterAPIClientID = @"319754533822-osu3v3hcci24umq6diathdm0dipds1fb.apps.googleusercontent.com";
  38. @synthesize isTesterSignedIn = _isTesterSignedIn;
  39. #pragma mark - Singleton Support
  40. - (instancetype)initWithApp:(FIRApp *)app
  41. appInfo:(NSDictionary *)appInfo {
  42. self = [super init];
  43. if (self) {
  44. self.safariHostingViewController = [[UIViewController alloc] init];
  45. // Save any properties here
  46. NSLog(@"APP DISTRIBUTION STARTED UP!");
  47. [GULAppDelegateSwizzler proxyOriginalDelegate];
  48. FIRAppDistributionAppDelegatorInterceptor *interceptor = [FIRAppDistributionAppDelegatorInterceptor sharedInstance];
  49. [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor];
  50. }
  51. NSString* path = [[NSBundle mainBundle] executablePath];
  52. FIRAppDistributionMachO* machO = [[FIRAppDistributionMachO alloc] initWithPath:path];
  53. NSLog(@"Slices: %@", machO.slices);
  54. // TODO: Lookup keychain to load auth state on init
  55. _isTesterSignedIn = self.authState ? YES: NO;
  56. return self;
  57. }
  58. + (void)load {
  59. [FIRApp registerInternalLibrary:(Class<FIRLibrary>)self
  60. withName:@"firebase-appdistribution"
  61. withVersion:@"0.0.0"]; //TODO: Get version from podspec
  62. }
  63. + (NSArray<FIRComponent *> *)componentsToRegister {
  64. FIRComponentCreationBlock creationBlock =
  65. ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
  66. if (!container.app.isDefaultApp) {
  67. NSLog(@"App Distribution must be used with the default Firebase app.");
  68. return nil;
  69. }
  70. *isCacheable = YES;
  71. return [[FIRAppDistribution alloc] initWithApp:container.app
  72. appInfo:NSBundle.mainBundle.infoDictionary];
  73. };
  74. FIRComponent *component =
  75. [FIRComponent componentWithProtocol:@protocol(FIRAppDistributionInstanceProvider)
  76. instantiationTiming:FIRInstantiationTimingEagerInDefaultApp
  77. dependencies:@[]
  78. creationBlock:creationBlock];
  79. return @[ component ];
  80. }
  81. + (instancetype)appDistribution {
  82. // The container will return the same instance since isCacheable is set
  83. FIRApp *defaultApp = [FIRApp defaultApp]; // Missing configure will be logged here.
  84. // Get the instance from the `FIRApp`'s container. This will create a new instance the
  85. // first time it is called, and since `isCacheable` is set in the component creation
  86. // block, it will return the existing instance on subsequent calls.
  87. id<FIRAppDistributionInstanceProvider> instance =
  88. FIR_COMPONENT(FIRAppDistributionInstanceProvider, defaultApp.container);
  89. // In the component creation block, we return an instance of `FIRAppDistribution`. Cast it and
  90. // return it.
  91. return (FIRAppDistribution *)instance;
  92. }
  93. - (void)signInTesterWithCompletion:(FIRAppDistributionSignInTesterCompletion)completion {
  94. NSURL *issuer = [NSURL URLWithString:@"https://accounts.google.com"];
  95. [OIDAuthorizationService discoverServiceConfigurationForIssuer:issuer
  96. completion:^(OIDServiceConfiguration *_Nullable configuration,
  97. NSError *_Nullable error) {
  98. if (!configuration) {
  99. NSLog(@"Error retrieving discovery document: %@",
  100. [error localizedDescription]);
  101. return;
  102. }
  103. NSString *redirectUrl = [@"dev.firebase.appdistribution." stringByAppendingString:[[[NSBundle mainBundle] bundleIdentifier] stringByAppendingString:@":/launch"]];
  104. OIDAuthorizationRequest *request =
  105. [[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
  106. clientId:TesterAPIClientID
  107. scopes:@[OIDScopeOpenID,
  108. OIDScopeProfile,
  109. OIDScopeTesterAPI]
  110. redirectURL:[NSURL URLWithString:redirectUrl]
  111. responseType:OIDResponseTypeCode
  112. additionalParameters:nil];
  113. // Create an empty window + viewController to host the Safari UI.
  114. UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  115. window.rootViewController = self.safariHostingViewController;
  116. // Place it at the highest level within the stack.
  117. window.windowLevel = +CGFLOAT_MAX;
  118. // Run it.
  119. [window makeKeyAndVisible];
  120. // performs authentication request
  121. [FIRAppDistributionAppDelegatorInterceptor sharedInstance].currentAuthorizationFlow =
  122. [OIDAuthState authStateByPresentingAuthorizationRequest:request
  123. presentingViewController:self.safariHostingViewController
  124. callback:^(OIDAuthState *_Nullable authState,
  125. NSError *_Nullable error) {
  126. self.authState = authState;
  127. self->_isTesterSignedIn = self.authState ? YES : NO;
  128. completion(error);
  129. }];
  130. }];
  131. }
  132. - (void)signOutTester {
  133. self.authState = nil;
  134. _isTesterSignedIn = false;
  135. }
  136. - (void)fetchReleases:(FIRAppDistributionUpdateCheckCompletion)completion {
  137. NSLog(@"Token: %@", self.authState.lastTokenResponse.accessToken);
  138. NSURLSession *URLSession = [NSURLSession sharedSession];
  139. NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
  140. NSString *urlString = [NSString stringWithFormat:ReleasesEndpointURL, [[FIRApp defaultApp] options].googleAppID];
  141. [request setURL:[NSURL URLWithString:urlString]];
  142. [request setHTTPMethod:@"GET"];
  143. [request setValue:[NSString stringWithFormat:@"Bearer %@", self.authState.lastTokenResponse.accessToken] forHTTPHeaderField:@"Authorization"];
  144. NSURLSessionDataTask *listReleasesDataTask =
  145. [URLSession dataTaskWithRequest:request
  146. completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
  147. if (error) {
  148. // TODO: Reformat error into error code
  149. completion(nil, error);
  150. return;
  151. }
  152. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
  153. if (httpResponse.statusCode == 200) {
  154. NSLog(@"Response Code: %ld", httpResponse.statusCode);
  155. [self handleReleasesAPIResponseWithData:data completion:completion];
  156. } else {
  157. NSLog(@"Error Response Code: %ld", httpResponse.statusCode);
  158. // TODO: Handle non-200 http response
  159. dispatch_async(dispatch_get_main_queue(), ^{
  160. completion(nil, nil);
  161. });
  162. }
  163. }];
  164. [listReleasesDataTask resume];
  165. }
  166. - (void)handleReleasesAPIResponseWithData:(NSData*)data
  167. completion:(FIRAppDistributionUpdateCheckCompletion)completion {
  168. // TODO: Parse response from tester API, check instance identifier and maybe return a release
  169. //NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
  170. NSError *error = nil;
  171. NSDictionary *object = [NSJSONSerialization
  172. JSONObjectWithData:data
  173. options:0
  174. error:&error];
  175. if(error) {
  176. NSLog(@"Error parsing the object - %@ error (%@)", object, error);
  177. }
  178. NSLog(@"Response releases %@", [object objectForKey:@"releases"]);
  179. NSArray *releaseList = [object objectForKey:@"releases"];
  180. for (NSDictionary *releaseDict in releaseList) {
  181. if([[releaseDict objectForKey:@"latest"] boolValue]) {
  182. NSString *codeHash = [releaseDict objectForKey:@"codeHash"];
  183. NSString *executablePath = [[NSBundle mainBundle] executablePath];
  184. FIRAppDistributionMachO *machO = [[FIRAppDistributionMachO alloc] initWithPath:executablePath];
  185. if(![codeHash isEqualToString:machO.codeHash]) {
  186. NSLog(@"Hash from service %@", codeHash);
  187. NSLog(@"Hash extracted from app %@", machO.codeHash);
  188. //Update available!
  189. // Ensure we dispatch on the main thread to allow any UI to update
  190. FIRAppDistributionRelease *release = [[FIRAppDistributionRelease alloc] initWithDictionary:releaseDict];
  191. NSLog(@"FIRAppDistributionRelease display version %@", release.displayVersion);
  192. dispatch_async(dispatch_get_main_queue(), ^{
  193. completion(release, nil);
  194. });
  195. }
  196. break;
  197. }
  198. }
  199. // Ensure we dispatch on the main thread to allow any UI to update
  200. dispatch_async(dispatch_get_main_queue(), ^{
  201. completion(nil, nil);
  202. });
  203. }
  204. - (void)checkForUpdateWithCompletion:(FIRAppDistributionUpdateCheckCompletion)completion {
  205. if(self.isTesterSignedIn) {
  206. [self fetchReleases:completion];
  207. } else {
  208. UIAlertController *alert = [UIAlertController
  209. alertControllerWithTitle:@"Enable in-app alerts"
  210. message:@"Sign in with your Firebase App Distribution Google account to turn on in-app alerts for new test releases."
  211. preferredStyle:UIAlertControllerStyleAlert];
  212. UIAlertAction *yesButton = [UIAlertAction
  213. 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
  227. actionWithTitle:@"Not now"
  228. style:UIAlertActionStyleDefault
  229. handler:^(UIAlertAction * action) {
  230. // precaution to ensure window gets destroyed
  231. self.window.hidden = YES;
  232. self.window = nil;
  233. completion(nil, nil);
  234. }];
  235. [alert addAction:noButton];
  236. [alert addAction:yesButton];
  237. // Create an empty window + viewController to host the Safari UI.
  238. self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  239. self.window.rootViewController = [[UIViewController alloc] init];
  240. // Place it at the highest level within the stack.
  241. self.window.windowLevel = +CGFLOAT_MAX;
  242. // Run it.
  243. [self.window makeKeyAndVisible];
  244. [self.window.rootViewController presentViewController:alert animated:YES completion:nil];
  245. }
  246. }
  247. @end