From 0244e34b9a89f84c8228119493ec63b9ea8a03db Mon Sep 17 00:00:00 2001 From: Simon Lightfoot Date: Thu, 22 Nov 2018 00:05:16 +0000 Subject: [PATCH] Improved error handling for Android and iOS. (#775) Updated FireAuth docs. Updated deprecated API "fetchProvidersForEmail" to "fetchSignInMethodsForEmail". Added "unlinkCredential". Updated changelog and brought wrapper inline with (#915). --- packages/firebase_auth/CHANGELOG.md | 10 + packages/firebase_auth/android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 5 + .../firebaseauth/FirebaseAuthPlugin.java | 304 +++++----- .../ios/Classes/FirebaseAuthPlugin.m | 127 ++-- packages/firebase_auth/lib/firebase_auth.dart | 554 ++++++++++++++++-- .../test/firebase_auth_test.dart | 178 +++++- 7 files changed, 917 insertions(+), 263 deletions(-) create mode 100644 packages/firebase_auth/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/firebase_auth/CHANGELOG.md b/packages/firebase_auth/CHANGELOG.md index 104de5398129..d0aa9da89192 100644 --- a/packages/firebase_auth/CHANGELOG.md +++ b/packages/firebase_auth/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.6.7 + +* `FirebaseAuth` and `FirebaseUser` are now fully documented. +* `PlatformExceptions` now report error codes as stated in docs. +* Credentials can now be unlinked from Accounts with new methods on `FirebaseUser`. + +## 0.6.6 + +* Users can now reauthenticate in response to operations that require a recent sign-in. + ## 0.6.5 * Fixing async method `verifyPhoneNumber`, that would never return even in a successful call. diff --git a/packages/firebase_auth/android/build.gradle b/packages/firebase_auth/android/build.gradle index 21495d43a35b..7f96901337ba 100755 --- a/packages/firebase_auth/android/build.gradle +++ b/packages/firebase_auth/android/build.gradle @@ -32,6 +32,6 @@ android { disable 'InvalidPackage' } dependencies { - api 'com.google.firebase:firebase-auth:16.0.4' + api 'com.google.firebase:firebase-auth:16.0.5' } } diff --git a/packages/firebase_auth/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_auth/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..019065d1d650 --- /dev/null +++ b/packages/firebase_auth/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java b/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java index a3a68ded7497..4bf1ce277a29 100755 --- a/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java +++ b/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java @@ -6,14 +6,34 @@ import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.SparseArray; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.FirebaseApiNotAvailableException; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; +import com.google.firebase.FirebaseNetworkException; import com.google.firebase.FirebaseTooManyRequestsException; -import com.google.firebase.auth.*; +import com.google.firebase.auth.AuthCredential; +import com.google.firebase.auth.AuthResult; +import com.google.firebase.auth.EmailAuthProvider; +import com.google.firebase.auth.FacebookAuthProvider; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuth.AuthStateListener; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.auth.GetTokenResult; +import com.google.firebase.auth.GithubAuthProvider; +import com.google.firebase.auth.GoogleAuthProvider; +import com.google.firebase.auth.PhoneAuthCredential; +import com.google.firebase.auth.PhoneAuthProvider; +import com.google.firebase.auth.PhoneAuthProvider.ForceResendingToken; +import com.google.firebase.auth.SignInMethodQueryResult; +import com.google.firebase.auth.TwitterAuthProvider; +import com.google.firebase.auth.UserInfo; +import com.google.firebase.auth.UserProfileChangeRequest; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -23,23 +43,20 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; /** Flutter plugin for Firebase Auth. */ public class FirebaseAuthPlugin implements MethodCallHandler { private final PluginRegistry.Registrar registrar; - private final SparseArray authStateListeners = - new SparseArray<>(); - private final SparseArray forceResendingTokens = - new SparseArray<>(); + private final SparseArray authStateListeners = new SparseArray<>(); + private final SparseArray forceResendingTokens = new SparseArray<>(); private final MethodChannel channel; // Handles are ints used as indexes into the sparse array of active observers private int nextHandle = 0; - private static final String ERROR_REASON_EXCEPTION = "exception"; - public static void registerWith(PluginRegistry.Registrar registrar) { MethodChannel channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/firebase_auth"); @@ -53,7 +70,7 @@ private FirebaseAuthPlugin(PluginRegistry.Registrar registrar, MethodChannel cha } private FirebaseAuth getAuth(MethodCall call) { - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String appName = (String) arguments.get("app"); FirebaseApp app = FirebaseApp.getInstance(appName); return FirebaseAuth.getInstance(app); @@ -71,8 +88,8 @@ public void onMethodCall(MethodCall call, Result result) { case "createUserWithEmailAndPassword": handleCreateUserWithEmailAndPassword(call, result, getAuth(call)); break; - case "fetchProvidersForEmail": - handleFetchProvidersForEmail(call, result, getAuth(call)); + case "fetchSignInMethodsForEmail": + handleFetchSignInMethodsForEmail(call, result, getAuth(call)); break; case "sendPasswordResetEmail": handleSendPasswordResetEmail(call, result, getAuth(call)); @@ -140,6 +157,9 @@ public void onMethodCall(MethodCall call, Result result) { case "linkWithGithubCredential": handleLinkWithGithubCredential(call, result, getAuth(call)); break; + case "unlinkCredential": + handleUnlinkCredential(call, result, getAuth(call)); + break; case "updateEmail": handleUpdateEmail(call, result, getAuth(call)); break; @@ -172,8 +192,7 @@ public void onMethodCall(MethodCall call, Result result) { private void handleSignInWithPhoneNumber( MethodCall call, Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String verificationId = arguments.get("verificationId"); String smsCode = arguments.get("smsCode"); @@ -186,10 +205,10 @@ private void handleSignInWithPhoneNumber( private void handleVerifyPhoneNumber( MethodCall call, Result result, final FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - final int handle = call.argument("handle"); - String phoneNumber = call.argument("phoneNumber"); - int timeout = call.argument("timeout"); + Map arguments = call.arguments(); + final int handle = (int) arguments.get("handle"); + String phoneNumber = (String) arguments.get("phoneNumber"); + int timeout = (int) arguments.get("timeout"); PhoneAuthProvider.OnVerificationStateChangedCallbacks verificationCallbacks = new PhoneAuthProvider.OnVerificationStateChangedCallbacks() { @@ -238,7 +257,7 @@ public void onCodeAutoRetrievalTimeOut(String verificationId) { }; if (call.argument("forceResendingToken") != null) { - int forceResendingTokenKey = call.argument("forceResendingToken"); + int forceResendingTokenKey = (int) arguments.get("forceResendingToken"); PhoneAuthProvider.ForceResendingToken forceResendingToken = forceResendingTokens.get(forceResendingTokenKey); PhoneAuthProvider.getInstance() @@ -263,9 +282,7 @@ public void onCodeAutoRetrievalTimeOut(String verificationId) { } private Map getVerifyPhoneNumberExceptionMap(FirebaseException e) { - Map exceptionMap = new HashMap<>(); String errorCode = "verifyPhoneNumberError"; - if (e instanceof FirebaseAuthInvalidCredentialsException) { errorCode = "invalidCredential"; } else if (e instanceof FirebaseAuthException) { @@ -275,6 +292,8 @@ private Map getVerifyPhoneNumberExceptionMap(FirebaseException e } else if (e instanceof FirebaseApiNotAvailableException) { errorCode = "apiNotAvailable"; } + + Map exceptionMap = new HashMap<>(); exceptionMap.put("code", errorCode); exceptionMap.put("message", e.getMessage()); return exceptionMap; @@ -282,8 +301,7 @@ private Map getVerifyPhoneNumberExceptionMap(FirebaseException e private void handleLinkWithEmailAndPassword( MethodCall call, Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String email = arguments.get("email"); String password = arguments.get("password"); @@ -294,7 +312,8 @@ private void handleLinkWithEmailAndPassword( .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleCurrentUser(MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + private void handleCurrentUser( + @SuppressWarnings("unused") MethodCall call, final Result result, FirebaseAuth firebaseAuth) { FirebaseUser user = firebaseAuth.getCurrentUser(); if (user == null) { result.success(null); @@ -305,14 +324,13 @@ private void handleCurrentUser(MethodCall call, final Result result, FirebaseAut } private void handleSignInAnonymously( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + @SuppressWarnings("unused") MethodCall call, Result result, FirebaseAuth firebaseAuth) { firebaseAuth.signInAnonymously().addOnCompleteListener(new SignInCompleteListener(result)); } private void handleCreateUserWithEmailAndPassword( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String email = arguments.get("email"); String password = arguments.get("password"); @@ -321,21 +339,19 @@ private void handleCreateUserWithEmailAndPassword( .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleFetchProvidersForEmail( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleFetchSignInMethodsForEmail( + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String email = arguments.get("email"); firebaseAuth - .fetchProvidersForEmail(email) - .addOnCompleteListener(new ProvidersCompleteListener(result)); + .fetchSignInMethodsForEmail(email) + .addOnCompleteListener(new GetSignInMethodsCompleteListener(result)); } private void handleSendPasswordResetEmail( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String email = arguments.get("email"); firebaseAuth @@ -344,24 +360,30 @@ private void handleSendPasswordResetEmail( } private void handleSendEmailVerification( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + @SuppressWarnings("unused") MethodCall call, Result result, FirebaseAuth firebaseAuth) { firebaseAuth .getCurrentUser() .sendEmailVerification() .addOnCompleteListener(new TaskVoidCompleteListener(result)); } - private void handleReload(MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + private void handleReload(MethodCall call, Result result, FirebaseAuth firebaseAuth) { firebaseAuth .getCurrentUser() .reload() .addOnCompleteListener(new TaskVoidCompleteListener(result)); } + private void handleDelete(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + firebaseAuth + .getCurrentUser() + .delete() + .addOnCompleteListener(new TaskVoidCompleteListener(result)); + } + private void handleSignInWithEmailAndPassword( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String email = arguments.get("email"); String password = arguments.get("password"); @@ -370,19 +392,11 @@ private void handleSignInWithEmailAndPassword( .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleDelete(MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - firebaseAuth - .getCurrentUser() - .delete() - .addOnCompleteListener(new TaskVoidCompleteListener(result)); - } - - private void handleSignInWithGoogle( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleSignInWithGoogle(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String idToken = arguments.get("idToken"); String accessToken = arguments.get("accessToken"); + AuthCredential credential = GoogleAuthProvider.getCredential(idToken, accessToken); firebaseAuth .signInWithCredential(credential) @@ -391,8 +405,7 @@ private void handleSignInWithGoogle( private void handleReauthenticateWithEmailAndPassword( MethodCall call, Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String email = arguments.get("email"); String password = arguments.get("password"); @@ -405,10 +418,10 @@ private void handleReauthenticateWithEmailAndPassword( private void handleReauthenticateWithGoogleCredential( MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String idToken = arguments.get("idToken"); String accessToken = arguments.get("accessToken"); + AuthCredential credential = GoogleAuthProvider.getCredential(idToken, accessToken); firebaseAuth .getCurrentUser() @@ -418,9 +431,9 @@ private void handleReauthenticateWithGoogleCredential( private void handleReauthenticateWithFacebookCredential( MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String accessToken = arguments.get("accessToken"); + AuthCredential credential = FacebookAuthProvider.getCredential(accessToken); firebaseAuth .getCurrentUser() @@ -430,10 +443,10 @@ private void handleReauthenticateWithFacebookCredential( private void handleReauthenticateWithTwitterCredential( MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String authToken = arguments.get("authToken"); String authTokenSecret = arguments.get("authTokenSecret"); + AuthCredential credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); firebaseAuth .getCurrentUser() @@ -444,6 +457,7 @@ private void handleReauthenticateWithTwitterCredential( private void handleReauthenticateWithGithubCredential( MethodCall call, final Result result, FirebaseAuth firebaseAuth) { String token = call.argument("token"); + AuthCredential credential = GithubAuthProvider.getCredential(token); firebaseAuth .getCurrentUser() @@ -452,11 +466,11 @@ private void handleReauthenticateWithGithubCredential( } private void handleLinkWithGoogleCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String idToken = arguments.get("idToken"); String accessToken = arguments.get("accessToken"); + AuthCredential credential = GoogleAuthProvider.getCredential(idToken, accessToken); firebaseAuth .getCurrentUser() @@ -465,10 +479,10 @@ private void handleLinkWithGoogleCredential( } private void handleLinkWithFacebookCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String accessToken = arguments.get("accessToken"); + AuthCredential credential = FacebookAuthProvider.getCredential(accessToken); firebaseAuth .getCurrentUser() @@ -476,63 +490,73 @@ private void handleLinkWithFacebookCredential( .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleLinkWithTwitterCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; - String authToken = arguments.get("authToken"); - String authTokenSecret = arguments.get("authTokenSecret"); - AuthCredential credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); - firebaseAuth - .getCurrentUser() - .linkWithCredential(credential) - .addOnCompleteListener(new SignInCompleteListener(result)); - } + private void handleSignInWithFacebook(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); + String accessToken = arguments.get("accessToken"); - private void handleLinkWithGithubCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - String token = call.argument("token"); - AuthCredential credential = GithubAuthProvider.getCredential(token); + AuthCredential credential = FacebookAuthProvider.getCredential(accessToken); firebaseAuth - .getCurrentUser() - .linkWithCredential(credential) + .signInWithCredential(credential) .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleSignInWithFacebook( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; - String accessToken = arguments.get("accessToken"); - AuthCredential credential = FacebookAuthProvider.getCredential(accessToken); + private void handleSignInWithTwitter(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + String authToken = call.argument("authToken"); + String authTokenSecret = call.argument("authTokenSecret"); + + AuthCredential credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); firebaseAuth .signInWithCredential(credential) .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleSignInWithTwitter( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + private void handleLinkWithTwitterCredential( + MethodCall call, Result result, FirebaseAuth firebaseAuth) { String authToken = call.argument("authToken"); String authTokenSecret = call.argument("authTokenSecret"); + AuthCredential credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); firebaseAuth - .signInWithCredential(credential) + .getCurrentUser() + .linkWithCredential(credential) .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleSignInWithGithub( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + private void handleSignInWithGithub(MethodCall call, Result result, FirebaseAuth firebaseAuth) { String token = call.argument("token"); + AuthCredential credential = GithubAuthProvider.getCredential(token); firebaseAuth .signInWithCredential(credential) .addOnCompleteListener(new SignInCompleteListener(result)); } + private void handleLinkWithGithubCredential( + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + String token = call.argument("token"); + + AuthCredential credential = GithubAuthProvider.getCredential(token); + firebaseAuth + .getCurrentUser() + .linkWithCredential(credential) + .addOnCompleteListener(new SignInCompleteListener(result)); + } + + private void handleUnlinkCredential(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); + final String provider = arguments.get("provider"); + + firebaseAuth + .getCurrentUser() + .unlink(provider) + .addOnCompleteListener(new SignInCompleteListener(result)); + } + private void handleSignInWithCustomToken( MethodCall call, final Result result, FirebaseAuth firebaseAuth) { Map arguments = call.arguments(); String token = arguments.get("token"); + firebaseAuth .signInWithCustomToken(token) .addOnCompleteListener(new SignInCompleteListener(result)); @@ -544,9 +568,9 @@ private void handleSignOut(MethodCall call, final Result result, FirebaseAuth fi } private void handleGetToken(MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); boolean refresh = arguments.get("refresh"); + firebaseAuth .getCurrentUser() .getIdToken(refresh) @@ -557,35 +581,34 @@ public void onComplete(@NonNull Task task) { String idToken = task.getResult().getToken(); result.success(idToken); } else { - result.error(ERROR_REASON_EXCEPTION, task.getException().getMessage(), null); + reportException(result, task.getException()); } } }); } - private void handleUpdateEmail(MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleUpdateEmail(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); + final String email = arguments.get("email"); + firebaseAuth .getCurrentUser() - .updateEmail(arguments.get("email")) + .updateEmail(email) .addOnCompleteListener(new TaskVoidCompleteListener(result)); } - private void handleUpdatePassword( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleUpdatePassword(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); + final String password = arguments.get("password"); + firebaseAuth .getCurrentUser() - .updatePassword(arguments.get("password")) + .updatePassword(password) .addOnCompleteListener(new TaskVoidCompleteListener(result)); } - private void handleUpdateProfile( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleUpdateProfile(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); UserProfileChangeRequest.Builder builder = new UserProfileChangeRequest.Builder(); if (arguments.containsKey("displayName")) { @@ -602,7 +625,7 @@ private void handleUpdateProfile( } private void handleStartListeningAuthState( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + @SuppressWarnings("unused") MethodCall call, Result result, FirebaseAuth firebaseAuth) { final int handle = nextHandle++; FirebaseAuth.AuthStateListener listener = new FirebaseAuth.AuthStateListener() { @@ -612,7 +635,6 @@ public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { Map userMap = mapFromUser(user); Map map = new HashMap<>(); map.put("id", handle); - if (userMap != null) { map.put("user", userMap); } @@ -625,7 +647,7 @@ public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { } private void handleStopListeningAuthState( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + MethodCall call, Result result, FirebaseAuth firebaseAuth) { Map arguments = call.arguments(); Integer id = arguments.get("id"); @@ -635,17 +657,16 @@ private void handleStopListeningAuthState( authStateListeners.remove(id); result.success(null); } else { - result.error( - ERROR_REASON_EXCEPTION, - String.format("Listener with identifier '%d' not found.", id), - null); + reportException( + result, + new FirebaseAuthException( + "ERROR_LISTENER_NOT_FOUND", + String.format(Locale.US, "Listener with identifier '%d' not found.", id))); } } - private void handleSetLanguageCode( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleSetLanguageCode(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String language = arguments.get("language"); firebaseAuth.setLanguageCode(language); @@ -661,9 +682,8 @@ private class SignInCompleteListener implements OnCompleteListener { @Override public void onComplete(@NonNull Task task) { - if (!task.isSuccessful()) { - Exception e = task.getException(); - result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); + if (!task.isSuccessful() || task.getResult() == null) { + reportException(result, task.getException()); } else { FirebaseUser user = task.getResult().getUser(); Map userMap = Collections.unmodifiableMap(mapFromUser(user)); @@ -682,28 +702,27 @@ private class TaskVoidCompleteListener implements OnCompleteListener { @Override public void onComplete(@NonNull Task task) { if (!task.isSuccessful()) { - Exception e = task.getException(); - result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); + reportException(result, task.getException()); } else { result.success(null); } } } - private class ProvidersCompleteListener implements OnCompleteListener { + private class GetSignInMethodsCompleteListener + implements OnCompleteListener { private final Result result; - ProvidersCompleteListener(Result result) { + GetSignInMethodsCompleteListener(Result result) { this.result = result; } @Override - public void onComplete(@NonNull Task task) { - if (!task.isSuccessful()) { - Exception e = task.getException(); - result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); + public void onComplete(@NonNull Task task) { + if (!task.isSuccessful() || task.getResult() == null) { + reportException(result, task.getException()); } else { - List providers = task.getResult().getProviders(); + List providers = task.getResult().getSignInMethods(); result.success(providers); } } @@ -749,4 +768,23 @@ private Map mapFromUser(FirebaseUser user) { return null; } } + + private void reportException(Result result, @Nullable Exception exception) { + if (exception != null) { + if (exception instanceof FirebaseAuthException) { + final FirebaseAuthException authException = (FirebaseAuthException) exception; + result.error(authException.getErrorCode(), exception.getMessage(), null); + } else if (exception instanceof FirebaseApiNotAvailableException) { + result.error("ERROR_API_NOT_AVAILABLE", exception.getMessage(), null); + } else if (exception instanceof FirebaseTooManyRequestsException) { + result.error("ERROR_TOO_MANY_REQUESTS", exception.getMessage(), null); + } else if (exception instanceof FirebaseNetworkException) { + result.error("ERROR_NETWORK_REQUEST_FAILED", exception.getMessage(), null); + } else { + result.error(exception.getClass().getSimpleName(), exception.getMessage(), null); + } + } else { + result.error("ERROR_UNKNOWN", "An unknown error occurred.", null); + } + } } diff --git a/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m index ebac80fea00e..5bbeaf30a77d 100644 --- a/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m +++ b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m @@ -6,15 +6,17 @@ #import "Firebase/Firebase.h" -@interface NSError (FlutterError) -@property(readonly, nonatomic) FlutterError *flutterError; +@interface NSError (FIRAuthErrorCode) +@property(readonly, nonatomic) NSString *firAuthErrorCode; @end -@implementation NSError (FlutterError) -- (FlutterError *)flutterError { - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)self.code] - message:self.domain - details:self.localizedDescription]; +@implementation NSError (FIRAuthErrorCode) +- (NSString *)firAuthErrorCode { + NSString *code = [self userInfo][FIRAuthErrorNameKey]; + if (code != nil) { + return code; + } + return [NSString stringWithFormat:@"ERROR_%d", (int)self.code]; } @end @@ -73,8 +75,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result }]; } else if ([@"signInAnonymously" isEqualToString:call.method]) { [[self getAuth:call.arguments] - signInAnonymouslyWithCompletion:^(FIRAuthDataResult *dataResult, NSError *error) { - [self sendResult:result forUser:dataResult.user error:error]; + signInAnonymouslyWithCompletion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forUser:authResult.user error:error]; }]; } else if ([@"signInWithGoogle" isEqualToString:call.method]) { NSString *idToken = call.arguments[@"idToken"]; @@ -114,35 +116,35 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments] createUserWithEmail:email password:password - completion:^(FIRAuthDataResult *dataResult, NSError *error) { - [self sendResult:result forUser:dataResult.user error:error]; + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forUser:authResult.user error:error]; }]; - } else if ([@"fetchProvidersForEmail" isEqualToString:call.method]) { + } else if ([@"fetchSignInMethodsForEmail" isEqualToString:call.method]) { NSString *email = call.arguments[@"email"]; [[self getAuth:call.arguments] fetchProvidersForEmail:email completion:^(NSArray *providers, NSError *error) { - [self sendResult:result forProviders:providers error:error]; + [self sendResult:result forObject:providers error:error]; }]; } else if ([@"sendEmailVerification" isEqualToString:call.method]) { [[self getAuth:call.arguments].currentUser sendEmailVerificationWithCompletion:^(NSError *_Nullable error) { - [self sendResult:result forProviders:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"reload" isEqualToString:call.method]) { [[self getAuth:call.arguments].currentUser reloadWithCompletion:^(NSError *_Nullable error) { - [self sendResult:result forProviders:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"delete" isEqualToString:call.method]) { [[self getAuth:call.arguments].currentUser deleteWithCompletion:^(NSError *_Nullable error) { - [self sendResult:result forProviders:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"sendPasswordResetEmail" isEqualToString:call.method]) { NSString *email = call.arguments[@"email"]; [[self getAuth:call.arguments] sendPasswordResetWithEmail:email completion:^(NSError *error) { [self sendResult:result - forUser:nil + forObject:nil error:error]; }]; } else if ([@"signInWithEmailAndPassword" isEqualToString:call.method]) { @@ -151,23 +153,23 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments] signInWithEmail:email password:password - completion:^(FIRAuthDataResult *dataResult, NSError *error) { - [self sendResult:result forUser:dataResult.user error:error]; + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forUser:authResult.user error:error]; }]; } else if ([@"signOut" isEqualToString:call.method]) { NSError *signOutError; BOOL status = [[self getAuth:call.arguments] signOut:&signOutError]; if (!status) { NSLog(@"Error signing out: %@", signOutError); - [self sendResult:result forUser:nil error:signOutError]; + [self sendResult:result forObject:nil error:signOutError]; } else { - [self sendResult:result forUser:nil error:nil]; + [self sendResult:result forObject:nil error:nil]; } } else if ([@"getIdToken" isEqualToString:call.method]) { [[self getAuth:call.arguments].currentUser getIDTokenForcingRefresh:YES completion:^(NSString *_Nullable token, NSError *_Nullable error) { - result(error != nil ? error.flutterError : token); + [self sendResult:result forObject:token error:error]; }]; } else if ([@"reauthenticateWithEmailAndPassword" isEqualToString:call.method]) { NSString *email = call.arguments[@"email"]; @@ -177,7 +179,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments].currentUser reauthenticateWithCredential:credential completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"reauthenticateWithGoogleCredential" isEqualToString:call.method]) { NSString *idToken = call.arguments[@"idToken"]; @@ -187,7 +189,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments].currentUser reauthenticateWithCredential:credential completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"reauthenticateWithFacebookCredential" isEqualToString:call.method]) { NSString *accessToken = call.arguments[@"accessToken"]; @@ -195,7 +197,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments].currentUser reauthenticateWithCredential:credential completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"reauthenticateWithTwitterCredential" isEqualToString:call.method]) { NSString *authToken = call.arguments[@"authToken"]; @@ -205,7 +207,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments].currentUser reauthenticateWithCredential:credential completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"reauthenticateWithGithubCredential" isEqualToString:call.method]) { NSString *token = call.arguments[@"token"]; @@ -213,7 +215,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments].currentUser reauthenticateWithCredential:credential completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"linkWithEmailAndPassword" isEqualToString:call.method]) { NSString *email = call.arguments[@"email"]; @@ -260,24 +262,32 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([@"linkWithGithubCredential" isEqualToString:call.method]) { NSString *token = call.arguments[@"token"]; FIRAuthCredential *credential = [FIRGitHubAuthProvider credentialWithToken:token]; - [[self getAuth:call.arguments].currentUser linkWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result - forUser:user - error:error]; - }]; + [[self getAuth:call.arguments].currentUser + linkWithCredential:credential + completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + [self sendResult:result forUser:user error:error]; + }]; + } else if ([@"unlinkCredential" isEqualToString:call.method]) { + NSString *provider = call.arguments[@"provider"]; + [[self getAuth:call.arguments].currentUser + unlinkFromProvider:provider + completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + [self sendResult:result forUser:user error:error]; + }]; } else if ([@"updateEmail" isEqualToString:call.method]) { NSString *email = call.arguments[@"email"]; [[self getAuth:call.arguments].currentUser updateEmail:email completion:^(NSError *error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result + forObject:nil + error:error]; }]; } else if ([@"updatePassword" isEqualToString:call.method]) { NSString *password = call.arguments[@"password"]; [[self getAuth:call.arguments].currentUser updatePassword:password completion:^(NSError *error) { [self sendResult:result - forUser:nil + forObject:nil error:error]; }]; } else if ([@"updateProfile" isEqualToString:call.method]) { @@ -290,20 +300,14 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result changeRequest.photoURL = [NSURL URLWithString:call.arguments[@"photoUrl"]]; } [changeRequest commitChangesWithCompletion:^(NSError *error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; - } else if ([@"updateEmail" isEqualToString:call.method]) { - NSString *toEmail = call.arguments[@"email"]; - [[self getAuth:call.arguments].currentUser updateEmail:toEmail - completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; - }]; } else if ([@"signInWithCustomToken" isEqualToString:call.method]) { NSString *token = call.arguments[@"token"]; [[self getAuth:call.arguments] signInWithCustomToken:token - completion:^(FIRAuthDataResult *dataResult, NSError *error) { - [self sendResult:result forUser:dataResult.user error:error]; + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forUser:authResult.user error:error]; }]; } else if ([@"startListeningAuthState" isEqualToString:call.method]) { @@ -332,7 +336,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result result(nil); } else { result([FlutterError - errorWithCode:@"not_found" + errorWithCode:@"ERROR_LISTENER_NOT_FOUND" message:[NSString stringWithFormat:@"Listener with identifier '%d' not found.", identifier.intValue] details:nil]); @@ -364,14 +368,15 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result FIRPhoneAuthCredential *credential = [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationId verificationCode:smsCode]; - [[self getAuth:call.arguments] signInWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result forUser:user error:error]; - }]; + [[self getAuth:call.arguments] + signInWithCredential:credential + completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + [self sendResult:result forUser:user error:error]; + }]; } else if ([@"setLanguageCode" isEqualToString:call.method]) { NSString *language = call.arguments[@"language"]; [[self getAuth:call.arguments] setLanguageCode:language]; - [self sendResult:result forUser:nil error:nil]; + [self sendResult:result forObject:nil error:nil]; } else { result(FlutterMethodNotImplemented); } @@ -397,24 +402,20 @@ - (NSMutableDictionary *)dictionaryFromUser:(FIRUser *)user { } - (void)sendResult:(FlutterResult)result forUser:(FIRUser *)user error:(NSError *)error { - if (error != nil) { - result(error.flutterError); - } else if (user == nil) { - result(nil); - } else { - result([self dictionaryFromUser:user]); - } + [self sendResult:result + forObject:(user != nil ? [self dictionaryFromUser:user] : nil) + error:error]; } -- (void)sendResult:(FlutterResult)result - forProviders:(NSArray *)providers - error:(NSError *)error { +- (void)sendResult:(FlutterResult)result forObject:(NSObject *)object error:(NSError *)error { if (error != nil) { - result(error.flutterError); - } else if (providers == nil) { + result([FlutterError errorWithCode:error.firAuthErrorCode + message:error.localizedDescription + details:nil]); + } else if (object == nil) { result(nil); } else { - result(providers); + result(object); } } diff --git a/packages/firebase_auth/lib/firebase_auth.dart b/packages/firebase_auth/lib/firebase_auth.dart index 9392695bf837..1c377e581ab6 100755 --- a/packages/firebase_auth/lib/firebase_auth.dart +++ b/packages/firebase_auth/lib/firebase_auth.dart @@ -52,6 +52,7 @@ class UserInfo { } /// Represents user profile data that can be updated by [updateProfile] +/// /// The purpose of having separate class with a map is to give possibility /// to check if value was set to null or not provided class UserUpdateInfo { @@ -59,13 +60,13 @@ class UserUpdateInfo { final Map _updateData = {}; set displayName(String displayName) => - _updateData["displayName"] = displayName; + _updateData['displayName'] = displayName; - String get displayName => _updateData["displayName"]; + String get displayName => _updateData['displayName']; - set photoUrl(String photoUri) => _updateData["photoUrl"] = photoUri; + set photoUrl(String photoUri) => _updateData['photoUrl'] = photoUri; - String get photoUrl => _updateData["photoUrl"]; + String get photoUrl => _updateData['photoUrl']; } /// Represents a user. @@ -92,6 +93,10 @@ class FirebaseUser extends UserInfo { /// Obtains the id token for the current user, forcing a [refresh] if desired. /// + /// Useful when authenticating against your own backend. Use our server + /// SDKs or follow the official documentation to securely verify the + /// integrity and validity of this token. + /// /// Completes with an error if the user is signed out. Future getIdToken({bool refresh = false}) async { return await FirebaseAuth.channel @@ -101,12 +106,14 @@ class FirebaseUser extends UserInfo { }); } + /// Initiates email verification for the user. Future sendEmailVerification() async { await FirebaseAuth.channel.invokeMethod( 'sendEmailVerification', {'app': _app.name}); } - /// Manually refreshes the data of the current user (for example, attached providers, display name, and so on). + /// Manually refreshes the data of the current user (for example, + /// attached providers, display name, and so on). Future reload() async { await FirebaseAuth.channel .invokeMethod('reload', {'app': _app.name}); @@ -119,6 +126,21 @@ class FirebaseUser extends UserInfo { } /// Updates the email address of the user. + /// + /// The original email address recipient will receive an email that allows + /// them to revoke the email address change, in order to protect them + /// from account hijacking. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the email address is malformed. + /// • `ERROR_EMAIL_ALREADY_IN_USE` - If the email is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future updateEmail(String email) async { assert(email != null); return await FirebaseAuth.channel.invokeMethod( @@ -128,6 +150,19 @@ class FirebaseUser extends UserInfo { } /// Updates the password of the user. + /// + /// Anonymous users who update both their email and password will no + /// longer be anonymous. They will be able to log in with these credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future updatePassword(String password) async { assert(password != null); return await FirebaseAuth.channel.invokeMethod( @@ -137,6 +172,10 @@ class FirebaseUser extends UserInfo { } /// Updates the user profile information. + /// + /// Errors: + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) Future updateProfile(UserUpdateInfo userUpdateInfo) async { assert(userUpdateInfo != null); final Map data = userUpdateInfo._updateData; @@ -147,6 +186,96 @@ class FirebaseUser extends UserInfo { ); } + /// Detaches Email & Password from this user. + /// + /// This detaches the Email & Password from the current user. This will + /// prevent the user from signing in to this account with those credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have an Email & Password linked to their account. + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + Future unlinkEmailAndPassword() async { + return await FirebaseAuth.channel.invokeMethod( + 'unlinkCredential', + {'provider': 'password', 'app': _app.name}, + ); + } + + /// Detaches Google from this user. + /// + /// This detaches the Google Account from the current user. This will + /// prevent the user from signing in to this account with those credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Google Account linked to their account. + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + Future unlinkGoogleCredential() async { + return await FirebaseAuth.channel.invokeMethod( + 'unlinkCredential', + {'provider': 'google.com', 'app': _app.name}, + ); + } + + /// Detaches Facebook from this user. + /// + /// This detaches the Facebook Account from the current user. This will + /// prevent the user from signing in to this account with those credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Facebook Account linked to their account. + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + Future unlinkFacebookCredential() async { + return await FirebaseAuth.channel.invokeMethod( + 'unlinkCredential', + {'provider': 'facebook.com', 'app': _app.name}, + ); + } + + /// Detaches Twitter from this user. + /// + /// This detaches the Twitter Account from the current user. This will + /// prevent the user from signing in to this account with those credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Twitter Account linked to their account. + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + Future unlinkTwitterCredential() async { + return await FirebaseAuth.channel.invokeMethod( + 'unlinkCredential', + {'provider': 'twitter.com', 'app': _app.name}, + ); + } + + /// Detaches Github from this user. + /// + /// This detaches the Github Account from the current user. This will + /// prevent the user from signing in to this account with those credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Github Account linked to their account. + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + Future unlinkGithubCredential() async { + return await FirebaseAuth.channel.invokeMethod( + 'unlinkCredential', + {'provider': 'github.com', 'app': _app.name}, + ); + } + @override String toString() { return '$runtimeType($_data)'; @@ -221,9 +350,11 @@ class FirebaseAuth { /// returned instead. If there is any other existing user signed in, that /// user will be signed out. /// - /// Will throw a PlatformException if - /// FIRAuthErrorCodeOperationNotAllowed - Indicates that anonymous accounts are not enabled. Enable them in the Auth section of the Firebase console. - /// See FIRAuthErrors for a list of error codes that are common to all API methods. + /// **Important**: You must enable Anonymous accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Anonymous accounts are not enabled. Future signInAnonymously() async { final Map data = await channel .invokeMethod('signInAnonymously', {"app": app.name}); @@ -231,6 +362,15 @@ class FirebaseAuth { return currentUser; } + /// Tries to create a new user account with the given email address and password. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// Errors: + /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. + /// • `ERROR_INVALID_CREDENTIAL` - If the email address is malformed. + /// • `ERROR_EMAIL_ALREADY_IN_USE` - If the email is already in use by a different account. Future createUserWithEmailAndPassword({ @required String email, @required String password, @@ -245,17 +385,33 @@ class FirebaseAuth { return currentUser; } - Future> fetchProvidersForEmail({ + /// Returns a list of sign-in methods that can be used to sign in a given + /// user (identified by its main email address). + /// + /// This method is useful when you support multiple authentication mechanisms + /// if you want to implement an email-first authentication flow. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [email] address is malformed. + /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address. + Future> fetchSignInMethodsForEmail({ @required String email, }) async { assert(email != null); final List providers = await channel.invokeMethod( - 'fetchProvidersForEmail', + 'fetchSignInMethodsForEmail', {'email': email, 'app': app.name}, ); return providers?.cast(); } + /// Triggers the Firebase Authentication backend to send a password-reset + /// email to the given email address, which must correspond to an existing + /// user of your app. + /// + /// Errors: + /// • `ERROR_INVALID_EMAIL` - If the [email] address is malformed. + /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address. Future sendPasswordResetEmail({ @required String email, }) async { @@ -266,6 +422,21 @@ class FirebaseAuth { ); } + /// Tries to sign in a user with the given email address and password. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// **Important**: You must enable Email & Password accounts in the Auth + /// section of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_EMAIL` - If the [email] address is malformed. + /// • `ERROR_WRONG_PASSWORD` - If the [password] is wrong. + /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address, or if the user has been deleted. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_TOO_MANY_REQUESTS` - If there was too many attempts to sign in as this user. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future signInWithEmailAndPassword({ @required String email, @required String password, @@ -280,36 +451,123 @@ class FirebaseAuth { return currentUser; } + /// Tries to sign in a user with the given Google [idToken] and [accessToken]. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// If the user doesn't have an account already, one will be created automatically. + /// + /// **Important**: You must enable Google accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Google. + /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. + /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Google accounts are not enabled. + Future signInWithGoogle({ + @required String idToken, + @required String accessToken, + }) async { + assert(idToken != null); + assert(accessToken != null); + final Map data = await channel.invokeMethod( + 'signInWithGoogle', + { + 'idToken': idToken, + 'accessToken': accessToken, + 'app': app.name, + }, + ); + final FirebaseUser currentUser = FirebaseUser._(data, app); + return currentUser; + } + + /// Tries to sign in a user with the given Facebook [accessToken]. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// If the user doesn't have an account already, one will be created automatically. + /// + /// **Important**: You must enable Facebook accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [accessToken] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Facebook. + /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. + /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Facebook accounts are not enabled. Future signInWithFacebook( {@required String accessToken}) async { assert(accessToken != null); - final Map data = await channel.invokeMethod( - 'signInWithFacebook', - {'accessToken': accessToken, 'app': app.name}); + final Map data = + await channel.invokeMethod('signInWithFacebook', { + 'accessToken': accessToken, + 'app': app.name, + }); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } - /// Signs in with a Twitter account using the specified credentials. + /// Tries to sign in a user with the given Twitter [authToken] and [authTokenSecret]. /// - /// The returned future completes with the signed-in user or a [PlatformException], if sign in failed. + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// If the user doesn't have an account already, one will be created automatically. + /// + /// **Important**: You must enable Twitter accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Twitter. + /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. + /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Twitter accounts are not enabled. Future signInWithTwitter({ @required String authToken, @required String authTokenSecret, }) async { assert(authToken != null); assert(authTokenSecret != null); - final Map data = await channel.invokeMethod( - 'signInWithTwitter', { + final Map data = + await channel.invokeMethod('signInWithTwitter', { 'authToken': authToken, 'authTokenSecret': authTokenSecret, - 'app': app.name + 'app': app.name, }); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } - Future signInWithGithub({@required String token}) async { + /// Tries to sign in a user with the given Github [token]. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// If the user doesn't have an account already, one will be created automatically. + /// + /// **Important**: You must enable Github accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [token] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Github. + /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. + /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Github accounts are not enabled. + Future signInWithGithub({ + @required String token, + }) async { assert(token != null); final Map data = await channel.invokeMethod('signInWithGithub', { @@ -320,24 +578,20 @@ class FirebaseAuth { return currentUser; } - Future signInWithGoogle({ - @required String idToken, - @required String accessToken, - }) async { - assert(idToken != null); - assert(accessToken != null); - final Map data = await channel.invokeMethod( - 'signInWithGoogle', - { - 'idToken': idToken, - 'accessToken': accessToken, - 'app': app.name - }, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - + /// Tries to sign in a user with the given Phone [verificationId] and [smsCode]. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// If the user doesn't have an account already, one will be created automatically. + /// + /// **Important**: You must enable Phone accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [verificationId] or [smsCode] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Phone accounts are not enabled. Future signInWithPhoneNumber({ @required String verificationId, @required String smsCode, @@ -347,13 +601,55 @@ class FirebaseAuth { { 'verificationId': verificationId, 'smsCode': smsCode, - 'app': app.name + 'app': app.name, }, ); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } + /// Starts the phone number verification process for the given phone number. + /// + /// Either sends an SMS with a 6 digit code to the phone number specified, + /// or sign's the user in and [verificationCompleted] is called. + /// + /// No duplicated SMS will be sent out upon re-entry (before timeout). + /// + /// Make sure to test all scenarios below: + /// • You directly get logged in if Google Play Services verified the phone + /// number instantly or helped you auto-retrieve the verification code. + /// • Auto-retrieve verification code timed out. + /// • Error cases when you receive [verificationFailed] callback. + /// + /// [phoneNumber] The phone number for the account the user is signing up + /// for or signing into. Make sure to pass in a phone number with country + /// code prefixed with plus sign ('+'). + /// + /// [timeout] The maximum amount of time you are willing to wait for SMS + /// auto-retrieval to be completed by the library. Maximum allowed value + /// is 2 minutes. Use 0 to disable SMS-auto-retrieval. Setting this to 0 + /// will also cause [codeAutoRetrievalTimeout] to be called immediately. + /// If you specified a positive value less than 30 seconds, library will + /// default to 30 seconds. + /// + /// [forceResendingToken] The [forceResendingToken] obtained from [codeSent] + /// callback to force re-sending another verification SMS before the + /// auto-retrieval timeout. + /// + /// [verificationCompleted] This callback must be implemented. + /// It will trigger when an SMS is auto-retrieved or the phone number has + /// been instantly verified. The callback will provide a [FirebaseUser]. + /// + /// [verificationFailed] This callback must be implemented. + /// Triggered when an error occurred during phone number verification. + /// + /// [codeSent] Optional callback. + /// It will trigger when an SMS has been sent to the users phone, + /// and will include a [verificationId] and [forceResendingToken]. + /// + /// [codeAutoRetrievalTimeout] Optional callback. + /// It will trigger when SMS auto-retrieval times out and provide a + /// [verificationId]. Future verifyPhoneNumber({ @required String phoneNumber, @required Duration timeout, @@ -377,12 +673,30 @@ class FirebaseAuth { 'phoneNumber': phoneNumber, 'timeout': timeout.inMilliseconds, 'forceResendingToken': forceResendingToken, - 'app': app.name + 'app': app.name, }; await channel.invokeMethod('verifyPhoneNumber', params); } + /// Tries to sign in a user with a given Custom Token [token]. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// Use this method after you retrieve a Firebase Auth Custom Token from your server. + /// + /// If the user identified by the [uid] specified in the token doesn't + /// have an account already, one will be created automatically. + /// + /// Read how to use Custom Token authentication and the cases where it is + /// useful in [the guides](https://firebase.google.com/docs/auth/android/custom-auth). + /// + /// Errors: + /// • `ERROR_INVALID_CUSTOM_TOKEN` - The custom token format is incorrect. + /// Please check the documentation. + /// • `ERROR_CUSTOM_TOKEN_MISMATCH` - Invalid configuration. + /// Ensure your app's SHA1 is correct in the Firebase console. Future signInWithCustomToken({@required String token}) async { assert(token != null); final Map data = await channel.invokeMethod( @@ -393,12 +707,16 @@ class FirebaseAuth { return currentUser; } + /// Signs out the current user and clears it from the disk cache. + /// + /// If successful, it signs the user out of the app and updates + /// the [onAuthStateChanged] stream. Future signOut() async { return await channel .invokeMethod("signOut", {'app': app.name}); } - /// Asynchronously gets current user, or `null` if there is none. + /// Returns the currently signed-in [FirebaseUser] or [null] if there is none. Future currentUser() async { final Map data = await channel .invokeMethod("currentUser", {'app': app.name}); @@ -407,12 +725,19 @@ class FirebaseAuth { return currentUser; } - /// Links email account with current user and returns [Future] - /// basically current user with additional email information + /// Links the given [email] and [password] to the current user. + /// + /// This allows the user to sign in to this account in the future with + /// the given [email] and [password]. /// - /// throws [PlatformException] when - /// 1. email address is already used - /// 2. wrong email and password provided + /// Errors: + /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. + /// • `ERROR_INVALID_CREDENTIAL` - If the email address is malformed. + /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the email is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has an Email & Password linked. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future linkWithEmailAndPassword({ @required String email, @required String password, @@ -427,14 +752,18 @@ class FirebaseAuth { return currentUser; } - /// Links google account with current user and returns [Future] + /// Links the Google Account to the current user using [idToken] and [accessToken]. + /// + /// This allows the user to sign in to this account in the future with + /// the given Google Account. /// - /// throws [PlatformException] when - /// 1. No current user provided (user has not logged in) - /// 2. No google credentials were found for given [idToken] and [accessToken] - /// 3. Google account already linked with another [FirebaseUser] - /// Detailed documentation on possible error causes can be found in [Android docs](https://firebase.google.com/docs/reference/android/com/google/firebase/auth/FirebaseUser#exceptions_4) and [iOS docs](https://firebase.google.com/docs/reference/ios/firebaseauth/api/reference/Classes/FIRUser#/c:objc(cs)FIRUser(im)linkWithCredential:completion:) - /// TODO: Throw custom exceptions with error codes indicating cause of exception + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [idToken] or [accessToken] is malformed or has expired. + /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Google account is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Google account linked. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Google accounts are not enabled. Future linkWithGoogleCredential({ @required String idToken, @required String accessToken, @@ -446,51 +775,107 @@ class FirebaseAuth { { 'idToken': idToken, 'accessToken': accessToken, - 'app': app.name + 'app': app.name, }, ); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } + /// Links the Facebook Account to the current user using [accessToken]. + /// + /// This allows the user to sign in to this account in the future with + /// the given Facebook Account. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [accessToken] is malformed or has expired. + /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Facebook account is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Facebook account linked. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Facebook accounts are not enabled. Future linkWithFacebookCredential({ @required String accessToken, }) async { assert(accessToken != null); final Map data = await channel.invokeMethod( 'linkWithFacebookCredential', - {'accessToken': accessToken, 'app': app.name}, + { + 'accessToken': accessToken, + 'app': app.name, + }, ); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } + /// Links the Twitter Account to the current user using [authToken] and [authTokenSecret]. + /// + /// This allows the user to sign in to this account in the future with + /// the given Twitter Account. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. + /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Twitter account is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Twitter account linked. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Twitter accounts are not enabled. Future linkWithTwitterCredential({ @required String authToken, @required String authTokenSecret, }) async { + assert(authToken != null); + assert(authTokenSecret != null); final Map data = await channel.invokeMethod( 'linkWithTwitterCredential', { - 'app': app.name, 'authToken': authToken, 'authTokenSecret': authTokenSecret, + 'app': app.name, }, ); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } - Future linkWithGithubCredential( - {@required String token}) async { + /// Links the Github Account to the current user using [token]. + /// + /// This allows the user to sign in to this account in the future with + /// the given Github Account. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [token] is malformed or has expired. + /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Github account is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Github account linked. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Github accounts are not enabled. + Future linkWithGithubCredential({ + @required String token, + }) async { assert(token != null); final Map data = await channel.invokeMethod( - 'linkWithGithubCredential', - {'app': app.name, 'token': token}); + 'linkWithGithubCredential', + { + 'app': app.name, + 'token': token, + }, + ); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } + /// Reauthenticates the current user with given [email] and [password]. + /// + /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` + /// response to operations that require a recent sign-in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [email] and/or [password] are incorrect. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future reauthenticateWithEmailAndPassword({ @required String email, @required String password, @@ -503,6 +888,16 @@ class FirebaseAuth { ); } + /// Reauthenticates the current user with the Google Account specified by [idToken] and [accessToken]. + /// + /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` + /// response to operations that require a recent sign-in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [idToken] or [accessToken] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future reauthenticateWithGoogleCredential({ @required String idToken, @required String accessToken, @@ -519,6 +914,16 @@ class FirebaseAuth { ); } + /// Reauthenticates the current user with the Facebook Account specified by [accessToken]. + /// + /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` + /// response to operations that require a recent sign-in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [accessToken] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future reauthenticateWithFacebookCredential({ @required String accessToken, }) { @@ -529,6 +934,16 @@ class FirebaseAuth { ); } + /// Reauthenticates the current user with the Twitter Account specified by [authToken] and [authTokenSecret]. + /// + /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` + /// response to operations that require a recent sign-in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future reauthenticateWithTwitterCredential({ @required String authToken, @required String authTokenSecret, @@ -543,10 +958,25 @@ class FirebaseAuth { ); } + /// Reauthenticates the current user with the Github Account specified by [token]. + /// + /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` + /// response to operations that require a recent sign-in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [token] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future reauthenticateWithGithubCredential({@required String token}) { assert(token != null); - return channel.invokeMethod('reauthenticateWithGithubCredential', - {'app': app.name, 'token': token}); + return channel.invokeMethod( + 'reauthenticateWithGithubCredential', + { + 'app': app.name, + 'token': token, + }, + ); } /// Sets the user-facing language code for auth operations that can be @@ -560,7 +990,7 @@ class FirebaseAuth { }); } - Future _callHandler(MethodCall call) async { + Future _callHandler(MethodCall call) async { switch (call.method) { case 'onAuthStateChanged': _onAuthStageChangedHandler(call); diff --git a/packages/firebase_auth/test/firebase_auth_test.dart b/packages/firebase_auth/test/firebase_auth_test.dart index 868d4ff3e9cd..ae83be64e65e 100755 --- a/packages/firebase_auth/test/firebase_auth_test.dart +++ b/packages/firebase_auth/test/firebase_auth_test.dart @@ -52,7 +52,7 @@ void main() { case "updateProfile": return null; break; - case "fetchProvidersForEmail": + case "fetchSignInMethodsForEmail": return List(0); break; case "verifyPhoneNumber": @@ -135,16 +135,16 @@ void main() { ); }); - test('fetchProvidersForEmail', () async { + test('fetchSignInMethodsForEmail', () async { final List providers = - await auth.fetchProvidersForEmail(email: kMockEmail); + await auth.fetchSignInMethodsForEmail(email: kMockEmail); expect(providers, isNotNull); expect(providers.length, 0); expect( log, [ isMethodCall( - 'fetchProvidersForEmail', + 'fetchSignInMethodsForEmail', arguments: { 'email': kMockEmail, 'app': auth.app.name @@ -154,6 +154,86 @@ void main() { ); }); + test('linkWithTwitterCredential', () async { + final FirebaseUser user = await auth.linkWithTwitterCredential( + authToken: kMockIdToken, + authTokenSecret: kMockAccessToken, + ); + verifyUser(user); + expect( + log, + [ + isMethodCall( + 'linkWithTwitterCredential', + arguments: { + 'authToken': kMockIdToken, + 'authTokenSecret': kMockAccessToken, + 'app': auth.app.name, + }, + ), + ], + ); + }); + + test('signInWithTwitter', () async { + final FirebaseUser user = await auth.signInWithTwitter( + authToken: kMockIdToken, + authTokenSecret: kMockAccessToken, + ); + verifyUser(user); + expect( + log, + [ + isMethodCall( + 'signInWithTwitter', + arguments: { + 'authToken': kMockIdToken, + 'authTokenSecret': kMockAccessToken, + 'app': auth.app.name, + }, + ), + ], + ); + }); + + test('linkWithGithubCredential', () async { + final FirebaseUser user = await auth.linkWithGithubCredential( + token: kMockGithubToken, + ); + verifyUser(user); + expect( + log, + [ + isMethodCall( + 'linkWithGithubCredential', + arguments: { + 'token': kMockGithubToken, + 'app': auth.app.name, + }, + ), + ], + ); + }); + + test('signInWithGithub', () async { + final FirebaseUser user = await auth.signInWithGithub( + token: kMockGithubToken, + ); + verifyUser(user); + expect( + log, + [ + isMethodCall( + 'signInWithGithub', + arguments: { + 'token': kMockGithubToken, + 'app': auth.app.name, + }, + ), + ], + ); + }); + test('linkWithEmailAndPassword', () async { final FirebaseUser user = await auth.linkWithEmailAndPassword( email: kMockEmail, @@ -637,6 +717,96 @@ void main() { ]); }); + test('unlinkEmailAndPassword', () async { + final FirebaseUser user = await auth.currentUser(); + await user.unlinkEmailAndPassword(); + expect(log, [ + isMethodCall( + 'currentUser', + arguments: {'app': auth.app.name}, + ), + isMethodCall( + 'unlinkCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'password', + }, + ), + ]); + }); + + test('unlinkGoogleCredential', () async { + final FirebaseUser user = await auth.currentUser(); + await user.unlinkGoogleCredential(); + expect(log, [ + isMethodCall( + 'currentUser', + arguments: {'app': auth.app.name}, + ), + isMethodCall( + 'unlinkCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'google.com', + }, + ), + ]); + }); + + test('unlinkFacebookCredential', () async { + final FirebaseUser user = await auth.currentUser(); + await user.unlinkFacebookCredential(); + expect(log, [ + isMethodCall( + 'currentUser', + arguments: {'app': auth.app.name}, + ), + isMethodCall( + 'unlinkCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'facebook.com', + }, + ), + ]); + }); + + test('unlinkTwitterCredential', () async { + final FirebaseUser user = await auth.currentUser(); + await user.unlinkTwitterCredential(); + expect(log, [ + isMethodCall( + 'currentUser', + arguments: {'app': auth.app.name}, + ), + isMethodCall( + 'unlinkCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'twitter.com', + }, + ), + ]); + }); + + test('unlinkGithubCredential', () async { + final FirebaseUser user = await auth.currentUser(); + await user.unlinkGithubCredential(); + expect(log, [ + isMethodCall( + 'currentUser', + arguments: {'app': auth.app.name}, + ), + isMethodCall( + 'unlinkCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'github.com', + }, + ), + ]); + }); + test('signInWithCustomToken', () async { final FirebaseUser user = await auth.signInWithCustomToken(token: kMockCustomToken);