blob: 559997f9d6213ca72296a0ea44179016f9619924 [file] [log] [blame]
// Copyright 2017, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#import "FLTGoogleSignInPlugin.h"
#import <GoogleSignIn/GoogleSignIn.h>
// The key within `GoogleService-Info.plist` used to hold the application's
// client id. See https://developers.google.com/identity/sign-in/ios/start
// for more info.
static NSString *const kClientIdKey = @"CLIENT_ID";
// These error codes must match with ones declared on Android and Dart sides.
static NSString *const kErrorReasonSignInRequired = @"sign_in_required";
static NSString *const kErrorReasonSignInCanceled = @"sign_in_canceled";
static NSString *const kErrorReasonSignInFailed = @"sign_in_failed";
static FlutterError *getFlutterError(NSError *error) {
NSString *errorCode;
if (error.code == kGIDSignInErrorCodeHasNoAuthInKeychain) {
errorCode = kErrorReasonSignInRequired;
} else if (error.code == kGIDSignInErrorCodeCanceled) {
errorCode = kErrorReasonSignInCanceled;
} else {
errorCode = kErrorReasonSignInFailed;
}
return [FlutterError errorWithCode:errorCode
message:error.domain
details:error.localizedDescription];
}
@interface FLTGoogleSignInPlugin () <GIDSignInDelegate>
@end
@implementation FLTGoogleSignInPlugin {
FlutterResult _accountRequest;
}
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FlutterMethodChannel *channel =
[FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/google_sign_in"
binaryMessenger:[registrar messenger]];
FLTGoogleSignInPlugin *instance = [[FLTGoogleSignInPlugin alloc] init];
[registrar addApplicationDelegate:instance];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (instancetype)init {
self = [super init];
if (self) {
[GIDSignIn sharedInstance].delegate = self;
// On the iOS simulator, we get "Broken pipe" errors after sign-in for some
// unknown reason. We can avoid crashing the app by ignoring them.
signal(SIGPIPE, SIG_IGN);
}
return self;
}
#pragma mark - <FlutterPlugin> protocol
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
if ([call.method isEqualToString:@"init"]) {
NSString *signInOption = call.arguments[@"signInOption"];
if ([signInOption isEqualToString:@"SignInOption.games"]) {
result([FlutterError errorWithCode:@"unsupported-options"
message:@"Games sign in is not supported on iOS"
details:nil]);
} else {
NSString *path = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info"
ofType:@"plist"];
if (path) {
NSMutableDictionary *plist = [[NSMutableDictionary alloc] initWithContentsOfFile:path];
[GIDSignIn sharedInstance].clientID = plist[kClientIdKey];
[GIDSignIn sharedInstance].scopes = call.arguments[@"scopes"];
[GIDSignIn sharedInstance].hostedDomain = call.arguments[@"hostedDomain"];
result(nil);
} else {
result([FlutterError errorWithCode:@"missing-config"
message:@"GoogleService-Info.plist file not found"
details:nil]);
}
}
} else if ([call.method isEqualToString:@"signInSilently"]) {
if ([self setAccountRequest:result]) {
[[GIDSignIn sharedInstance] restorePreviousSignIn];
}
} else if ([call.method isEqualToString:@"isSignedIn"]) {
result(@([[GIDSignIn sharedInstance] hasPreviousSignIn]));
} else if ([call.method isEqualToString:@"signIn"]) {
[GIDSignIn sharedInstance].presentingViewController = [self topViewController];
if ([self setAccountRequest:result]) {
@try {
[[GIDSignIn sharedInstance] signIn];
} @catch (NSException *e) {
result([FlutterError errorWithCode:@"google_sign_in" message:e.reason details:e.name]);
[e raise];
}
}
} else if ([call.method isEqualToString:@"getTokens"]) {
GIDGoogleUser *currentUser = [GIDSignIn sharedInstance].currentUser;
GIDAuthentication *auth = currentUser.authentication;
[auth getTokensWithHandler:^void(GIDAuthentication *authentication, NSError *error) {
result(error != nil ? getFlutterError(error) : @{
@"idToken" : authentication.idToken,
@"accessToken" : authentication.accessToken,
});
}];
} else if ([call.method isEqualToString:@"signOut"]) {
[[GIDSignIn sharedInstance] signOut];
result(nil);
} else if ([call.method isEqualToString:@"disconnect"]) {
if ([self setAccountRequest:result]) {
[[GIDSignIn sharedInstance] disconnect];
}
} else if ([call.method isEqualToString:@"clearAuthCache"]) {
// There's nothing to be done here on iOS since the expired/invalid
// tokens are refreshed automatically by getTokensWithHandler.
result(nil);
} else {
result(FlutterMethodNotImplemented);
}
}
- (BOOL)setAccountRequest:(FlutterResult)request {
if (_accountRequest != nil) {
request([FlutterError errorWithCode:@"concurrent-requests"
message:@"Concurrent requests to account signin"
details:nil]);
return NO;
}
_accountRequest = request;
return YES;
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options {
NSString *sourceApplication = options[UIApplicationOpenURLOptionsSourceApplicationKey];
return [[GIDSignIn sharedInstance] handleURL:url];
}
#pragma mark - <GIDSignInUIDelegate> protocol
- (void)signIn:(GIDSignIn *)signIn presentViewController:(UIViewController *)viewController {
UIViewController *rootViewController =
[UIApplication sharedApplication].delegate.window.rootViewController;
[rootViewController presentViewController:viewController animated:YES completion:nil];
}
- (void)signIn:(GIDSignIn *)signIn dismissViewController:(UIViewController *)viewController {
[viewController dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - <GIDSignInDelegate> protocol
- (void)signIn:(GIDSignIn *)signIn
didSignInForUser:(GIDGoogleUser *)user
withError:(NSError *)error {
if (error != nil) {
// Forward all errors and let Dart side decide how to handle.
[self respondWithAccount:nil error:error];
} else {
NSURL *photoUrl;
if (user.profile.hasImage) {
// Placeholder that will be replaced by on the Dart side based on screen
// size
photoUrl = [user.profile imageURLWithDimension:1337];
}
[self respondWithAccount:@{
@"displayName" : user.profile.name ?: [NSNull null],
@"email" : user.profile.email ?: [NSNull null],
@"id" : user.userID ?: [NSNull null],
@"photoUrl" : [photoUrl absoluteString] ?: [NSNull null],
}
error:nil];
}
}
- (void)signIn:(GIDSignIn *)signIn
didDisconnectWithUser:(GIDGoogleUser *)user
withError:(NSError *)error {
[self respondWithAccount:@{} error:nil];
}
#pragma mark - private methods
- (void)respondWithAccount:(id)account error:(NSError *)error {
FlutterResult result = _accountRequest;
_accountRequest = nil;
result(error != nil ? getFlutterError(error) : account);
}
- (UIViewController *)topViewController {
return [self topViewControllerFromViewController:[UIApplication sharedApplication]
.keyWindow.rootViewController];
}
/**
* This method recursively iterate through the view hierarchy
* to return the top most view controller.
*
* It supports the following scenarios:
*
* - The view controller is presenting another view.
* - The view controller is a UINavigationController.
* - The view controller is a UITabBarController.
*
* @return The top most view controller.
*/
- (UIViewController *)topViewControllerFromViewController:(UIViewController *)viewController {
if ([viewController isKindOfClass:[UINavigationController class]]) {
UINavigationController *navigationController = (UINavigationController *)viewController;
return [self
topViewControllerFromViewController:[navigationController.viewControllers lastObject]];
}
if ([viewController isKindOfClass:[UITabBarController class]]) {
UITabBarController *tabController = (UITabBarController *)viewController;
return [self topViewControllerFromViewController:tabController.selectedViewController];
}
if (viewController.presentedViewController) {
return [self topViewControllerFromViewController:viewController.presentedViewController];
}
return viewController;
}
@end