| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487 |
- // Copyright 2020 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- #import <AuthenticationServices/AuthenticationServices.h>
- #import <SafariServices/SafariServices.h>
- #import <FirebaseCore/FIRAppInternal.h>
- #import <FirebaseCore/FIRComponent.h>
- #import <FirebaseCore/FIRComponentContainer.h>
- #import <FirebaseCore/FIROptions.h>
- #import <FirebaseInstallations/FirebaseInstallations.h>
- #import <GoogleUtilities/GULAppDelegateSwizzler.h>
- #import "FIRAppDistribution+Private.h"
- #import "FIRAppDistributionAuthPersistence+Private.h"
- #import "FIRAppDistributionMachO+Private.h"
- #import "FIRAppDistributionRelease+Private.h"
- #import "FIRFADLogger.h"
- #import "FIRAppDistributionAppDelegateInterceptor.h"
- /// Empty protocol to register with FirebaseCore's component system.
- @protocol FIRAppDistributionInstanceProvider <NSObject>
- @end
- @interface FIRAppDistribution () <FIRLibrary,
- FIRAppDistributionInstanceProvider,
- ASWebAuthenticationPresentationContextProviding,
- SFSafariViewControllerDelegate>
- @property(nonatomic) BOOL isTesterSignedIn;
- @end
- NSString *const FIRAppDistributionErrorDomain = @"com.firebase.appdistribution";
- NSString *const FIRAppDistributionErrorDetailsKey = @"details";
- @implementation FIRAppDistribution
- // The OAuth scope needed to authorize the App Distribution Tester API
- NSString *const kOIDScopeTesterAPI = @"https://www.googleapis.com/auth/cloud-platform";
- // The App Distribution Tester API endpoint used to retrieve releases
- NSString *const kReleasesEndpointURL = @"https://firebaseapptesters.googleapis.com/v1alpha/devices/"
- @"-/testerApps/%@/installations/%@/releases";
- NSString *const kTesterAPIClientID =
- @"319754533822-osu3v3hcci24umq6diathdm0dipds1fb.apps.googleusercontent.com";
- NSString *const kIssuerURL = @"https://accounts.google.com";
- NSString *const kAppDistroLibraryName = @"fire-fad";
- NSString *const kReleasesKey = @"releases";
- NSString *const kLatestReleaseKey = @"latest";
- NSString *const kCodeHashKey = @"codeHash";
- NSString *const kAuthErrorMessage = @"Unable to authenticate the tester";
- NSString *const kAuthCancelledErrorMessage = @"Tester cancelled sign-in";
- @synthesize isTesterSignedIn = _isTesterSignedIn;
- API_AVAILABLE(ios(9.0))
- SFSafariViewController *_safariVC;
- API_AVAILABLE(ios(12.0))
- ASWebAuthenticationSession *_webAuthenticationVC;
- API_AVAILABLE(ios(11.0))
- SFAuthenticationSession *_safariAuthenticationVC;
- - (BOOL)isTesterSignedIn {
- // FIRFADInfoLog(@"Checking if tester is signed in");
- // return [self tryInitializeAuthState];
- return NO;
- }
- #pragma mark - Singleton Support
- - (instancetype)initWithApp:(FIRApp *)app appInfo:(NSDictionary *)appInfo {
- // FIRFADInfoLog(@"Initializing Firebase App Distribution");
- self = [super init];
- if (self) {
- self.safariHostingViewController = [[UIViewController alloc] init];
- [GULAppDelegateSwizzler proxyOriginalDelegate];
- FIRAppDistributionAppDelegatorInterceptor *interceptor =
- [FIRAppDistributionAppDelegatorInterceptor sharedInstance];
- [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor];
- }
- // self.authPersistence = [[FIRAppDistributionAuthPersistence alloc]
- // initWithAppId:[[FIRApp defaultApp] options].googleAppID];
- return self;
- }
- + (void)load {
- NSString *version =
- [NSString stringWithUTF8String:(const char *const)STR_EXPAND(FIRAppDistribution_VERSION)];
- [FIRApp registerInternalLibrary:(Class<FIRLibrary>)self
- withName:kAppDistroLibraryName
- withVersion:version];
- }
- + (NSArray<FIRComponent *> *)componentsToRegister {
- FIRComponentCreationBlock creationBlock =
- ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
- if (!container.app.isDefaultApp) {
- // TODO: Remove this and log error
- @throw([NSException exceptionWithName:@"NotImplementedException"
- reason:@"This code path is not implemented yet"
- userInfo:nil]);
- return nil;
- }
- *isCacheable = YES;
- return [[FIRAppDistribution alloc] initWithApp:container.app
- appInfo:NSBundle.mainBundle.infoDictionary];
- };
- FIRComponent *component =
- [FIRComponent componentWithProtocol:@protocol(FIRAppDistributionInstanceProvider)
- instantiationTiming:FIRInstantiationTimingEagerInDefaultApp
- dependencies:@[]
- creationBlock:creationBlock];
- return @[ component ];
- }
- + (instancetype)appDistribution {
- // The container will return the same instance since isCacheable is set
- FIRApp *defaultApp = [FIRApp defaultApp]; // Missing configure will be logged here.
- // Get the instance from the `FIRApp`'s container. This will create a new instance the
- // first time it is called, and since `isCacheable` is set in the component creation
- // block, it will return the existing instance on subsequent calls.
- id<FIRAppDistributionInstanceProvider> instance =
- FIR_COMPONENT(FIRAppDistributionInstanceProvider, defaultApp.container);
- // In the component creation block, we return an instance of `FIRAppDistribution`. Cast it and
- // return it.
- NSLog(@"Instance returned! %@", instance);
- return (FIRAppDistribution *)instance;
- }
- - (void)signInTesterWithCompletion:(void (^)(NSError *_Nullable error))completion {
- NSLog(@"Testing: App Distribution sign in");
- // TODO: Check if tester is already signed in
- [self setupUIWindowForLogin];
- FIRInstallations *installations = [FIRInstallations installations];
- // Get a Firebase Installation ID (FID).
- [installations installationIDWithCompletion:^(NSString *__nullable identifier,
- NSError *__nullable error) {
- if (error) {
- completion(error);
- return;
- }
- NSString *requestURL = [NSString
- stringWithFormat:@"https://partnerdash.google.com/apps/appdistribution/pub/apps/%@/"
- @"installations/%@/buildalerts?appName=%@",
- [[FIRApp defaultApp] options].googleAppID, identifier, [self getAppName]];
- NSLog(@"Registration URL: %@", requestURL);
- if (@available(iOS 12.0, *)) {
- ASWebAuthenticationSession *authenticationVC = [[ASWebAuthenticationSession alloc]
- initWithURL:[[NSURL alloc] initWithString:requestURL]
- callbackURLScheme:@"com.firebase.appdistribution"
- completionHandler:^(NSURL *_Nullable callbackURL, NSError *_Nullable error) {
- [self cleanupUIWindow];
- NSLog(@"Testing: Sign in Complete!");
- if (callbackURL) {
- self.isTesterSignedIn = true;
- completion(nil);
- } else {
- self.isTesterSignedIn = false;
- completion(error);
- }
- }];
- if (@available(iOS 13.0, *)) {
- authenticationVC.presentationContextProvider = self;
- }
- _webAuthenticationVC = authenticationVC;
- [authenticationVC start];
- } else if (@available(iOS 11.0, *)) {
- _safariAuthenticationVC = [[SFAuthenticationSession alloc]
- initWithURL:[[NSURL alloc] initWithString:requestURL]
- callbackURLScheme:@"com.firebase.appdistribution"
- completionHandler:^(NSURL *_Nullable callbackURL, NSError *_Nullable error) {
- [self cleanupUIWindow];
- NSLog(@"Testing: Sign in Complete!");
- if (callbackURL) {
- self.isTesterSignedIn = true;
- completion(nil);
- } else {
- self.isTesterSignedIn = false;
- completion(error);
- }
- }];
- } else {
- SFSafariViewController *safariVC = [[SFSafariViewController alloc] initWithURL:requestURL];
- safariVC.delegate = self;
- _safariVC = safariVC;
- [self->_safariHostingViewController presentViewController:safariVC
- animated:YES
- completion:nil];
- }
- }];
- }
- - (NSString *)getAppName {
- NSBundle *mainBundle = [NSBundle mainBundle];
- NSString *name = [mainBundle objectForInfoDictionaryKey:@"CFBundleName"];
- if (name) return name;
- name = [mainBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
- return name;
- }
- - (void)signOutTester {
- // FIRFADInfoLog(@"Tester sign out");
- // NSError *error;
- // BOOL didClearAuthState = [self.authPersistence clearAuthState:&error];
- // if (!didClearAuthState) {
- // FIRFADErrorLog(@"Error clearing token from keychain: %@", [error localizedDescription]);
- // [self logUnderlyingKeychainError:error];
- //
- // } else {
- // FIRFADInfoLog(@"Successfully cleared auth state from keychain");
- // }
- self.authState = nil;
- self.isTesterSignedIn = false;
- }
- - (NSError *)NSErrorForErrorCodeAndMessage:(FIRAppDistributionError)errorCode
- message:(NSString *)message {
- NSDictionary *userInfo = @{FIRAppDistributionErrorDetailsKey : message};
- return [NSError errorWithDomain:FIRAppDistributionErrorDomain code:errorCode userInfo:userInfo];
- }
- - (void)fetchReleases:(FIRAppDistributionUpdateCheckCompletion)completion {
- // OR for default FIRApp:
- FIRInstallations *installations = [FIRInstallations installations];
- // Get a FIS Authentication Token.
- [installations authTokenWithCompletion:^(
- FIRInstallationsAuthTokenResult *_Nullable authTokenResult,
- NSError *_Nullable error) {
- if (error) {
- // FIRFADErrorLog(@"Error getting fresh auth tokens. Will sign out tester. Error: %@",
- // [error localizedDescription]);
- // TODO: Do we need a less aggresive strategy here? maybe a retry?
- [self signOutTester];
- NSError *HTTPError =
- [self NSErrorForErrorCodeAndMessage:FIRAppDistributionErrorAuthenticationFailure
- message:kAuthErrorMessage];
- dispatch_async(dispatch_get_main_queue(), ^{
- completion(nil, HTTPError);
- });
- return;
- }
- [installations installationIDWithCompletion:^(NSString *__nullable identifier,
- NSError *__nullable error) {
- // perform your API request using the tokens
- NSURLSession *URLSession = [NSURLSession sharedSession];
- NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
- NSString *URLString =
- [NSString stringWithFormat:kReleasesEndpointURL,
- [[FIRApp defaultApp] options].googleAppID, identifier];
- // FIRFADInfoLog(@"Requesting releases for app id - %@",
- // [[FIRApp defaultApp] options].googleAppID);
- [request setURL:[NSURL URLWithString:URLString]];
- [request setHTTPMethod:@"GET"];
- [request setValue:authTokenResult.authToken
- forHTTPHeaderField:@"X-Goog-Firebase-Installations-Auth"];
- [request setValue:[[FIRApp defaultApp] options].APIKey forHTTPHeaderField:@"X-Goog-Api-Key"];
- NSLog(@"Url : %@, Auth token: %@ API KEY: %@", URLString, authTokenResult.authToken,
- [[FIRApp defaultApp] options].APIKey);
- NSURLSessionDataTask *listReleasesDataTask = [URLSession
- dataTaskWithRequest:request
- completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
- NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
- NSLog(@"HTTPResonse status code %ld response %@", (long)HTTPResponse.statusCode,
- HTTPResponse);
- if (error || HTTPResponse.statusCode != 200) {
- NSError *HTTPError = nil;
- if (HTTPResponse == nil && error) {
- // Handles network timeouts or no internet connectivity
- NSString *message = error.userInfo[NSLocalizedDescriptionKey]
- ? error.userInfo[NSLocalizedDescriptionKey]
- : @"";
- HTTPError =
- [self NSErrorForErrorCodeAndMessage:FIRAppDistributionErrorNetworkFailure
- message:message];
- } else if (HTTPResponse.statusCode == 401) {
- // TODO: Maybe sign out tester?
- HTTPError = [self
- NSErrorForErrorCodeAndMessage:FIRAppDistributionErrorAuthenticationFailure
- message:kAuthErrorMessage];
- } else {
- HTTPError = [self NSErrorForErrorCodeAndMessage:FIRAppDistributionErrorUnknown
- message:@""];
- }
- // FIRFADErrorLog(@"App Tester API service error - %@",
- // [HTTPError localizedDescription]);
- dispatch_async(dispatch_get_main_queue(), ^{
- completion(nil, HTTPError);
- });
- } else {
- [self handleReleasesAPIResponseWithData:data completion:completion];
- }
- }];
- [listReleasesDataTask resume];
- }];
- }];
- }
- - (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:
- (ASWebAuthenticationSession *)session API_AVAILABLE(ios(13.0)) {
- return self.safariHostingViewController.view.window;
- }
- - (void)setupUIWindowForLogin {
- if (self.window) {
- return;
- }
- // Create an empty window + viewController to host the Safari UI.
- self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
- self.window.rootViewController = self.safariHostingViewController;
- // Place it at the highest level within the stack.
- self.window.windowLevel = +CGFLOAT_MAX;
- // Run it.
- [self.window makeKeyAndVisible];
- }
- - (void)cleanupUIWindow {
- if (self.window) {
- self.window.hidden = YES;
- self.window = nil;
- }
- _safariAuthenticationVC = nil;
- _safariVC = nil;
- _webAuthenticationVC = nil;
- }
- //- (void)logUnderlyingKeychainError:(NSError *)error {
- // NSError *underlyingError = [error.userInfo objectForKey:NSUnderlyingErrorKey];
- // if (underlyingError) {
- // FIRFADErrorLog(@"Keychain error - %@", [underlyingError localizedDescription]);
- // }
- //}
- - (void)handleReleasesAPIResponseWithData:data
- completion:(FIRAppDistributionUpdateCheckCompletion)completion {
- NSError *error = nil;
- NSDictionary *serializedResponse = [NSJSONSerialization JSONObjectWithData:data
- options:0
- error:&error];
- if (error) {
- // FIRFADErrorLog(@"Tester API - Error serializing json response");
- NSString *message =
- error.userInfo[NSLocalizedDescriptionKey] ? error.userInfo[NSLocalizedDescriptionKey] : @"";
- NSError *error = [self NSErrorForErrorCodeAndMessage:FIRAppDistributionErrorUnknown
- message:message];
- dispatch_async(dispatch_get_main_queue(), ^{
- completion(nil, error);
- });
- return;
- }
- NSArray *releaseList = [serializedResponse objectForKey:kReleasesKey];
- for (NSDictionary *releaseDict in releaseList) {
- if ([[releaseDict objectForKey:kLatestReleaseKey] boolValue]) {
- // FIRFADInfoLog(@"Tester API - found latest release in response. Checking if code hash
- // match");
- NSString *codeHash = [releaseDict objectForKey:kCodeHashKey];
- NSString *executablePath = [[NSBundle mainBundle] executablePath];
- FIRAppDistributionMachO *machO =
- [[FIRAppDistributionMachO alloc] initWithPath:executablePath];
- // FIRFADInfoLog(@"Code hash for the app on device - %@", machO.codeHash);
- // FIRFADInfoLog(@"Code hash for the release from the service response - %@", codeHash);
- if (codeHash && ![codeHash isEqualToString:machO.codeHash]) {
- FIRAppDistributionRelease *release =
- [[FIRAppDistributionRelease alloc] initWithDictionary:releaseDict];
- dispatch_async(dispatch_get_main_queue(), ^{
- // FIRFADInfoLog(@"Found new release");
- completion(release, nil);
- });
- return;
- }
- break;
- }
- }
- // FIRFADInfoLog(@"Tester API - No new release found");
- dispatch_async(dispatch_get_main_queue(), ^{
- completion(nil, nil);
- });
- }
- - (void)checkForUpdateWithCompletion:(FIRAppDistributionUpdateCheckCompletion)completion {
- NSLog(@"CheckForUpdateWithCompletion");
- if (false) {
- [self fetchReleases:completion];
- } else {
- UIAlertController *alert = [UIAlertController
- alertControllerWithTitle:@"Enable in-app alerts"
- message:@"Sign in with your Firebase App Distribution Google account to "
- @"turn on in-app alerts for new test releases."
- preferredStyle:UIAlertControllerStyleAlert];
- UIAlertAction *yesButton =
- [UIAlertAction actionWithTitle:@"Turn on"
- style:UIAlertActionStyleDefault
- handler:^(UIAlertAction *action) {
- [self signInTesterWithCompletion:^(NSError *_Nullable error) {
- if (error) {
- completion(nil, error);
- return;
- }
- [self fetchReleases:completion];
- }];
- }];
- UIAlertAction *noButton = [UIAlertAction actionWithTitle:@"Not now"
- style:UIAlertActionStyleDefault
- handler:^(UIAlertAction *action) {
- // precaution to ensure window gets destroyed
- [self cleanupUIWindow];
- completion(nil, nil);
- }];
- [alert addAction:noButton];
- [alert addAction:yesButton];
- // Create an empty window + viewController to host the Safari UI.
- [self setupUIWindowForLogin];
- [self.window.rootViewController presentViewController:alert animated:YES completion:nil];
- }
- }
- - (void)safariViewControllerDidFinish:(SFSafariViewController *)controller NS_AVAILABLE_IOS(9.0) {
- [self cleanupUIWindow];
- }
- @end
|