Files
firebase-ios-sdk/FirebaseAppDistribution/Sources/FIRAppDistribution.m
T
16e062c355 Switch App Distribution SDK from AppAuth to FirebaseInstallations (#6126)
* Adding error handling and returning correct error codes

* * Handling auth cancellation a few other error cases

* Adding some logging for EAP (Using NSLogs temporarily. DONOT MERGE)

* Add in a setter for clientID to allow overrides (#5514)

* Add in a setter for clientID to allow overrides

* Update styling

* Adding some result parsing logs

* Fixing code hash logging

* Add FirebaseAppDistribution to ZipBuilder

* * Making sure keychain is accesible after first unlock even if app is backgrounded (#5563)
* Fix auth persistence logging and invalid type casting (#5631)
* Logging app id requesting releases (#5655)

* AppDistribution keychain access fixes (#5683)

* Using the GULKeychainUtils instead of custom keychain utility. Also added better logging

* Updating keychain name and fixing tests

* Code review feedback updates

* Making serviceID unique per app

* Adding gmp app id to the keychain along with the bundle id

* Moving Keychain Utility functions in the AuthPersistence class and deleting the utility class

* Lazy initialize auth state (#5755)

* Lazy initialize auth state

* Making sure we dont log an error is auth state is not persisted in the keychain

* Switch from NSLog to FIRFADLogger (#5756)

* Add FIRLogger
* Switch from NSLog to FIRFADLogger

* * Use FIS Auth for authenticating the tester
* Remove AppAuth and start wiring up interceptor to do sign in

* Extracting registration flow UI in interceptor

* Adding support for iOS 9 and 10

* Create app distribution api service to handle tester api calls. (#6060)

* Pull out calls to the FAD API to make them reuseable

* Fix tests and run styl updates

* Uncomment FIRFADLog Statements

* Use [] accessor rather than . accessor

* Re-running style updates

* Trying to get these checks to pass the first time around

* App distribution signin persist (#6063)

* Pull out calls to the FAD API to make them reuseable

* Fix tests and run styl updates

* Add in UserDefaults storage after a tester has signed in

* Add testing for the main AppDistribution file

* Ran style.sh

* Remove FIRFADLocalStorage and call GULUserDefaults directly. Update tests to stop mocking on teardown.

* Add support for dynamic url schemes and fix two crashes (#6081)

* Add support for dynamic url schemes and fix two crashes

  * Add support for dynamic url schemes in the format appdistribution-<app id without colons>
  * Fix crash when launching an app directly via a URL scheme
  * Fix a crash on iOS 10 simulator caused by calling NSLogv twice
  * Remove uses of NSLog
  * Fix warnings

* Switch encodedAppId to be a class method

* Fix styles

* Create changelog and readme in preparation for release. (#6095)

Just changing markdown files so will not wait for gha checks.

* Change slices property from copy to strong (#6097)

Fix lint warning

* Change error handlers to return bool values instead of void (#6101)

* Mapping sign in flow errors to app distribution errors. Also renaming AppDelegateInterceptor to FIRAppDistributionUIService (#6099)

* Add github action for App Distribution (#6094)

* Updating app distribution deployment target to iOS 9 (#6118)

* Making sure we return granular errors during sign in persistence. Also fixing some naming and comment issues (#6117)

* Encoding colons to dashes in the app id (#6119)

* Move Internal headers to Sources (#6106)

* Move Internal headers to Sources/ folder

* PR feedback

* Remove appDistribution from zip builder

* Respond to code review feedback (#6138)

* Initializing UIWindow using initWithWindowScene for ios 13 and removing UI code from main FIRAppDistribution.m (#6144)

* Initializing UIWindow using initWithWindowScene for ios 13. Also making sure we only reset window in the App delegate if a registration flow is in progress.

* Remove UI code from main class, add unit tests, and clean up UIService

* stylish

* Remove defines in favor of solely 'if (@available(iOS 13.0, *))'

* Respond to code review feedback

Co-authored-by: Cleo Schneider <cleoschneider@google.com>

Co-authored-by: Pranav Rajgopal <pranavrajgopal@google.com>
Co-authored-by: Cleo Schneider <cleoschneider@google.com>
2020-07-28 16:39:29 -04:00

302 lines
12 KiB
Objective-C

// 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 <Foundation/Foundation.h>
#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
#import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
#import "GoogleUtilities/AppDelegateSwizzler/Private/GULAppDelegateSwizzler.h"
#import "GoogleUtilities/UserDefaults/Private/GULUserDefaults.h"
#import "FirebaseAppDistribution/Sources/FIRAppDistributionMachO.h"
#import "FirebaseAppDistribution/Sources/FIRAppDistributionUIService.h"
#import "FirebaseAppDistribution/Sources/FIRFADApiService.h"
#import "FirebaseAppDistribution/Sources/FIRFADLogger.h"
#import "FirebaseAppDistribution/Sources/Private/FIRAppDistribution.h"
#import "FirebaseAppDistribution/Sources/Private/FIRAppDistributionRelease.h"
/// Empty protocol to register with FirebaseCore's component system.
@protocol FIRAppDistributionInstanceProvider <NSObject>
@end
@interface FIRAppDistribution () <FIRLibrary, FIRAppDistributionInstanceProvider>
@property(nonatomic) BOOL isTesterSignedIn;
@property(nullable, nonatomic) FIRAppDistributionUIService *uiService;
@end
NSString *const FIRAppDistributionErrorDomain = @"com.firebase.appdistribution";
NSString *const FIRAppDistributionErrorDetailsKey = @"details";
@implementation FIRAppDistribution
// The App Distribution Tester API endpoint used to retrieve releases
NSString *const kReleasesEndpointURL = @"https://firebaseapptesters.googleapis.com/v1alpha/devices/"
@"-/testerApps/%@/installations/%@/releases";
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";
NSString *const kFIRFADSignInStateKey = @"FIRFADSignInState";
@synthesize isTesterSignedIn = _isTesterSignedIn;
- (BOOL)isTesterSignedIn {
BOOL signInState = [[GULUserDefaults standardUserDefaults] boolForKey:kFIRFADSignInStateKey];
FIRFADInfoLog(@"Tester is %@signed in.", signInState ? @"" : @"not ");
return signInState;
}
#pragma mark - Singleton Support
- (instancetype)initWithApp:(FIRApp *)app appInfo:(NSDictionary *)appInfo {
// FIRFADInfoLog(@"Initializing Firebase App Distribution");
self = [super init];
if (self) {
[GULAppDelegateSwizzler proxyOriginalDelegate];
self.uiService = [FIRAppDistributionUIService sharedInstance];
[GULAppDelegateSwizzler registerAppDelegateInterceptor:[self uiService]];
}
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) {
FIRFADErrorLog(@"Firebase App Distribution only works with the default app.");
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.
FIRFADDebugLog(@"Instance returned: %@", instance);
return (FIRAppDistribution *)instance;
}
- (void)signInTesterWithCompletion:(void (^)(NSError *_Nullable error))completion {
FIRFADDebugLog(@"Prompting tester for sign in");
if ([self isTesterSignedIn]) {
completion(nil);
return;
}
[[self uiService] initializeUIState];
FIRInstallations *installations = [FIRInstallations installations];
// Get a Firebase Installation ID (FID).
[installations installationIDWithCompletion:^(NSString *__nullable identifier,
NSError *__nullable error) {
if (error) {
NSString *description = error.userInfo[NSLocalizedDescriptionKey]
? error.userInfo[NSLocalizedDescriptionKey]
: @"Failed to retrieve Installation ID.";
completion([self NSErrorForErrorCodeAndMessage:FIRAppDistributionErrorUnknown
message:description]);
[[self uiService] resetUIState];
return;
}
NSString *requestURL = [NSString
stringWithFormat:@"https://partnerdash.google.com/apps/appdistribution/pub/apps/%@/"
@"installations/%@/buildalerts?appName=%@",
[[FIRApp defaultApp] options].googleAppID, identifier, [self getAppName]];
FIRFADDebugLog(@"Registration URL: %@", requestURL);
[[self uiService]
appDistributionRegistrationFlow:[[NSURL alloc] initWithString:requestURL]
withCompletion:^(NSError *_Nullable error) {
FIRFADInfoLog(@"Tester sign in complete.");
if (error) {
completion(error);
return;
}
[self persistTesterSignInStateAndHandleCompletion:completion];
}];
}];
}
- (void)persistTesterSignInStateAndHandleCompletion:(void (^)(NSError *_Nullable error))completion {
[FIRFADApiService
fetchReleasesWithCompletion:^(NSArray *_Nullable releases, NSError *_Nullable error) {
if (error) {
FIRFADErrorLog(@"Tester Sign in persistence. Could not fetch releases with code %ld - %@",
[error code], [error localizedDescription]);
completion([self mapFetchReleasesError:error]);
return;
}
[[GULUserDefaults standardUserDefaults] setBool:YES forKey:kFIRFADSignInStateKey];
completion(nil);
}];
}
- (NSString *)getAppName {
NSBundle *mainBundle = [NSBundle mainBundle];
NSString *name = [mainBundle objectForInfoDictionaryKey:@"CFBundleName"];
if (name)
return
[name stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet
URLHostAllowedCharacterSet]];
name = [mainBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
return [name stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet
URLHostAllowedCharacterSet]];
}
- (void)signOutTester {
FIRFADDebugLog(@"Tester is signed out.");
[[GULUserDefaults standardUserDefaults] setBool:NO forKey:kFIRFADSignInStateKey];
}
- (NSError *)NSErrorForErrorCodeAndMessage:(FIRAppDistributionError)errorCode
message:(NSString *)message {
NSDictionary *userInfo = @{FIRAppDistributionErrorDetailsKey : message};
return [NSError errorWithDomain:FIRAppDistributionErrorDomain code:errorCode userInfo:userInfo];
}
- (NSError *_Nullable)mapFetchReleasesError:(NSError *)error {
if ([error domain] == kFIRFADApiErrorDomain) {
FIRFADErrorLog(@"Failed to retrieve releases: %ld", (long)[error code]);
switch ([error code]) {
case FIRFADApiErrorTimeout:
return [self NSErrorForErrorCodeAndMessage:FIRAppDistributionErrorNetworkFailure
message:@"Failed to fetch releases due to timeout."];
case FIRFADApiErrorUnauthenticated:
case FIRFADApiErrorUnauthorized:
case FIRFADApiTokenGenerationFailure:
case FIRFADApiInstallationIdentifierError:
case FIRFADApiErrorNotFound:
return [self NSErrorForErrorCodeAndMessage:FIRAppDistributionErrorAuthenticationFailure
message:@"Could not authenticate tester"];
default:
return [self NSErrorForErrorCodeAndMessage:FIRAppDistributionErrorUnknown
message:@"Failed to fetch releases for unknown reason."];
}
}
FIRFADErrorLog(@"Failed to retrieve releases with unexpected domain %@: %ld", [error domain],
(long)[error code]);
return [self NSErrorForErrorCodeAndMessage:FIRAppDistributionErrorUnknown
message:@"Failed to fetch releases for unknown reason."];
}
- (void)fetchNewLatestRelease:(FIRAppDistributionUpdateCheckCompletion)completion {
[FIRFADApiService
fetchReleasesWithCompletion:^(NSArray *_Nullable releases, NSError *_Nullable error) {
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, [self mapFetchReleasesError:error]);
});
return;
}
for (NSDictionary *releaseDict in releases) {
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 with version: %@", [release displayVersion]);
completion(release, nil);
});
return;
}
}
}
completion(nil, nil);
}];
}
- (void)checkForUpdateWithCompletion:(FIRAppDistributionUpdateCheckCompletion)completion {
FIRFADInfoLog(@"CheckForUpdateWithCompletion");
if ([self isTesterSignedIn]) {
[self fetchNewLatestRelease:completion];
} else {
FIRFADUIActionCompletion actionCompletion = ^(BOOL continued) {
if (continued) {
[self signInTesterWithCompletion:^(NSError *_Nullable error) {
if (error) {
completion(nil, error);
return;
}
[self fetchNewLatestRelease:completion];
}];
} else {
completion(
nil, [self NSErrorForErrorCodeAndMessage:FIRAppDistributionErrorAuthenticationCancelled
message:@"Tester cancelled authentication flow."]);
}
};
[[self uiService] showUIAlertWithCompletion:actionCompletion];
}
}
@end