From 1b85c8b7fbcc3f21767f23981cb35061772d483f Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 20 Jul 2022 09:13:38 +0200 Subject: [PATCH] feat(auth): add phone MFA (#9044) * feat(auth, android): add phone MFA to Android (#8998) * chores: update format CI step to match local melos format * chores: update format CI step to match local melos format * feat(auth, android): mfa * feat(auth, android): add MFA to phone options * feat(auth, android): add phone MFA * feat(auth, android): add MFA signin * feat(auth): add documentation * feat(auth, android): fix typings * feat(auth, android): fix tests * feat(auth, android): add input for phone number to example app * feat(auth, android): add unenroll and getEnrolledFactors * feat(auth, android): fix formatting * feat(auth, android): fix formatting * feat(ui, android): use mocktail for mocking dependencies * feat(auth, android): fix analyze * feat(auth): fix melos generate command * Update packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart Co-authored-by: Mike Diarmid Co-authored-by: Mike Diarmid * feat(auth, ios): add phone MFA (#9008) * chores: update format CI step to match local melos format * chores: update format CI step to match local melos format * feat(auth, android): mfa * feat(auth, android): add MFA to phone options * feat(auth, android): add phone MFA * feat(auth, android): add MFA signin * feat(auth, android): fix tests * feat(auth, android): add input for phone number to example app * feat(auth, android): add unenroll and getEnrolledFactors * feat(auth, android): fix formatting * feat(auth, android): fix formatting * feat(auth, android): fix analyze * feat(auth, ios): implement enroll * feat(auth, ios): implement MFA for signin * feat(auth, ios): add conditions to allow macos to build * feat(auth, ios): add conditions to allow macos to build * feat(auth, ios): fix ios build * Update packages/firebase_auth/firebase_auth/ios/Classes/FLTFirebaseAuthPlugin.m Co-authored-by: Mike Diarmid * fix: fix rebase Co-authored-by: Mike Diarmid * feat(auth): fix android merge * feat(auth): fix ios merge * test(auth): add MFA e2e (#9034) * feat(auth, android): add phone MFA to Android (#8998) * chores: update format CI step to match local melos format * chores: update format CI step to match local melos format * feat(auth, android): mfa * feat(auth, android): add MFA to phone options * feat(auth, android): add phone MFA * feat(auth, android): add MFA signin * feat(auth): add documentation * feat(auth, android): fix typings * feat(auth, android): fix tests * feat(auth, android): add input for phone number to example app * feat(auth, android): add unenroll and getEnrolledFactors * feat(auth, android): fix formatting * feat(auth, android): fix formatting * feat(ui, android): use mocktail for mocking dependencies * feat(auth, android): fix analyze * feat(auth): fix melos generate command * Update packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart Co-authored-by: Mike Diarmid Co-authored-by: Mike Diarmid * feat(auth, ios): add phone MFA (#9008) * chores: update format CI step to match local melos format * chores: update format CI step to match local melos format * feat(auth, android): mfa * feat(auth, android): add MFA to phone options * feat(auth, android): add phone MFA * feat(auth, android): add MFA signin * feat(auth, android): fix tests * feat(auth, android): add input for phone number to example app * feat(auth, android): add unenroll and getEnrolledFactors * feat(auth, android): fix formatting * feat(auth, android): fix formatting * feat(auth, android): fix analyze * feat(auth, ios): implement enroll * feat(auth, ios): implement MFA for signin * feat(auth, ios): add conditions to allow macos to build * feat(auth, ios): add conditions to allow macos to build * feat(auth, ios): fix ios build * Update packages/firebase_auth/firebase_auth/ios/Classes/FLTFirebaseAuthPlugin.m Co-authored-by: Mike Diarmid * fix: fix rebase Co-authored-by: Mike Diarmid * feat(auth): fix android merge * feat(auth): fix ios merge * feat(auth): add e2e tests * fix(auth): fix reintroduce tests * feat(auth): add documentation * feat(auth): add e2e tests Co-authored-by: Mike Diarmid * feat(auth, web): add phone mfa (#9031) * feat(auth): add multifactoruser * feat(auth, web): bridge * feat(auth, web): solve bridging errors * feat(auth, web): finish phone web mfa * feat(auth, web): finish phone web mfa * feat(auth, web): fix analyze * feat(auth, web): fix analyze * feat(auth, web): fix wrong argument * feat(auth, android): add early return * feat(auth, android): change dynamic to Object Co-authored-by: Mike Diarmid --- melos.yaml | 5 +- packages/firebase_auth/analysis_options.yaml | 11 + .../plugins/firebase/auth/Constants.java | 6 + .../auth/FlutterFirebaseAuthPlugin.java | 284 ++++++- .../FlutterFirebaseAuthPluginException.java | 9 + .../auth/GeneratedAndroidFirebaseAuth.java | 698 ++++++++++++++++++ .../PhoneNumberVerificationStreamHandler.java | 22 +- .../firebase_auth/example/lib/auth.dart | 125 ++-- .../firebase_auth/example/lib/profile.dart | 56 +- .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../ios/Classes/FLTFirebaseAuthPlugin.m | 247 ++++++- .../FLTPhoneNumberVerificationStreamHandler.m | 38 +- .../ios/Classes/Private/CustomPigeonHeader.h | 8 + .../FLTPhoneNumberVerificationStreamHandler.h | 7 + .../Classes/Public/FLTFirebaseAuthPlugin.h | 4 +- .../firebase_auth/ios/Classes/messages.g.h | 92 +++ .../firebase_auth/ios/Classes/messages.g.m | 464 ++++++++++++ .../firebase_auth/lib/firebase_auth.dart | 13 +- .../firebase_auth/lib/src/firebase_auth.dart | 19 +- .../firebase_auth/lib/src/multi_factor.dart | 53 ++ .../lib/src/recaptcha_verifier.dart | 2 + .../firebase_auth/lib/src/user.dart | 7 +- .../Classes/Private/CustomPigeonHeader.h | 1 + .../firebase_auth/macos/Classes/messages.g.h | 1 + .../firebase_auth/macos/Classes/messages.g.m | 1 + .../test/firebase_auth_test.dart | 19 +- .../firebase_auth/test/user_test.dart | 16 +- .../lib/firebase_auth_platform_interface.dart | 2 + .../firebase_auth_multi_factor_exception.dart | 33 + .../method_channel_firebase_auth.dart | 47 +- .../method_channel_multi_factor.dart | 151 ++++ .../method_channel/method_channel_user.dart | 15 +- .../method_channel_user_credential.dart | 5 +- .../src/method_channel/utils/exception.dart | 66 ++ .../method_channel/utils/pigeon_helper.dart | 25 + .../lib/src/pigeon/messages.pigeon.dart | 391 ++++++++++ .../platform_interface_firebase_auth.dart | 5 +- .../platform_interface_multi_factor.dart | 163 ++++ ..._interface_recaptcha_verifier_factory.dart | 1 + .../platform_interface_user.dart | 4 +- .../pigeons/messages.dart | 89 +++ .../pubspec.yaml | 6 +- .../method_channel_firebase_auth_test.dart | 14 +- .../test/pigeon/test_api.dart | 215 ++++++ ...rface_recaptcha_verifier_factory_test.dart | 4 +- ...atform_interface_user_credential_test.dart | 15 +- .../platform_interface_user_test.dart | 9 +- .../lib/firebase_auth_web.dart | 117 ++- ...firebase_auth_web_confirmation_result.dart | 3 +- .../src/firebase_auth_web_multi_factor.dart | 144 ++++ ...e_auth_web_recaptcha_verifier_factory.dart | 27 +- .../lib/src/firebase_auth_web_user.dart | 7 +- .../firebase_auth_web_user_credential.dart | 9 +- .../lib/src/interop/auth.dart | 37 +- .../lib/src/interop/auth_interop.dart | 118 ++- .../lib/src/interop/multi_factor.dart | 163 ++++ .../lib/src/interop/utils/utils.dart | 7 +- .../lib/src/utils/web_utils.dart | 66 +- packages/firebase_core/analysis_options.yaml | 3 +- .../core/GeneratedAndroidFirebaseCore.java | 2 +- .../firebase_core/ios/Classes/messages.g.h | 2 +- .../firebase_core/ios/Classes/messages.g.m | 35 +- .../lib/src/pigeon/messages.pigeon.dart | 54 +- .../lib/src/pigeon/test_api.dart | 136 ++-- packages/flutterfire_ui/pubspec.yaml | 5 +- .../test/auth/widgets/email_form_test.dart | 5 +- .../firebase_auth/firebase_auth_e2e.dart | 7 +- .../firebase_auth_multi_factor_e2e.dart | 404 ++++++++++ .../test_driver/firebase_auth/test_utils.dart | 11 + 69 files changed, 4485 insertions(+), 353 deletions(-) create mode 100644 packages/firebase_auth/analysis_options.yaml create mode 100644 packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/GeneratedAndroidFirebaseAuth.java create mode 100644 packages/firebase_auth/firebase_auth/example/macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/firebase_auth/firebase_auth/ios/Classes/Private/CustomPigeonHeader.h create mode 100644 packages/firebase_auth/firebase_auth/ios/Classes/messages.g.h create mode 100644 packages/firebase_auth/firebase_auth/ios/Classes/messages.g.m create mode 100644 packages/firebase_auth/firebase_auth/lib/src/multi_factor.dart create mode 120000 packages/firebase_auth/firebase_auth/macos/Classes/Private/CustomPigeonHeader.h create mode 120000 packages/firebase_auth/firebase_auth/macos/Classes/messages.g.h create mode 120000 packages/firebase_auth/firebase_auth/macos/Classes/messages.g.m create mode 100644 packages/firebase_auth/firebase_auth_platform_interface/lib/src/firebase_auth_multi_factor_exception.dart create mode 100644 packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart create mode 100644 packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/pigeon_helper.dart create mode 100644 packages/firebase_auth/firebase_auth_platform_interface/lib/src/pigeon/messages.pigeon.dart create mode 100644 packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_multi_factor.dart create mode 100644 packages/firebase_auth/firebase_auth_platform_interface/pigeons/messages.dart create mode 100644 packages/firebase_auth/firebase_auth_platform_interface/test/pigeon/test_api.dart create mode 100644 packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_multi_factor.dart create mode 100644 packages/firebase_auth/firebase_auth_web/lib/src/interop/multi_factor.dart create mode 100644 tests/test_driver/firebase_auth/firebase_auth_multi_factor_e2e.dart diff --git a/melos.yaml b/melos.yaml index bd54cbfe4e24..67c52d833139 100644 --- a/melos.yaml +++ b/melos.yaml @@ -186,8 +186,9 @@ scripts: generate:pigeon: run: | - melos exec -- "flutter pub run pigeon --input ./pigeons/messages.dart" - melos run generate:pigeon:macos + melos exec -- "flutter pub run pigeon --input ./pigeons/messages.dart" && \ + melos run generate:pigeon:macos --no-select && \ + melos run format --no-select select-package: file-exists: 'pigeons/messages.dart' description: Generate the pigeon messages for all the supported packages. diff --git a/packages/firebase_auth/analysis_options.yaml b/packages/firebase_auth/analysis_options.yaml new file mode 100644 index 000000000000..8b3451b56a01 --- /dev/null +++ b/packages/firebase_auth/analysis_options.yaml @@ -0,0 +1,11 @@ +# Copyright 2021 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# in the LICENSE file. + +include: ../../analysis_options.yaml + +analyzer: + # TODO(Lyokone): not working if added on the root analysis file + exclude: + - firebase_auth_platform_interface/lib/src/pigeon/messages.pigeon.dart + - firebase_auth_platform_interface/test/pigeon/test_api.dart diff --git a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/Constants.java b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/Constants.java index 32ae211171c6..0d2151ac317c 100644 --- a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/Constants.java +++ b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/Constants.java @@ -83,4 +83,10 @@ public class Constants { public static final String APP_VERIFICATION_DISABLED_FOR_TESTING = "appVerificationDisabledForTesting"; public static final String FORCE_RECAPTCHA_FLOW = "forceRecaptchaFlow"; + + // MultiFactor + public static final String MULTI_FACTOR_HINTS = "multiFactorHints"; + public static final String MULTI_FACTOR_SESSION_ID = "multiFactorSessionId"; + public static final String MULTI_FACTOR_RESOLVER_ID = "multiFactorResolverId"; + public static final String MULTI_FACTOR_INFO = "multiFactorInfo"; } diff --git a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPlugin.java b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPlugin.java index 74802ec67ce3..97c8e2379d49 100755 --- a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPlugin.java +++ b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPlugin.java @@ -28,19 +28,28 @@ import com.google.firebase.auth.FacebookAuthProvider; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseAuthMultiFactorException; import com.google.firebase.auth.FirebaseAuthProvider; import com.google.firebase.auth.FirebaseUser; import com.google.firebase.auth.FirebaseUserMetadata; import com.google.firebase.auth.GetTokenResult; import com.google.firebase.auth.GithubAuthProvider; import com.google.firebase.auth.GoogleAuthProvider; +import com.google.firebase.auth.MultiFactor; +import com.google.firebase.auth.MultiFactorAssertion; +import com.google.firebase.auth.MultiFactorInfo; +import com.google.firebase.auth.MultiFactorResolver; +import com.google.firebase.auth.MultiFactorSession; import com.google.firebase.auth.OAuthProvider; import com.google.firebase.auth.PhoneAuthCredential; import com.google.firebase.auth.PhoneAuthProvider; +import com.google.firebase.auth.PhoneMultiFactorGenerator; +import com.google.firebase.auth.PhoneMultiFactorInfo; 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 com.google.firebase.internal.api.FirebaseNoSignedInUserException; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; @@ -63,7 +72,12 @@ /** Flutter plugin for Firebase Auth. */ public class FlutterFirebaseAuthPlugin - implements FlutterFirebasePlugin, MethodCallHandler, FlutterPlugin, ActivityAware { + implements FlutterFirebasePlugin, + MethodCallHandler, + FlutterPlugin, + ActivityAware, + GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi, + GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi { private static final String METHOD_CHANNEL_NAME = "plugins.flutter.io/firebase_auth"; @@ -98,6 +112,8 @@ private void initInstance(BinaryMessenger messenger) { registerPlugin(METHOD_CHANNEL_NAME, this); channel = new MethodChannel(messenger, METHOD_CHANNEL_NAME); channel.setMethodCallHandler(this); + GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi.setup(messenger, this); + GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi.setup(messenger, this); this.messenger = messenger; } @@ -112,6 +128,8 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { channel.setMethodCallHandler(null); channel = null; messenger = null; + GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi.setup(null, this); + GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi.setup(null, this); removeEventListeners(); } @@ -159,6 +177,11 @@ private FirebaseUser getCurrentUser(Map arguments) { return FirebaseAuth.getInstance(app).getCurrentUser(); } + private FirebaseUser getCurrentUser(String appName) { + FirebaseApp app = FirebaseApp.getInstance(appName); + return FirebaseAuth.getInstance(app).getCurrentUser(); + } + private AuthCredential getCredential(Map arguments) throws FlutterFirebaseAuthPluginException { @SuppressWarnings("unchecked") @@ -790,13 +813,79 @@ private Task> signInWithEmailAndPassword(Map taskCompletionSource.setResult(parseAuthResult(authResult)); } catch (Exception e) { - taskCompletionSource.setException(e); + if (e.getCause() instanceof FirebaseAuthMultiFactorException) { + final FirebaseAuthMultiFactorException multiFactorException = + (FirebaseAuthMultiFactorException) e.getCause(); + Map output = new HashMap<>(); + + MultiFactorResolver multiFactorResolver = multiFactorException.getResolver(); + final List hints = multiFactorResolver.getHints(); + + final MultiFactorSession session = multiFactorResolver.getSession(); + final String sessionId = UUID.randomUUID().toString(); + multiFactorSessionMap.put(sessionId, session); + + final String resolverId = UUID.randomUUID().toString(); + multiFactorResolverMap.put(resolverId, multiFactorResolver); + + final List> pigeonHints = multiFactorInfoToMap(hints); + + output.put(Constants.APP_NAME, getAuth(arguments).getApp().getName()); + + output.put(Constants.MULTI_FACTOR_HINTS, pigeonHints); + + output.put(Constants.MULTI_FACTOR_SESSION_ID, sessionId); + output.put(Constants.MULTI_FACTOR_RESOLVER_ID, resolverId); + + taskCompletionSource.setException( + new FlutterFirebaseAuthPluginException( + multiFactorException.getErrorCode(), + multiFactorException.getLocalizedMessage(), + output)); + } else { + taskCompletionSource.setException(e); + } } }); return taskCompletionSource.getTask(); } + private List multiFactorInfoToPigeon( + List hints) { + List pigeonHints = new ArrayList<>(); + for (MultiFactorInfo info : hints) { + if (info instanceof PhoneMultiFactorInfo) { + pigeonHints.add( + new GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo.Builder() + .setPhoneNumber(((PhoneMultiFactorInfo) info).getPhoneNumber()) + .setDisplayName(info.getDisplayName()) + .setEnrollmentTimestamp((double) info.getEnrollmentTimestamp()) + .setUid(info.getUid()) + .setFactorId(info.getFactorId()) + .build()); + + } else { + pigeonHints.add( + new GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo.Builder() + .setDisplayName(info.getDisplayName()) + .setEnrollmentTimestamp((double) info.getEnrollmentTimestamp()) + .setUid(info.getUid()) + .setFactorId(info.getFactorId()) + .build()); + } + } + return pigeonHints; + } + + private List> multiFactorInfoToMap(List hints) { + List> pigeonHints = new ArrayList<>(); + for (GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo info : multiFactorInfoToPigeon(hints)) { + pigeonHints.add(info.toMap()); + } + return pigeonHints; + } + private Task> signInWithEmailLink(Map arguments) { TaskCompletionSource> taskCompletionSource = new TaskCompletionSource<>(); @@ -883,10 +972,36 @@ private Task verifyPhoneNumber(Map arguments) { String eventChannelName = METHOD_CHANNEL_NAME + "/phone/" + UUID.randomUUID().toString(); EventChannel channel = new EventChannel(messenger, eventChannelName); + + final String multiFactorSessionId = + (String) arguments.get(Constants.MULTI_FACTOR_SESSION_ID); + MultiFactorSession multiFactorSession = null; + + if (multiFactorSessionId != null) { + multiFactorSession = multiFactorSessionMap.get(multiFactorSessionId); + } + + final String multiFactorInfoId = (String) arguments.get(Constants.MULTI_FACTOR_INFO); + PhoneMultiFactorInfo multiFactorInfo = null; + + if (multiFactorInfoId != null) { + for (String resolverId : multiFactorResolverMap.keySet()) { + for (MultiFactorInfo info : multiFactorResolverMap.get(resolverId).getHints()) { + if (info.getUid().equals(multiFactorInfoId) + && info instanceof PhoneMultiFactorInfo) { + multiFactorInfo = (PhoneMultiFactorInfo) info; + break; + } + } + } + } + PhoneNumberVerificationStreamHandler handler = new PhoneNumberVerificationStreamHandler( getActivity(), arguments, + multiFactorSession, + multiFactorInfo, credential -> { int hashCode = credential.hashCode(); authCredentials.put(hashCode, credential); @@ -1573,4 +1688,169 @@ private void removeEventListeners() { } streamHandlers.clear(); } + + // Map an app id to a map of user id to a MultiFactorUser object. + private final Map> multiFactorUserMap = new HashMap<>(); + + // Map an id to a MultiFactorSession object. + private final Map multiFactorSessionMap = new HashMap<>(); + + // Map an id to a MultiFactorSession object. + private final Map multiFactorResolverMap = new HashMap<>(); + + private MultiFactor getAppMultiFactor(@NonNull String appName) + throws FirebaseNoSignedInUserException { + final FirebaseUser currentUser = getCurrentUser(appName); + if (currentUser == null) { + throw new FirebaseNoSignedInUserException("No user is signed in"); + } + if (multiFactorUserMap.get(appName) == null) { + multiFactorUserMap.put(appName, new HashMap<>()); + } + + final Map appMultiFactorUser = multiFactorUserMap.get(appName); + if (appMultiFactorUser.get(currentUser.getUid()) == null) { + appMultiFactorUser.put(currentUser.getUid(), currentUser.getMultiFactor()); + } + + final MultiFactor multiFactor = appMultiFactorUser.get(currentUser.getUid()); + return multiFactor; + } + + @Override + public void enrollPhone( + @NonNull String appName, + @NonNull GeneratedAndroidFirebaseAuth.PigeonPhoneMultiFactorAssertion assertion, + @Nullable String displayName, + GeneratedAndroidFirebaseAuth.Result result) { + final MultiFactor multiFactor; + try { + multiFactor = getAppMultiFactor(appName); + } catch (FirebaseNoSignedInUserException e) { + result.error(e); + return; + } + + PhoneAuthCredential credential = + PhoneAuthProvider.getCredential( + assertion.getVerificationId(), assertion.getVerificationCode()); + + MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential); + + multiFactor + .enroll(multiFactorAssertion, displayName) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + result.success(null); + } else { + result.error(task.getException()); + } + }); + } + + @Override + public void getSession( + @NonNull String appName, + GeneratedAndroidFirebaseAuth.Result + result) { + final MultiFactor multiFactor; + try { + multiFactor = getAppMultiFactor(appName); + } catch (FirebaseNoSignedInUserException e) { + result.error(e); + return; + } + + multiFactor + .getSession() + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + final MultiFactorSession sessionResult = task.getResult(); + final String id = UUID.randomUUID().toString(); + multiFactorSessionMap.put(id, sessionResult); + result.success( + new GeneratedAndroidFirebaseAuth.PigeonMultiFactorSession.Builder() + .setId(id) + .build()); + } else { + Exception exception = task.getException(); + result.error(exception); + } + }); + } + + @Override + public void unenroll( + @NonNull String appName, + @Nullable String factorUid, + GeneratedAndroidFirebaseAuth.Result result) { + final MultiFactor multiFactor; + try { + multiFactor = getAppMultiFactor(appName); + } catch (FirebaseNoSignedInUserException e) { + result.error(e); + return; + } + + multiFactor + .unenroll(factorUid) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + result.success(null); + } else { + result.error(task.getException()); + } + }); + } + + @Override + public void getEnrolledFactors( + @NonNull String appName, + GeneratedAndroidFirebaseAuth.Result> + result) { + final MultiFactor multiFactor; + try { + multiFactor = getAppMultiFactor(appName); + } catch (FirebaseNoSignedInUserException e) { + result.error(e); + return; + } + + final List factors = multiFactor.getEnrolledFactors(); + + final List resultFactors = + multiFactorInfoToPigeon(factors); + + result.success(resultFactors); + } + + @Override + public void resolveSignIn( + @NonNull String resolverId, + @NonNull GeneratedAndroidFirebaseAuth.PigeonPhoneMultiFactorAssertion assertion, + GeneratedAndroidFirebaseAuth.Result> result) { + final MultiFactorResolver resolver = multiFactorResolverMap.get(resolverId); + + PhoneAuthCredential credential = + PhoneAuthProvider.getCredential( + assertion.getVerificationId(), assertion.getVerificationCode()); + + MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential); + + resolver + .resolveSignIn(multiFactorAssertion) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + final AuthResult authResult = task.getResult(); + result.success(parseAuthResult(authResult)); + } else { + Exception exception = task.getException(); + result.error(exception); + } + }); + } } diff --git a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPluginException.java b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPluginException.java index a495c57761cf..537a1c2e5273 100644 --- a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPluginException.java +++ b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPluginException.java @@ -24,6 +24,15 @@ public class FlutterFirebaseAuthPluginException extends Exception { this.message = message; } + FlutterFirebaseAuthPluginException( + @NonNull String code, @NonNull String message, @NonNull Map additionalData) { + super(message, null); + + this.code = code; + this.message = message; + this.additionalData = additionalData; + } + FlutterFirebaseAuthPluginException(@NonNull Exception nativeException, Throwable cause) { super(nativeException.getMessage(), cause); diff --git a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/GeneratedAndroidFirebaseAuth.java b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/GeneratedAndroidFirebaseAuth.java new file mode 100644 index 000000000000..49e7395a4ba9 --- /dev/null +++ b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/GeneratedAndroidFirebaseAuth.java @@ -0,0 +1,698 @@ +// Autogenerated from Pigeon (v3.2.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package io.flutter.plugins.firebase.auth; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +public class GeneratedAndroidFirebaseAuth { + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class PigeonMultiFactorSession { + private @NonNull String id; + + public @NonNull String getId() { + return id; + } + + public void setId(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"id\" is null."); + } + this.id = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private PigeonMultiFactorSession() {} + + public static final class Builder { + private @Nullable String id; + + public @NonNull Builder setId(@NonNull String setterArg) { + this.id = setterArg; + return this; + } + + public @NonNull PigeonMultiFactorSession build() { + PigeonMultiFactorSession pigeonReturn = new PigeonMultiFactorSession(); + pigeonReturn.setId(id); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("id", id); + return toMapResult; + } + + static @NonNull PigeonMultiFactorSession fromMap(@NonNull Map map) { + PigeonMultiFactorSession pigeonResult = new PigeonMultiFactorSession(); + Object id = map.get("id"); + pigeonResult.setId((String) id); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class PigeonPhoneMultiFactorAssertion { + private @NonNull String verificationId; + + public @NonNull String getVerificationId() { + return verificationId; + } + + public void setVerificationId(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"verificationId\" is null."); + } + this.verificationId = setterArg; + } + + private @NonNull String verificationCode; + + public @NonNull String getVerificationCode() { + return verificationCode; + } + + public void setVerificationCode(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"verificationCode\" is null."); + } + this.verificationCode = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private PigeonPhoneMultiFactorAssertion() {} + + public static final class Builder { + private @Nullable String verificationId; + + public @NonNull Builder setVerificationId(@NonNull String setterArg) { + this.verificationId = setterArg; + return this; + } + + private @Nullable String verificationCode; + + public @NonNull Builder setVerificationCode(@NonNull String setterArg) { + this.verificationCode = setterArg; + return this; + } + + public @NonNull PigeonPhoneMultiFactorAssertion build() { + PigeonPhoneMultiFactorAssertion pigeonReturn = new PigeonPhoneMultiFactorAssertion(); + pigeonReturn.setVerificationId(verificationId); + pigeonReturn.setVerificationCode(verificationCode); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("verificationId", verificationId); + toMapResult.put("verificationCode", verificationCode); + return toMapResult; + } + + static @NonNull PigeonPhoneMultiFactorAssertion fromMap(@NonNull Map map) { + PigeonPhoneMultiFactorAssertion pigeonResult = new PigeonPhoneMultiFactorAssertion(); + Object verificationId = map.get("verificationId"); + pigeonResult.setVerificationId((String) verificationId); + Object verificationCode = map.get("verificationCode"); + pigeonResult.setVerificationCode((String) verificationCode); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class PigeonMultiFactorInfo { + private @Nullable String displayName; + + public @Nullable String getDisplayName() { + return displayName; + } + + public void setDisplayName(@Nullable String setterArg) { + this.displayName = setterArg; + } + + private @NonNull Double enrollmentTimestamp; + + public @NonNull Double getEnrollmentTimestamp() { + return enrollmentTimestamp; + } + + public void setEnrollmentTimestamp(@NonNull Double setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"enrollmentTimestamp\" is null."); + } + this.enrollmentTimestamp = setterArg; + } + + private @NonNull String factorId; + + public @NonNull String getFactorId() { + return factorId; + } + + public void setFactorId(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"factorId\" is null."); + } + this.factorId = setterArg; + } + + private @NonNull String uid; + + public @NonNull String getUid() { + return uid; + } + + public void setUid(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"uid\" is null."); + } + this.uid = setterArg; + } + + private @Nullable String phoneNumber; + + public @Nullable String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(@Nullable String setterArg) { + this.phoneNumber = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private PigeonMultiFactorInfo() {} + + public static final class Builder { + private @Nullable String displayName; + + public @NonNull Builder setDisplayName(@Nullable String setterArg) { + this.displayName = setterArg; + return this; + } + + private @Nullable Double enrollmentTimestamp; + + public @NonNull Builder setEnrollmentTimestamp(@NonNull Double setterArg) { + this.enrollmentTimestamp = setterArg; + return this; + } + + private @Nullable String factorId; + + public @NonNull Builder setFactorId(@NonNull String setterArg) { + this.factorId = setterArg; + return this; + } + + private @Nullable String uid; + + public @NonNull Builder setUid(@NonNull String setterArg) { + this.uid = setterArg; + return this; + } + + private @Nullable String phoneNumber; + + public @NonNull Builder setPhoneNumber(@Nullable String setterArg) { + this.phoneNumber = setterArg; + return this; + } + + public @NonNull PigeonMultiFactorInfo build() { + PigeonMultiFactorInfo pigeonReturn = new PigeonMultiFactorInfo(); + pigeonReturn.setDisplayName(displayName); + pigeonReturn.setEnrollmentTimestamp(enrollmentTimestamp); + pigeonReturn.setFactorId(factorId); + pigeonReturn.setUid(uid); + pigeonReturn.setPhoneNumber(phoneNumber); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("displayName", displayName); + toMapResult.put("enrollmentTimestamp", enrollmentTimestamp); + toMapResult.put("factorId", factorId); + toMapResult.put("uid", uid); + toMapResult.put("phoneNumber", phoneNumber); + return toMapResult; + } + + static @NonNull PigeonMultiFactorInfo fromMap(@NonNull Map map) { + PigeonMultiFactorInfo pigeonResult = new PigeonMultiFactorInfo(); + Object displayName = map.get("displayName"); + pigeonResult.setDisplayName((String) displayName); + Object enrollmentTimestamp = map.get("enrollmentTimestamp"); + pigeonResult.setEnrollmentTimestamp((Double) enrollmentTimestamp); + Object factorId = map.get("factorId"); + pigeonResult.setFactorId((String) factorId); + Object uid = map.get("uid"); + pigeonResult.setUid((String) uid); + Object phoneNumber = map.get("phoneNumber"); + pigeonResult.setPhoneNumber((String) phoneNumber); + return pigeonResult; + } + } + + public interface Result { + void success(T result); + + void error(Throwable error); + } + + private static class MultiFactorUserHostApiCodec extends StandardMessageCodec { + public static final MultiFactorUserHostApiCodec INSTANCE = new MultiFactorUserHostApiCodec(); + + private MultiFactorUserHostApiCodec() {} + + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return PigeonMultiFactorInfo.fromMap((Map) readValue(buffer)); + + case (byte) 129: + return PigeonMultiFactorSession.fromMap((Map) readValue(buffer)); + + case (byte) 130: + return PigeonPhoneMultiFactorAssertion.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof PigeonMultiFactorInfo) { + stream.write(128); + writeValue(stream, ((PigeonMultiFactorInfo) value).toMap()); + } else if (value instanceof PigeonMultiFactorSession) { + stream.write(129); + writeValue(stream, ((PigeonMultiFactorSession) value).toMap()); + } else if (value instanceof PigeonPhoneMultiFactorAssertion) { + stream.write(130); + writeValue(stream, ((PigeonPhoneMultiFactorAssertion) value).toMap()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface MultiFactorUserHostApi { + void enrollPhone( + @NonNull String appName, + @NonNull PigeonPhoneMultiFactorAssertion assertion, + @Nullable String displayName, + Result result); + + void getSession(@NonNull String appName, Result result); + + void unenroll(@NonNull String appName, @Nullable String factorUid, Result result); + + void getEnrolledFactors(@NonNull String appName, Result> result); + + /** The codec used by MultiFactorUserHostApi. */ + static MessageCodec getCodec() { + return MultiFactorUserHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `MultiFactorUserHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, MultiFactorUserHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.MultiFactorUserHostApi.enrollPhone", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String appNameArg = (String) args.get(0); + if (appNameArg == null) { + throw new NullPointerException("appNameArg unexpectedly null."); + } + PigeonPhoneMultiFactorAssertion assertionArg = + (PigeonPhoneMultiFactorAssertion) args.get(1); + if (assertionArg == null) { + throw new NullPointerException("assertionArg unexpectedly null."); + } + String displayNameArg = (String) args.get(2); + Result resultCallback = + new Result() { + public void success(Void result) { + wrapped.put("result", null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.enrollPhone(appNameArg, assertionArg, displayNameArg, resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.MultiFactorUserHostApi.getSession", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String appNameArg = (String) args.get(0); + if (appNameArg == null) { + throw new NullPointerException("appNameArg unexpectedly null."); + } + Result resultCallback = + new Result() { + public void success(PigeonMultiFactorSession result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.getSession(appNameArg, resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.MultiFactorUserHostApi.unenroll", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String appNameArg = (String) args.get(0); + if (appNameArg == null) { + throw new NullPointerException("appNameArg unexpectedly null."); + } + String factorUidArg = (String) args.get(1); + Result resultCallback = + new Result() { + public void success(Void result) { + wrapped.put("result", null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.unenroll(appNameArg, factorUidArg, resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.MultiFactorUserHostApi.getEnrolledFactors", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String appNameArg = (String) args.get(0); + if (appNameArg == null) { + throw new NullPointerException("appNameArg unexpectedly null."); + } + Result> resultCallback = + new Result>() { + public void success(List result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.getEnrolledFactors(appNameArg, resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class MultiFactoResolverHostApiCodec extends StandardMessageCodec { + public static final MultiFactoResolverHostApiCodec INSTANCE = + new MultiFactoResolverHostApiCodec(); + + private MultiFactoResolverHostApiCodec() {} + + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return PigeonMultiFactorInfo.fromMap((Map) readValue(buffer)); + + case (byte) 129: + return PigeonMultiFactorSession.fromMap((Map) readValue(buffer)); + + case (byte) 130: + return PigeonPhoneMultiFactorAssertion.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof PigeonMultiFactorInfo) { + stream.write(128); + writeValue(stream, ((PigeonMultiFactorInfo) value).toMap()); + } else if (value instanceof PigeonMultiFactorSession) { + stream.write(129); + writeValue(stream, ((PigeonMultiFactorSession) value).toMap()); + } else if (value instanceof PigeonPhoneMultiFactorAssertion) { + stream.write(130); + writeValue(stream, ((PigeonPhoneMultiFactorAssertion) value).toMap()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface MultiFactoResolverHostApi { + void resolveSignIn( + @NonNull String resolverId, + @NonNull PigeonPhoneMultiFactorAssertion assertion, + Result> result); + + /** The codec used by MultiFactoResolverHostApi. */ + static MessageCodec getCodec() { + return MultiFactoResolverHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `MultiFactoResolverHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, MultiFactoResolverHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.MultiFactoResolverHostApi.resolveSignIn", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String resolverIdArg = (String) args.get(0); + if (resolverIdArg == null) { + throw new NullPointerException("resolverIdArg unexpectedly null."); + } + PigeonPhoneMultiFactorAssertion assertionArg = + (PigeonPhoneMultiFactorAssertion) args.get(1); + if (assertionArg == null) { + throw new NullPointerException("assertionArg unexpectedly null."); + } + Result> resultCallback = + new Result>() { + public void success(Map result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.resolveSignIn(resolverIdArg, assertionArg, resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class GenerateInterfacesCodec extends StandardMessageCodec { + public static final GenerateInterfacesCodec INSTANCE = new GenerateInterfacesCodec(); + + private GenerateInterfacesCodec() {} + + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return PigeonMultiFactorInfo.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof PigeonMultiFactorInfo) { + stream.write(128); + writeValue(stream, ((PigeonMultiFactorInfo) value).toMap()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface GenerateInterfaces { + void generateInterfaces(@NonNull PigeonMultiFactorInfo info); + + /** The codec used by GenerateInterfaces. */ + static MessageCodec getCodec() { + return GenerateInterfacesCodec.INSTANCE; + } + + /** + * Sets up an instance of `GenerateInterfaces` to handle messages through the `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, GenerateInterfaces api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.GenerateInterfaces.generateInterfaces", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + PigeonMultiFactorInfo infoArg = (PigeonMultiFactorInfo) args.get(0); + if (infoArg == null) { + throw new NullPointerException("infoArg unexpectedly null."); + } + api.generateInterfaces(infoArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static Map wrapError(Throwable exception) { + Map errorMap = new HashMap<>(); + errorMap.put("message", exception.toString()); + errorMap.put("code", exception.getClass().getSimpleName()); + errorMap.put( + "details", + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + return errorMap; + } +} diff --git a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/PhoneNumberVerificationStreamHandler.java b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/PhoneNumberVerificationStreamHandler.java index d2ff5e699f9e..4e2be1cab103 100644 --- a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/PhoneNumberVerificationStreamHandler.java +++ b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/PhoneNumberVerificationStreamHandler.java @@ -5,10 +5,12 @@ import androidx.annotation.Nullable; import com.google.firebase.FirebaseException; import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.MultiFactorSession; import com.google.firebase.auth.PhoneAuthCredential; import com.google.firebase.auth.PhoneAuthOptions; import com.google.firebase.auth.PhoneAuthProvider; import com.google.firebase.auth.PhoneAuthProvider.ForceResendingToken; +import com.google.firebase.auth.PhoneMultiFactorInfo; import io.flutter.plugin.common.EventChannel.EventSink; import io.flutter.plugin.common.EventChannel.StreamHandler; import java.util.HashMap; @@ -26,9 +28,12 @@ interface OnCredentialsListener { final AtomicReference activityRef = new AtomicReference<>(null); final FirebaseAuth firebaseAuth; final String phoneNumber; + final PhoneMultiFactorInfo multiFactorInfo; final int timeout; final OnCredentialsListener onCredentialsListener; + final MultiFactorSession multiFactorSession; + String autoRetrievedSmsCodeForTesting; Integer forceResendingToken; @@ -39,11 +44,15 @@ interface OnCredentialsListener { public PhoneNumberVerificationStreamHandler( Activity activity, Map arguments, + @Nullable MultiFactorSession multiFactorSession, + @Nullable PhoneMultiFactorInfo multiFactorInfo, OnCredentialsListener onCredentialsListener) { this.activityRef.set(activity); + this.multiFactorSession = multiFactorSession; + this.multiFactorInfo = multiFactorInfo; firebaseAuth = FlutterFirebaseAuthPlugin.getAuth(arguments); - phoneNumber = (String) Objects.requireNonNull(arguments.get(Constants.PHONE_NUMBER)); + phoneNumber = (String) arguments.get(Constants.PHONE_NUMBER); timeout = (int) Objects.requireNonNull(arguments.get(Constants.TIMEOUT)); if (arguments.containsKey(Constants.AUTO_RETRIEVED_SMS_CODE_FOR_TESTING)) { @@ -140,8 +149,17 @@ public void onCodeAutoRetrievalTimeOut(@NonNull String verificationId) { PhoneAuthOptions.Builder phoneAuthOptionsBuilder = new PhoneAuthOptions.Builder(firebaseAuth); phoneAuthOptionsBuilder.setActivity(activityRef.get()); - phoneAuthOptionsBuilder.setPhoneNumber(phoneNumber); phoneAuthOptionsBuilder.setCallbacks(callbacks); + + if (phoneNumber != null) { + phoneAuthOptionsBuilder.setPhoneNumber(phoneNumber); + } + if (multiFactorSession != null) { + phoneAuthOptionsBuilder.setMultiFactorSession(multiFactorSession); + } + if (multiFactorInfo != null) { + phoneAuthOptionsBuilder.setMultiFactorHint(multiFactorInfo); + } phoneAuthOptionsBuilder.setTimeout((long) timeout, TimeUnit.MILLISECONDS); if (forceResendingToken != null) { diff --git a/packages/firebase_auth/firebase_auth/example/lib/auth.dart b/packages/firebase_auth/firebase_auth/example/lib/auth.dart index a718cffa3af9..1574203c2f52 100644 --- a/packages/firebase_auth/firebase_auth/example/lib/auth.dart +++ b/packages/firebase_auth/firebase_auth/example/lib/auth.dart @@ -382,6 +382,43 @@ class _AuthGateState extends State { } else { await _phoneAuth(); } + } on FirebaseAuthMultiFactorException catch (e) { + setState(() { + error = '${e.message}'; + }); + final firstHint = e.resolver.hints.first; + if (firstHint is! PhoneMultiFactorInfo) { + return; + } + final auth = FirebaseAuth.instance; + await auth.verifyPhoneNumber( + multiFactorSession: e.resolver.session, + multiFactorInfo: firstHint, + verificationCompleted: (_) {}, + verificationFailed: print, + codeSent: (String verificationId, int? resendToken) async { + final smsCode = await getSmsCodeFromUser(context); + + if (smsCode != null) { + // Create a PhoneAuthCredential with the code + final credential = PhoneAuthProvider.credential( + verificationId: verificationId, + smsCode: smsCode, + ); + + try { + await e.resolver.resolveSignIn( + PhoneMultiFactorGenerator.getAssertion( + credential, + ), + ); + } on FirebaseAuthException catch (e) { + print(e.message); + } + } + }, + codeAutoRetrievalTimeout: print, + ); } on FirebaseAuthException catch (e) { setState(() { error = '${e.message}'; @@ -396,48 +433,6 @@ class _AuthGateState extends State { } } - Future getSmsCodeFromUser() async { - String? smsCode; - - // Update the UI - wait for the user to enter the SMS code - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) { - return AlertDialog( - title: const Text('SMS code:'), - actions: [ - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Sign in'), - ), - OutlinedButton( - onPressed: () { - smsCode = null; - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - ], - content: Container( - padding: const EdgeInsets.all(20), - child: TextField( - onChanged: (value) { - smsCode = value; - }, - textAlign: TextAlign.center, - autofocus: true, - ), - ), - ); - }, - ); - - return smsCode; - } - Future _phoneAuth() async { if (mode != AuthMode.phone) { setState(() { @@ -448,7 +443,7 @@ class _AuthGateState extends State { if (kIsWeb) { final confirmationResult = await _auth.signInWithPhoneNumber(phoneController.text); - final smsCode = await getSmsCodeFromUser(); + final smsCode = await getSmsCodeFromUser(context); if (smsCode != null) { await confirmationResult.confirm(smsCode); @@ -463,7 +458,7 @@ class _AuthGateState extends State { }); }, codeSent: (String verificationId, int? resendToken) async { - final smsCode = await getSmsCodeFromUser(); + final smsCode = await getSmsCodeFromUser(context); if (smsCode != null) { // Create a PhoneAuthCredential with the code @@ -592,3 +587,45 @@ class _AuthGateState extends State { } } } + +Future getSmsCodeFromUser(BuildContext context) async { + String? smsCode; + + // Update the UI - wait for the user to enter the SMS code + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + title: const Text('SMS code:'), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Sign in'), + ), + OutlinedButton( + onPressed: () { + smsCode = null; + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + ], + content: Container( + padding: const EdgeInsets.all(20), + child: TextField( + onChanged: (value) { + smsCode = value; + }, + textAlign: TextAlign.center, + autofocus: true, + ), + ), + ); + }, + ); + + return smsCode; +} diff --git a/packages/firebase_auth/firebase_auth/example/lib/profile.dart b/packages/firebase_auth/firebase_auth/example/lib/profile.dart index 66a0bcff0c2d..7c9d430e0459 100644 --- a/packages/firebase_auth/firebase_auth/example/lib/profile.dart +++ b/packages/firebase_auth/firebase_auth/example/lib/profile.dart @@ -23,6 +23,7 @@ class ProfilePage extends StatefulWidget { class _ProfilePageState extends State { late User user; late TextEditingController controller; + final phoneController = TextEditingController(); String? photoURL; @@ -167,7 +168,60 @@ class _ProfilePageState extends State { ), ], ), - const SizedBox(height: 40), + const SizedBox(height: 20), + TextButton( + onPressed: () { + user.sendEmailVerification(); + }, + child: const Text('Verify Email'), + ), + const SizedBox(height: 20), + TextFormField( + controller: phoneController, + decoration: const InputDecoration( + icon: Icon(Icons.phone), + hintText: '+33612345678', + labelText: 'Phone number', + ), + ), + const SizedBox(height: 20), + TextButton( + onPressed: () async { + final session = await user.multiFactor.getSession(); + final auth = FirebaseAuth.instance; + await auth.verifyPhoneNumber( + multiFactorSession: session, + phoneNumber: phoneController.text, + verificationCompleted: (_) {}, + verificationFailed: print, + codeSent: + (String verificationId, int? resendToken) async { + final smsCode = await getSmsCodeFromUser(context); + + if (smsCode != null) { + // Create a PhoneAuthCredential with the code + final credential = PhoneAuthProvider.credential( + verificationId: verificationId, + smsCode: smsCode, + ); + + try { + await user.multiFactor.enroll( + PhoneMultiFactorGenerator.getAssertion( + credential, + ), + ); + } on FirebaseAuthException catch (e) { + print(e.message); + } + } + }, + codeAutoRetrievalTimeout: print, + ); + }, + child: const Text('Verify Number For MFA'), + ), + const SizedBox(height: 20), TextButton( onPressed: _signOut, child: const Text('Sign out'), diff --git a/packages/firebase_auth/firebase_auth/example/macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/firebase_auth/firebase_auth/example/macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/firebase_auth/firebase_auth/example/macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/firebase_auth/firebase_auth/ios/Classes/FLTFirebaseAuthPlugin.m b/packages/firebase_auth/firebase_auth/ios/Classes/FLTFirebaseAuthPlugin.m index 378f3fa80082..009a0a660b66 100644 --- a/packages/firebase_auth/firebase_auth/ios/Classes/FLTFirebaseAuthPlugin.m +++ b/packages/firebase_auth/firebase_auth/ios/Classes/FLTFirebaseAuthPlugin.m @@ -10,10 +10,14 @@ #import "Private/FLTIdTokenChannelStreamHandler.h" #import "Private/FLTPhoneNumberVerificationStreamHandler.h" +#import "Private/CustomPigeonHeader.h" #import "Public/FLTFirebaseAuthPlugin.h" NSString *const kFLTFirebaseAuthChannelName = @"plugins.flutter.io/firebase_auth"; +// Argument Keys +NSString *const kAppName = @"appName"; + // Provider type keys. NSString *const kSignInMethodPassword = @"password"; NSString *const kSignInMethodEmailLink = @"emailLink"; @@ -41,6 +45,12 @@ NSString *const kArgumentSmsCode = @"smsCode"; NSString *const kArgumentActionCodeSettings = @"actionCodeSettings"; +// MultiFactor +NSString *const kArgumentMultiFactorHints = @"multiFactorHints"; +NSString *const kArgumentMultiFactorSessionId = @"multiFactorSessionId"; +NSString *const kArgumentMultiFactorResolverId = @"multiFactorResolverId"; +NSString *const kArgumentMultiFactorInfo = @"multiFactorInfo"; + // Manual error codes & messages. NSString *const kErrCodeNoCurrentUser = @"no-current-user"; NSString *const kErrMsgNoCurrentUser = @"No user currently signed in."; @@ -58,6 +68,14 @@ @implementation FLTFirebaseAuthPlugin { // Used for caching credentials between Method Channel method calls. NSMutableDictionary *_credentials; +#if TARGET_OS_IPHONE + // Map an id to a MultiFactorSession object. + NSMutableDictionary *_multiFactorSessionMap; + + // Map an id to a MultiFactorResolver object. + NSMutableDictionary *_multiFactorResolverMap; +#endif + NSObject *_binaryMessenger; NSMutableDictionary *_eventChannels; NSMutableDictionary *> *_streamHandlers; @@ -74,6 +92,11 @@ - (instancetype)init:(NSObject *)messenger { _binaryMessenger = messenger; _eventChannels = [NSMutableDictionary dictionary]; _streamHandlers = [NSMutableDictionary dictionary]; + +#if TARGET_OS_IPHONE + _multiFactorSessionMap = [NSMutableDictionary dictionary]; + _multiFactorResolverMap = [NSMutableDictionary dictionary]; +#endif } return self; } @@ -94,6 +117,8 @@ + (void)registerWithRegistrar:(NSObject *)registrar { #else [registrar publish:instance]; [registrar addApplicationDelegate:instance]; + MultiFactorUserHostApiSetup(registrar.messenger, instance); + MultiFactoResolverHostApiSetup(registrar.messenger, instance); #endif } @@ -121,18 +146,23 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)flutter FLTFirebaseMethodCallErrorBlock errorBlock = ^(NSString *_Nullable code, NSString *_Nullable message, NSDictionary *_Nullable details, NSError *_Nullable error) { + NSMutableDictionary *generatedDetails = [NSMutableDictionary new]; if (code == nil) { NSDictionary *errorDetails = [FLTFirebaseAuthPlugin getNSDictionaryFromNSError:error]; [self storeAuthCredentialIfPresent:error]; code = errorDetails[kArgumentCode]; message = errorDetails[@"message"]; - details = errorDetails; + generatedDetails = [NSMutableDictionary dictionaryWithDictionary:errorDetails]; } else { - details = @{ + generatedDetails = [NSMutableDictionary dictionaryWithDictionary:@{ kArgumentCode : code, @"message" : message, @"additionalData" : @{}, - }; + }]; + } + + if (details != nil) { + generatedDetails[@"additionalData"] = details; } if ([@"unknown" isEqualToString:code]) { @@ -143,7 +173,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)flutter flutterResult([FLTFirebasePlugin createFlutterErrorFromCode:code message:message - optionalDetails:details + optionalDetails:generatedDetails andOptionalNSError:error]); }; @@ -566,7 +596,56 @@ - (void)signInWithEmailAndPassword:(id)arguments password:arguments[@"password"] completion:^(FIRAuthDataResult *_Nullable authResult, NSError *_Nullable error) { if (error != nil) { - result.error(nil, nil, nil, error); + if (error.code == FIRAuthErrorCodeSecondFactorRequired) { +#if TARGET_OS_OSX + result.error(nil, nil, nil, error); +#else + FIRMultiFactorResolver *resolver = + (FIRMultiFactorResolver *) + error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey]; + + NSArray *hints = resolver.hints; + FIRMultiFactorSession *session = resolver.session; + + NSString *sessionId = [[NSUUID UUID] UUIDString]; + self->_multiFactorSessionMap[sessionId] = session; + + NSString *resolverId = [[NSUUID UUID] UUIDString]; + self->_multiFactorResolverMap[resolverId] = resolver; + + NSMutableArray *pigeonHints = [NSMutableArray array]; + + for (FIRMultiFactorInfo *multiFactorInfo in hints) { + NSString *phoneNumber; + if ([multiFactorInfo class] == [FIRPhoneMultiFactorInfo class]) { + FIRPhoneMultiFactorInfo *phoneFactorInfo = + (FIRPhoneMultiFactorInfo *)multiFactorInfo; + phoneNumber = phoneFactorInfo.phoneNumber; + } + + PigeonMultiFactorInfo *object = [PigeonMultiFactorInfo + makeWithDisplayName:multiFactorInfo.displayName + enrollmentTimestamp:[NSNumber numberWithDouble:multiFactorInfo.enrollmentDate + .timeIntervalSince1970] + factorId:multiFactorInfo.factorID + uid:multiFactorInfo.UID + phoneNumber:phoneNumber]; + + [pigeonHints addObject:object.toMap]; + } + + NSDictionary *output = @{ + kAppName : arguments[kAppName], + kArgumentMultiFactorHints : pigeonHints, + kArgumentMultiFactorSessionId : sessionId, + kArgumentMultiFactorResolverId : resolverId, + }; + result.error(nil, nil, output, error); +#endif + + } else { + result.error(nil, nil, nil, error); + } } else { result.success(authResult); } @@ -1036,8 +1115,33 @@ - (void)verifyPhoneNumber:(id)arguments withMethodCallResult:(FLTFirebaseMethodC FlutterEventChannel *channel = [FlutterEventChannel eventChannelWithName:name binaryMessenger:_binaryMessenger]; + NSString *multiFactorSessionId = arguments[kArgumentMultiFactorSessionId]; + FIRMultiFactorSession *multiFactorSession = nil; + + if (multiFactorSessionId != nil) { + multiFactorSession = _multiFactorSessionMap[multiFactorSessionId]; + } + + NSString *multiFactorInfoId = arguments[kArgumentMultiFactorInfo]; + + FIRPhoneMultiFactorInfo *multiFactorInfo = nil; + if (multiFactorInfoId != nil) { + for (NSString *resolverId in _multiFactorResolverMap) { + for (FIRMultiFactorInfo *info in _multiFactorResolverMap[resolverId].hints) { + if ([info.UID isEqualToString:multiFactorInfoId] && + [info class] == [FIRPhoneMultiFactorInfo class]) { + multiFactorInfo = (FIRPhoneMultiFactorInfo *)info; + break; + } + } + } + } + FLTPhoneNumberVerificationStreamHandler *handler = - [[FLTPhoneNumberVerificationStreamHandler alloc] initWithAuth:auth arguments:arguments]; + [[FLTPhoneNumberVerificationStreamHandler alloc] initWithAuth:auth + arguments:arguments + session:multiFactorSession + factorInfo:multiFactorInfo]; [channel setStreamHandler:handler]; [_eventChannels setObject:channel forKey:name]; @@ -1114,7 +1218,7 @@ + (NSDictionary *)getNSDictionaryFromNSError:(NSError *)error { } - (FIRAuth *_Nullable)getFIRAuthFromArguments:(NSDictionary *)arguments { - NSString *appNameDart = arguments[@"appName"]; + NSString *appNameDart = arguments[kAppName]; NSString *tenantId = arguments[@"tenantId"]; FIRApp *app = [FLTFirebasePlugin firebaseAppNamed:appNameDart]; FIRAuth *auth = [FIRAuth authWithApp:app]; @@ -1126,6 +1230,13 @@ - (FIRAuth *_Nullable)getFIRAuthFromArguments:(NSDictionary *)arguments { return auth; } +- (FIRAuth *_Nullable)getFIRAuthFromAppName:(NSString *)appNameDart { + FIRApp *app = [FLTFirebasePlugin firebaseAppNamed:appNameDart]; + FIRAuth *auth = [FIRAuth authWithApp:app]; + + return auth; +} + - (FIRActionCodeSettings *_Nullable)getFIRActionCodeSettingsFromArguments: (NSDictionary *)arguments { NSDictionary *actionCodeSettingsDictionary = arguments[kArgumentActionCodeSettings]; @@ -1365,4 +1476,126 @@ - (void)ensureAPNSTokenSetting { #endif } +#if TARGET_OS_IPHONE +- (FIRMultiFactor *)getAppMultiFactor:(nonnull NSString *)appName { + FIRAuth *auth = [self getFIRAuthFromAppName:appName]; + FIRUser *currentUser = auth.currentUser; + return currentUser.multiFactor; +} + +- (void)enrollPhoneAppName:(nonnull NSString *)appName + assertion:(nonnull PigeonPhoneMultiFactorAssertion *)assertion + displayName:(nullable NSString *)displayName + completion:(nonnull void (^)(FlutterError *_Nullable))completion { + FIRMultiFactor *multiFactor = [self getAppMultiFactor:appName]; + + FIRPhoneAuthCredential *credential = + [[FIRPhoneAuthProvider providerWithAuth:[self getFIRAuthFromAppName:appName]] + credentialWithVerificationID:[assertion verificationId] + verificationCode:[assertion verificationCode]]; + + FIRMultiFactorAssertion *multiFactorAssertion = + [FIRPhoneMultiFactorGenerator assertionWithCredential:credential]; + + [multiFactor enrollWithAssertion:multiFactorAssertion + displayName:displayName + completion:^(NSError *_Nullable error) { + if (error == nil) { + completion(nil); + } else { + completion([FlutterError errorWithCode:@"enroll-failed" + message:error.localizedDescription + details:nil]); + } + }]; +} + +- (void)getEnrolledFactorsAppName:(nonnull NSString *)appName + completion:(nonnull void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion { + FIRMultiFactor *multiFactor = [self getAppMultiFactor:appName]; + + NSArray *enrolledFactors = [multiFactor enrolledFactors]; + + NSMutableArray *results = [NSMutableArray array]; + + for (FIRMultiFactorInfo *multiFactorInfo in enrolledFactors) { + NSString *phoneNumber; + if ([multiFactorInfo class] == [FIRPhoneMultiFactorInfo class]) { + FIRPhoneMultiFactorInfo *phoneFactorInfo = (FIRPhoneMultiFactorInfo *)multiFactorInfo; + phoneNumber = phoneFactorInfo.phoneNumber; + } + + [results + addObject:[PigeonMultiFactorInfo + makeWithDisplayName:multiFactorInfo.displayName + enrollmentTimestamp:[NSNumber numberWithDouble:multiFactorInfo.enrollmentDate + .timeIntervalSince1970] + factorId:multiFactorInfo.factorID + uid:multiFactorInfo.UID + phoneNumber:phoneNumber]]; + } + + completion(results, nil); +} + +- (void)getSessionAppName:(nonnull NSString *)appName + completion:(nonnull void (^)(PigeonMultiFactorSession *_Nullable, + FlutterError *_Nullable))completion { + FIRMultiFactor *multiFactor = [self getAppMultiFactor:appName]; + [multiFactor getSessionWithCompletion:^(FIRMultiFactorSession *_Nullable session, + NSError *_Nullable error) { + NSString *UUID = [[NSUUID UUID] UUIDString]; + self->_multiFactorSessionMap[UUID] = session; + + PigeonMultiFactorSession *pigeonSession = [PigeonMultiFactorSession makeWithId:UUID]; + completion(pigeonSession, nil); + }]; +} + +- (void)unenrollAppName:(nonnull NSString *)appName + factorUid:(nullable NSString *)factorUid + completion:(nonnull void (^)(FlutterError *_Nullable))completion { + FIRMultiFactor *multiFactor = [self getAppMultiFactor:appName]; + [multiFactor unenrollWithFactorUID:factorUid + completion:^(NSError *_Nullable error) { + if (error == nil) { + completion(nil); + } else { + completion([FlutterError errorWithCode:@"unenroll-failed" + message:error.localizedDescription + details:nil]); + } + }]; +} + +- (void)resolveSignInResolverId:(nonnull NSString *)resolverId + assertion:(nonnull PigeonPhoneMultiFactorAssertion *)assertion + completion:(nonnull void (^)(NSDictionary *_Nullable, + FlutterError *_Nullable))completion { + FIRMultiFactorResolver *resolver = _multiFactorResolverMap[resolverId]; + + FIRPhoneAuthCredential *credential = + [[FIRPhoneAuthProvider provider] credentialWithVerificationID:[assertion verificationId] + verificationCode:[assertion verificationCode]]; + + FIRMultiFactorAssertion *multiFactorAssertion = + [FIRPhoneMultiFactorGenerator assertionWithCredential:credential]; + + [resolver + resolveSignInWithAssertion:multiFactorAssertion + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + if (error == nil) { + completion([self getNSDictionaryFromAuthResult:authResult], nil); + } else { + completion(nil, [FlutterError errorWithCode:@"resolve-signin-failed" + message:error.localizedDescription + details:nil]); + } + }]; +} + +#endif + @end diff --git a/packages/firebase_auth/firebase_auth/ios/Classes/FLTPhoneNumberVerificationStreamHandler.m b/packages/firebase_auth/firebase_auth/ios/Classes/FLTPhoneNumberVerificationStreamHandler.m index 624e01c392e5..68ee7c404d7d 100644 --- a/packages/firebase_auth/firebase_auth/ios/Classes/FLTPhoneNumberVerificationStreamHandler.m +++ b/packages/firebase_auth/firebase_auth/ios/Classes/FLTPhoneNumberVerificationStreamHandler.m @@ -8,8 +8,14 @@ @implementation FLTPhoneNumberVerificationStreamHandler { FIRAuth *_auth; NSString *_phoneNumber; +#if TARGET_OS_OSX +#else + FIRMultiFactorSession *_session; + FIRPhoneMultiFactorInfo *_factorInfo; +#endif } +#if TARGET_OS_OSX - (instancetype)initWithAuth:(id)auth arguments:(NSDictionary *)arguments { self = [super init]; if (self) { @@ -19,6 +25,22 @@ - (instancetype)initWithAuth:(id)auth arguments:(NSDictionary *)arguments { return self; } +#else +- (instancetype)initWithAuth:(id)auth + arguments:(NSDictionary *)arguments + session:(FIRMultiFactorSession *)session + factorInfo:(FIRPhoneMultiFactorInfo *)factorInfo { + self = [super init]; + if (self) { + _auth = auth; + _phoneNumber = arguments[@"phoneNumber"]; + _session = session; + _factorInfo = factorInfo; + } + return self; +} +#endif + - (FlutterError *)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)events { #if TARGET_OS_IPHONE id completer = ^(NSString *verificationID, NSError *error) { @@ -41,9 +63,19 @@ - (FlutterError *)onListenWithArguments:(id)arguments eventSink:(FlutterEventSin // Try catch to capture 'missing URL scheme' error. @try { - [[FIRPhoneAuthProvider providerWithAuth:_auth] verifyPhoneNumber:_phoneNumber - UIDelegate:nil - completion:completer]; + if (_factorInfo != nil) { + [[FIRPhoneAuthProvider providerWithAuth:_auth] + verifyPhoneNumberWithMultiFactorInfo:_factorInfo + UIDelegate:nil + multiFactorSession:_session + completion:completer]; + + } else { + [[FIRPhoneAuthProvider providerWithAuth:_auth] verifyPhoneNumber:_phoneNumber + UIDelegate:nil + multiFactorSession:_session + completion:completer]; + } } @catch (NSException *exception) { events(@{ @"name" : @"Auth#phoneVerificationFailed", diff --git a/packages/firebase_auth/firebase_auth/ios/Classes/Private/CustomPigeonHeader.h b/packages/firebase_auth/firebase_auth/ios/Classes/Private/CustomPigeonHeader.h new file mode 100644 index 000000000000..798ecfbffbed --- /dev/null +++ b/packages/firebase_auth/firebase_auth/ios/Classes/Private/CustomPigeonHeader.h @@ -0,0 +1,8 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#import "messages.g.h" + +@interface PigeonMultiFactorInfo (Map) +- (NSDictionary *)toMap; +@end diff --git a/packages/firebase_auth/firebase_auth/ios/Classes/Private/FLTPhoneNumberVerificationStreamHandler.h b/packages/firebase_auth/firebase_auth/ios/Classes/Private/FLTPhoneNumberVerificationStreamHandler.h index 939c0f92c911..9b61df9b46ea 100644 --- a/packages/firebase_auth/firebase_auth/ios/Classes/Private/FLTPhoneNumberVerificationStreamHandler.h +++ b/packages/firebase_auth/firebase_auth/ios/Classes/Private/FLTPhoneNumberVerificationStreamHandler.h @@ -18,7 +18,14 @@ NS_ASSUME_NONNULL_BEGIN @interface FLTPhoneNumberVerificationStreamHandler : NSObject +#if TARGET_OS_OSX - (instancetype)initWithAuth:(FIRAuth *)auth arguments:(NSDictionary *)arguments; +#else +- (instancetype)initWithAuth:(FIRAuth *)auth + arguments:(NSDictionary *)arguments + session:(FIRMultiFactorSession *)session + factorInfo:(FIRPhoneMultiFactorInfo *)factorInfo; +#endif @end diff --git a/packages/firebase_auth/firebase_auth/ios/Classes/Public/FLTFirebaseAuthPlugin.h b/packages/firebase_auth/firebase_auth/ios/Classes/Public/FLTFirebaseAuthPlugin.h index b57e0d7af846..35ca5756ad5c 100644 --- a/packages/firebase_auth/firebase_auth/ios/Classes/Public/FLTFirebaseAuthPlugin.h +++ b/packages/firebase_auth/firebase_auth/ios/Classes/Public/FLTFirebaseAuthPlugin.h @@ -13,8 +13,10 @@ #import #import +#import "messages.g.h" -@interface FLTFirebaseAuthPlugin : FLTFirebasePlugin +@interface FLTFirebaseAuthPlugin + : FLTFirebasePlugin + (id)getNSDictionaryFromAuthCredential:(FIRAuthCredential *)authCredential; + (NSDictionary *)getNSDictionaryFromUserInfo:(id)userInfo; diff --git a/packages/firebase_auth/firebase_auth/ios/Classes/messages.g.h b/packages/firebase_auth/firebase_auth/ios/Classes/messages.g.h new file mode 100644 index 000000000000..dc865a5d022c --- /dev/null +++ b/packages/firebase_auth/firebase_auth/ios/Classes/messages.g.h @@ -0,0 +1,92 @@ +// Autogenerated from Pigeon (v3.2.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +@class PigeonMultiFactorSession; +@class PigeonPhoneMultiFactorAssertion; +@class PigeonMultiFactorInfo; + +@interface PigeonMultiFactorSession : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithId:(NSString *)id; +@property(nonatomic, copy) NSString *id; +@end + +@interface PigeonPhoneMultiFactorAssertion : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithVerificationId:(NSString *)verificationId + verificationCode:(NSString *)verificationCode; +@property(nonatomic, copy) NSString *verificationId; +@property(nonatomic, copy) NSString *verificationCode; +@end + +@interface PigeonMultiFactorInfo : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithDisplayName:(nullable NSString *)displayName + enrollmentTimestamp:(NSNumber *)enrollmentTimestamp + factorId:(NSString *)factorId + uid:(NSString *)uid + phoneNumber:(nullable NSString *)phoneNumber; +@property(nonatomic, copy, nullable) NSString *displayName; +@property(nonatomic, strong) NSNumber *enrollmentTimestamp; +@property(nonatomic, copy) NSString *factorId; +@property(nonatomic, copy) NSString *uid; +@property(nonatomic, copy, nullable) NSString *phoneNumber; +@end + +/// The codec used by MultiFactorUserHostApi. +NSObject *MultiFactorUserHostApiGetCodec(void); + +@protocol MultiFactorUserHostApi +- (void)enrollPhoneAppName:(NSString *)appName + assertion:(PigeonPhoneMultiFactorAssertion *)assertion + displayName:(nullable NSString *)displayName + completion:(void (^)(FlutterError *_Nullable))completion; +- (void)getSessionAppName:(NSString *)appName + completion:(void (^)(PigeonMultiFactorSession *_Nullable, + FlutterError *_Nullable))completion; +- (void)unenrollAppName:(NSString *)appName + factorUid:(nullable NSString *)factorUid + completion:(void (^)(FlutterError *_Nullable))completion; +- (void)getEnrolledFactorsAppName:(NSString *)appName + completion:(void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion; +@end + +extern void MultiFactorUserHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by MultiFactoResolverHostApi. +NSObject *MultiFactoResolverHostApiGetCodec(void); + +@protocol MultiFactoResolverHostApi +- (void)resolveSignInResolverId:(NSString *)resolverId + assertion:(PigeonPhoneMultiFactorAssertion *)assertion + completion:(void (^)(NSDictionary *_Nullable, + FlutterError *_Nullable))completion; +@end + +extern void MultiFactoResolverHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by GenerateInterfaces. +NSObject *GenerateInterfacesGetCodec(void); + +@protocol GenerateInterfaces +- (void)generateInterfacesInfo:(PigeonMultiFactorInfo *)info + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void GenerateInterfacesSetup(id binaryMessenger, + NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/firebase_auth/firebase_auth/ios/Classes/messages.g.m b/packages/firebase_auth/firebase_auth/ios/Classes/messages.g.m new file mode 100644 index 000000000000..4abca35bf24a --- /dev/null +++ b/packages/firebase_auth/firebase_auth/ios/Classes/messages.g.m @@ -0,0 +1,464 @@ +// Autogenerated from Pigeon (v3.2.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "messages.g.h" +#if TARGET_OS_OSX +#import +#else +#import +#endif + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(id result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code" : (error.code ?: [NSNull null]), + @"message" : (error.message ?: [NSNull null]), + @"details" : (error.details ?: [NSNull null]), + }; + } + return @{ + @"result" : (result ?: [NSNull null]), + @"error" : errorDict, + }; +} +static id GetNullableObject(NSDictionary *dict, id key) { + id result = dict[key]; + return (result == [NSNull null]) ? nil : result; +} +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + +@interface PigeonMultiFactorSession () ++ (PigeonMultiFactorSession *)fromMap:(NSDictionary *)dict; ++ (nullable PigeonMultiFactorSession *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface PigeonPhoneMultiFactorAssertion () ++ (PigeonPhoneMultiFactorAssertion *)fromMap:(NSDictionary *)dict; ++ (nullable PigeonPhoneMultiFactorAssertion *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface PigeonMultiFactorInfo () ++ (PigeonMultiFactorInfo *)fromMap:(NSDictionary *)dict; ++ (nullable PigeonMultiFactorInfo *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end + +@implementation PigeonMultiFactorSession ++ (instancetype)makeWithId:(NSString *)id { + PigeonMultiFactorSession *pigeonResult = [[PigeonMultiFactorSession alloc] init]; + pigeonResult.id = id; + return pigeonResult; +} ++ (PigeonMultiFactorSession *)fromMap:(NSDictionary *)dict { + PigeonMultiFactorSession *pigeonResult = [[PigeonMultiFactorSession alloc] init]; + pigeonResult.id = GetNullableObject(dict, @"id"); + NSAssert(pigeonResult.id != nil, @""); + return pigeonResult; +} ++ (nullable PigeonMultiFactorSession *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [PigeonMultiFactorSession fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"id" : (self.id ?: [NSNull null]), + }; +} +@end + +@implementation PigeonPhoneMultiFactorAssertion ++ (instancetype)makeWithVerificationId:(NSString *)verificationId + verificationCode:(NSString *)verificationCode { + PigeonPhoneMultiFactorAssertion *pigeonResult = [[PigeonPhoneMultiFactorAssertion alloc] init]; + pigeonResult.verificationId = verificationId; + pigeonResult.verificationCode = verificationCode; + return pigeonResult; +} ++ (PigeonPhoneMultiFactorAssertion *)fromMap:(NSDictionary *)dict { + PigeonPhoneMultiFactorAssertion *pigeonResult = [[PigeonPhoneMultiFactorAssertion alloc] init]; + pigeonResult.verificationId = GetNullableObject(dict, @"verificationId"); + NSAssert(pigeonResult.verificationId != nil, @""); + pigeonResult.verificationCode = GetNullableObject(dict, @"verificationCode"); + NSAssert(pigeonResult.verificationCode != nil, @""); + return pigeonResult; +} ++ (nullable PigeonPhoneMultiFactorAssertion *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [PigeonPhoneMultiFactorAssertion fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"verificationId" : (self.verificationId ?: [NSNull null]), + @"verificationCode" : (self.verificationCode ?: [NSNull null]), + }; +} +@end + +@implementation PigeonMultiFactorInfo ++ (instancetype)makeWithDisplayName:(nullable NSString *)displayName + enrollmentTimestamp:(NSNumber *)enrollmentTimestamp + factorId:(NSString *)factorId + uid:(NSString *)uid + phoneNumber:(nullable NSString *)phoneNumber { + PigeonMultiFactorInfo *pigeonResult = [[PigeonMultiFactorInfo alloc] init]; + pigeonResult.displayName = displayName; + pigeonResult.enrollmentTimestamp = enrollmentTimestamp; + pigeonResult.factorId = factorId; + pigeonResult.uid = uid; + pigeonResult.phoneNumber = phoneNumber; + return pigeonResult; +} ++ (PigeonMultiFactorInfo *)fromMap:(NSDictionary *)dict { + PigeonMultiFactorInfo *pigeonResult = [[PigeonMultiFactorInfo alloc] init]; + pigeonResult.displayName = GetNullableObject(dict, @"displayName"); + pigeonResult.enrollmentTimestamp = GetNullableObject(dict, @"enrollmentTimestamp"); + NSAssert(pigeonResult.enrollmentTimestamp != nil, @""); + pigeonResult.factorId = GetNullableObject(dict, @"factorId"); + NSAssert(pigeonResult.factorId != nil, @""); + pigeonResult.uid = GetNullableObject(dict, @"uid"); + NSAssert(pigeonResult.uid != nil, @""); + pigeonResult.phoneNumber = GetNullableObject(dict, @"phoneNumber"); + return pigeonResult; +} ++ (nullable PigeonMultiFactorInfo *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [PigeonMultiFactorInfo fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"displayName" : (self.displayName ?: [NSNull null]), + @"enrollmentTimestamp" : (self.enrollmentTimestamp ?: [NSNull null]), + @"factorId" : (self.factorId ?: [NSNull null]), + @"uid" : (self.uid ?: [NSNull null]), + @"phoneNumber" : (self.phoneNumber ?: [NSNull null]), + }; +} +@end + +@interface MultiFactorUserHostApiCodecReader : FlutterStandardReader +@end +@implementation MultiFactorUserHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [PigeonMultiFactorInfo fromMap:[self readValue]]; + + case 129: + return [PigeonMultiFactorSession fromMap:[self readValue]]; + + case 130: + return [PigeonPhoneMultiFactorAssertion fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface MultiFactorUserHostApiCodecWriter : FlutterStandardWriter +@end +@implementation MultiFactorUserHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[PigeonMultiFactorInfo class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[PigeonMultiFactorSession class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[PigeonPhoneMultiFactorAssertion class]]) { + [self writeByte:130]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface MultiFactorUserHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation MultiFactorUserHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[MultiFactorUserHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[MultiFactorUserHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *MultiFactorUserHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + MultiFactorUserHostApiCodecReaderWriter *readerWriter = + [[MultiFactorUserHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void MultiFactorUserHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.MultiFactorUserHostApi.enrollPhone" + binaryMessenger:binaryMessenger + codec:MultiFactorUserHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(enrollPhoneAppName: + assertion:displayName:completion:)], + @"MultiFactorUserHostApi api (%@) doesn't respond to " + @"@selector(enrollPhoneAppName:assertion:displayName:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSString *arg_appName = GetNullableObjectAtIndex(args, 0); + PigeonPhoneMultiFactorAssertion *arg_assertion = GetNullableObjectAtIndex(args, 1); + NSString *arg_displayName = GetNullableObjectAtIndex(args, 2); + [api enrollPhoneAppName:arg_appName + assertion:arg_assertion + displayName:arg_displayName + completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.MultiFactorUserHostApi.getSession" + binaryMessenger:binaryMessenger + codec:MultiFactorUserHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getSessionAppName:completion:)], + @"MultiFactorUserHostApi api (%@) doesn't respond to " + @"@selector(getSessionAppName:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSString *arg_appName = GetNullableObjectAtIndex(args, 0); + [api getSessionAppName:arg_appName + completion:^(PigeonMultiFactorSession *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.MultiFactorUserHostApi.unenroll" + binaryMessenger:binaryMessenger + codec:MultiFactorUserHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(unenrollAppName:factorUid:completion:)], + @"MultiFactorUserHostApi api (%@) doesn't respond to " + @"@selector(unenrollAppName:factorUid:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSString *arg_appName = GetNullableObjectAtIndex(args, 0); + NSString *arg_factorUid = GetNullableObjectAtIndex(args, 1); + [api unenrollAppName:arg_appName + factorUid:arg_factorUid + completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.MultiFactorUserHostApi.getEnrolledFactors" + binaryMessenger:binaryMessenger + codec:MultiFactorUserHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getEnrolledFactorsAppName:completion:)], + @"MultiFactorUserHostApi api (%@) doesn't respond to " + @"@selector(getEnrolledFactorsAppName:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSString *arg_appName = GetNullableObjectAtIndex(args, 0); + [api getEnrolledFactorsAppName:arg_appName + completion:^(NSArray *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface MultiFactoResolverHostApiCodecReader : FlutterStandardReader +@end +@implementation MultiFactoResolverHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [PigeonMultiFactorInfo fromMap:[self readValue]]; + + case 129: + return [PigeonMultiFactorSession fromMap:[self readValue]]; + + case 130: + return [PigeonPhoneMultiFactorAssertion fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface MultiFactoResolverHostApiCodecWriter : FlutterStandardWriter +@end +@implementation MultiFactoResolverHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[PigeonMultiFactorInfo class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[PigeonMultiFactorSession class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[PigeonPhoneMultiFactorAssertion class]]) { + [self writeByte:130]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface MultiFactoResolverHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation MultiFactoResolverHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[MultiFactoResolverHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[MultiFactoResolverHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *MultiFactoResolverHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + MultiFactoResolverHostApiCodecReaderWriter *readerWriter = + [[MultiFactoResolverHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void MultiFactoResolverHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.MultiFactoResolverHostApi.resolveSignIn" + binaryMessenger:binaryMessenger + codec:MultiFactoResolverHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(resolveSignInResolverId:assertion:completion:)], + @"MultiFactoResolverHostApi api (%@) doesn't respond to " + @"@selector(resolveSignInResolverId:assertion:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSString *arg_resolverId = GetNullableObjectAtIndex(args, 0); + PigeonPhoneMultiFactorAssertion *arg_assertion = GetNullableObjectAtIndex(args, 1); + [api resolveSignInResolverId:arg_resolverId + assertion:arg_assertion + completion:^(NSDictionary *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface GenerateInterfacesCodecReader : FlutterStandardReader +@end +@implementation GenerateInterfacesCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [PigeonMultiFactorInfo fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface GenerateInterfacesCodecWriter : FlutterStandardWriter +@end +@implementation GenerateInterfacesCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[PigeonMultiFactorInfo class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface GenerateInterfacesCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation GenerateInterfacesCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[GenerateInterfacesCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[GenerateInterfacesCodecReader alloc] initWithData:data]; +} +@end + +NSObject *GenerateInterfacesGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + GenerateInterfacesCodecReaderWriter *readerWriter = + [[GenerateInterfacesCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void GenerateInterfacesSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.GenerateInterfaces.generateInterfaces" + binaryMessenger:binaryMessenger + codec:GenerateInterfacesGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(generateInterfacesInfo:error:)], + @"GenerateInterfaces api (%@) doesn't respond to " + @"@selector(generateInterfacesInfo:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + PigeonMultiFactorInfo *arg_info = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api generateInterfacesInfo:arg_info error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart index 8bec299f421a..d69a573c6352 100755 --- a/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart @@ -6,14 +6,17 @@ library firebase_auth; import 'dart:async'; -import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; import 'package:flutter/foundation.dart'; export 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart' show FirebaseAuthException, + FirebaseAuthMultiFactorException, + MultiFactorInfo, + PhoneMultiFactorInfo, IdTokenResult, UserMetadata, UserInfo, @@ -47,12 +50,12 @@ export 'package:firebase_auth_platform_interface/firebase_auth_platform_interfac RecaptchaVerifierOnError, RecaptchaVerifierSize, RecaptchaVerifierTheme; - export 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart' show FirebaseException; -part 'src/firebase_auth.dart'; -part 'src/user_credential.dart'; -part 'src/user.dart'; part 'src/confirmation_result.dart'; +part 'src/firebase_auth.dart'; +part 'src/multi_factor.dart'; part 'src/recaptcha_verifier.dart'; +part 'src/user.dart'; +part 'src/user_credential.dart'; diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index dbeb717d11c2..742399cfe156 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -607,7 +607,7 @@ class FirebaseAuth extends FirebasePluginPlatform { // If we add a recaptcha to the page by creating a new instance, we must // also clear that instance before proceeding. bool mustClear = verifier == null; - verifier ??= RecaptchaVerifier(); + verifier ??= RecaptchaVerifier(auth: _delegate); final result = await _delegate.signInWithPhoneNumber(phoneNumber, verifier.delegate); if (mustClear) { @@ -683,6 +683,13 @@ class FirebaseAuth extends FirebasePluginPlatform { /// [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 ('+'). + /// Should be null if it's a multi-factor sign in. + /// + /// [multiFactorInfo] The multi factor info you're using to verify the phone number. + /// Should be set if a [multiFactorSession] is provided. + /// + /// [multiFactorSession] The multi factor session you're using to verify the phone number. + /// Should be set if a [multiFactorInfo] is provided. /// /// [timeout] The maximum amount of time you are willing to wait for SMS /// auto-retrieval to be completed by the library. Maximum allowed value @@ -707,7 +714,8 @@ class FirebaseAuth extends FirebasePluginPlatform { /// [codeAutoRetrievalTimeout] Triggered when SMS auto-retrieval times out and /// provide a [verificationId]. Future verifyPhoneNumber({ - required String phoneNumber, + String? phoneNumber, + PhoneMultiFactorInfo? multiFactorInfo, required PhoneVerificationCompleted verificationCompleted, required PhoneVerificationFailed verificationFailed, required PhoneCodeSent codeSent, @@ -715,9 +723,15 @@ class FirebaseAuth extends FirebasePluginPlatform { @visibleForTesting String? autoRetrievedSmsCodeForTesting, Duration timeout = const Duration(seconds: 30), int? forceResendingToken, + MultiFactorSession? multiFactorSession, }) { + assert( + phoneNumber != null || multiFactorInfo != null, + 'Either phoneNumber or multiFactorInfo must be provided.', + ); return _delegate.verifyPhoneNumber( phoneNumber: phoneNumber, + multiFactorInfo: multiFactorInfo, timeout: timeout, forceResendingToken: forceResendingToken, verificationCompleted: verificationCompleted, @@ -726,6 +740,7 @@ class FirebaseAuth extends FirebasePluginPlatform { codeAutoRetrievalTimeout: codeAutoRetrievalTimeout, // ignore: invalid_use_of_visible_for_testing_member autoRetrievedSmsCodeForTesting: autoRetrievedSmsCodeForTesting, + multiFactorSession: multiFactorSession, ); } diff --git a/packages/firebase_auth/firebase_auth/lib/src/multi_factor.dart b/packages/firebase_auth/firebase_auth/lib/src/multi_factor.dart new file mode 100644 index 000000000000..4fa685133f85 --- /dev/null +++ b/packages/firebase_auth/firebase_auth/lib/src/multi_factor.dart @@ -0,0 +1,53 @@ +part of firebase_auth; + +/// Defines multi-factor related properties and operations pertaining to a [User]. +/// This class acts as the main entry point for enrolling or un-enrolling +/// second factors for a user, and provides access to their currently enrolled factors. +class MultiFactor { + MultiFactorPlatform _delegate; + + MultiFactor._(this._delegate); + + /// Returns a session identifier for a second factor enrollment operation. + Future getSession() { + return _delegate.getSession(); + } + + /// Enrolls a second factor as identified by the [MultiFactorAssertion] parameter for the current user. + /// + /// [displayName] can be used to provide a display name for the second factor. + Future enroll( + MultiFactorAssertionPlatform assertion, { + String? displayName, + }) async { + return _delegate.enroll(assertion, displayName: displayName); + } + + /// Unenrolls a second factor from this user. + /// + /// [factorUid] is the unique identifier of the second factor to unenroll. + /// [multiFactorInfo] is the [MultiFactorInfo] of the second factor to unenroll. + /// Only one of [factorUid] or [multiFactorInfo] should be provided. + Future unenroll({String? factorUid, MultiFactorInfo? multiFactorInfo}) { + return _delegate.unenroll( + factorUid: factorUid, + multiFactorInfo: multiFactorInfo, + ); + } + + /// Returns a list of the [MultiFactorInfo] already associated with this user. + Future> getEnrolledFactors() { + return _delegate.getEnrolledFactors(); + } +} + +/// Provider for generating a PhoneMultiFactorAssertion. +class PhoneMultiFactorGenerator { + /// Transforms a PhoneAuthCredential into a [MultiFactorAssertion] + /// which can be used to confirm ownership of a phone second factor. + static MultiFactorAssertionPlatform getAssertion( + PhoneAuthCredential credential, + ) { + return PhoneMultiFactorGeneratorPlatform.instance.getAssertion(credential); + } +} diff --git a/packages/firebase_auth/firebase_auth/lib/src/recaptcha_verifier.dart b/packages/firebase_auth/firebase_auth/lib/src/recaptcha_verifier.dart index 9721757c622e..7aa4bbf74d2b 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/recaptcha_verifier.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/recaptcha_verifier.dart @@ -46,6 +46,7 @@ class RecaptchaVerifier { /// /// [onExpired] An optional callback which is called when the reCAPTCHA expires. factory RecaptchaVerifier({ + required FirebaseAuthPlatform auth, String? container, RecaptchaVerifierSize size = RecaptchaVerifierSize.normal, RecaptchaVerifierTheme theme = RecaptchaVerifierTheme.light, @@ -55,6 +56,7 @@ class RecaptchaVerifier { }) { return RecaptchaVerifier._( _factory.delegateFor( + auth: auth, container: container, size: size, theme: theme, diff --git a/packages/firebase_auth/firebase_auth/lib/src/user.dart b/packages/firebase_auth/firebase_auth/lib/src/user.dart index f839cbf08036..fe691c57383f 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/user.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/user.dart @@ -10,6 +10,7 @@ class User { UserPlatform _delegate; final FirebaseAuth _auth; + MultiFactor? _multiFactor; User._(this._auth, this._delegate) { UserPlatform.verifyExtends(_delegate); @@ -262,7 +263,7 @@ class User { // If we add a recaptcha to the page by creating a new instance, we must // also clear that instance before proceeding. bool mustClear = verifier == null; - verifier ??= RecaptchaVerifier(); + verifier ??= RecaptchaVerifier(auth: _delegate.auth); final result = await _delegate.linkWithPhoneNumber(phoneNumber, verifier.delegate); if (mustClear) { @@ -421,6 +422,10 @@ class User { await _delegate.verifyBeforeUpdateEmail(newEmail, actionCodeSettings); } + MultiFactor get multiFactor { + return _multiFactor ??= MultiFactor._(_delegate.multiFactor); + } + @override String toString() { return '$User(' diff --git a/packages/firebase_auth/firebase_auth/macos/Classes/Private/CustomPigeonHeader.h b/packages/firebase_auth/firebase_auth/macos/Classes/Private/CustomPigeonHeader.h new file mode 120000 index 000000000000..a70e2787a3a4 --- /dev/null +++ b/packages/firebase_auth/firebase_auth/macos/Classes/Private/CustomPigeonHeader.h @@ -0,0 +1 @@ +../../../ios/Classes/Private/CustomPigeonHeader.h \ No newline at end of file diff --git a/packages/firebase_auth/firebase_auth/macos/Classes/messages.g.h b/packages/firebase_auth/firebase_auth/macos/Classes/messages.g.h new file mode 120000 index 000000000000..8c85826b010b --- /dev/null +++ b/packages/firebase_auth/firebase_auth/macos/Classes/messages.g.h @@ -0,0 +1 @@ +../../ios/Classes/messages.g.h \ No newline at end of file diff --git a/packages/firebase_auth/firebase_auth/macos/Classes/messages.g.m b/packages/firebase_auth/firebase_auth/macos/Classes/messages.g.m new file mode 120000 index 000000000000..5c65810afc79 --- /dev/null +++ b/packages/firebase_auth/firebase_auth/macos/Classes/messages.g.m @@ -0,0 +1 @@ +../../ios/Classes/messages.g.m \ No newline at end of file diff --git a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart index e5145109b42a..83fc47e55e8d 100644 --- a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart +++ b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart @@ -93,7 +93,8 @@ void main() { auth = FirebaseAuth.instanceFor(app: app); user = kMockUser; - mockUserPlatform = MockUserPlatform(mockAuthPlatform, user); + mockUserPlatform = MockUserPlatform( + mockAuthPlatform, TestMultiFactorPlatform(mockAuthPlatform), user); mockConfirmationResultPlatform = MockConfirmationResultPlatform(); mockAdditionalUserInfo = AdditionalUserInfo( isNewUser: false, @@ -987,6 +988,8 @@ class MockFirebaseAuth extends Mock @override Future verifyPhoneNumber({ String? phoneNumber, + PhoneMultiFactorInfo? multiFactorInfo, + MultiFactorSession? multiFactorSession, Object? verificationCompleted, Object? verificationFailed, Object? codeSent, @@ -1037,8 +1040,9 @@ class FakeFirebaseAuthPlatform extends Fake class MockUserPlatform extends Mock with MockPlatformInterfaceMixin implements TestUserPlatform { - MockUserPlatform(FirebaseAuthPlatform auth, Map _user) { - TestUserPlatform(auth, _user); + MockUserPlatform(FirebaseAuthPlatform auth, MultiFactorPlatform multiFactor, + Map _user) { + TestUserPlatform(auth, multiFactor, _user); } } @@ -1155,8 +1159,13 @@ class TestAuthProvider extends AuthProvider { } class TestUserPlatform extends UserPlatform { - TestUserPlatform(FirebaseAuthPlatform auth, Map data) - : super(auth, data); + TestUserPlatform(FirebaseAuthPlatform auth, MultiFactorPlatform multiFactor, + Map data) + : super(auth, multiFactor, data); +} + +class TestMultiFactorPlatform extends MultiFactorPlatform { + TestMultiFactorPlatform(FirebaseAuthPlatform auth) : super(auth); } class TestUserCredentialPlatform extends UserCredentialPlatform { diff --git a/packages/firebase_auth/firebase_auth/test/user_test.dart b/packages/firebase_auth/firebase_auth/test/user_test.dart index 182dcf9abc18..8879eec32cdf 100644 --- a/packages/firebase_auth/firebase_auth/test/user_test.dart +++ b/packages/firebase_auth/firebase_auth/test/user_test.dart @@ -5,15 +5,16 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_firebase_auth.dart'; +import 'package:mockito/mockito.dart'; // import 'package:mockito/annotations.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:mockito/mockito.dart'; // import './user_test.mocks.dart'; import './mock.dart'; +import 'firebase_auth_test.dart'; Map kMockUser1 = { 'isAnonymous': true, @@ -320,8 +321,8 @@ void main() { }); test('toString()', () async { - when(mockAuthPlatform.currentUser) - .thenReturn(TestUserPlatform(mockAuthPlatform, user!)); + when(mockAuthPlatform.currentUser).thenReturn(TestUserPlatform( + mockAuthPlatform, TestMultiFactorPlatform(mockAuthPlatform), user!)); const userInfo = 'UserInfo(' 'displayName: Flutter Test User, ' @@ -401,7 +402,7 @@ class MockUserPlatform extends Mock with MockPlatformInterfaceMixin implements TestUserPlatform { MockUserPlatform(FirebaseAuthPlatform auth, Map _user) { - TestUserPlatform(auth, _user); + TestUserPlatform(auth, TestMultiFactorPlatform(auth), _user); } @override @@ -566,8 +567,9 @@ class TestFirebaseAuthPlatform extends FirebaseAuthPlatform { } class TestUserPlatform extends UserPlatform { - TestUserPlatform(FirebaseAuthPlatform auth, Map data) - : super(auth, data); + TestUserPlatform(FirebaseAuthPlatform auth, MultiFactorPlatform multiFactor, + Map data) + : super(auth, multiFactor, data); } class TestUserCredentialPlatform extends UserCredentialPlatform { diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/firebase_auth_platform_interface.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/firebase_auth_platform_interface.dart index e80ed4099da4..3691f12ad66e 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/firebase_auth_platform_interface.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/firebase_auth_platform_interface.dart @@ -11,9 +11,11 @@ export 'src/additional_user_info.dart'; export 'src/auth_credential.dart'; export 'src/auth_provider.dart'; export 'src/firebase_auth_exception.dart'; +export 'src/firebase_auth_multi_factor_exception.dart'; export 'src/id_token_result.dart'; export 'src/platform_interface/platform_interface_confirmation_result.dart'; export 'src/platform_interface/platform_interface_firebase_auth.dart'; +export 'src/platform_interface/platform_interface_multi_factor.dart'; export 'src/platform_interface/platform_interface_recaptcha_verifier_factory.dart'; export 'src/platform_interface/platform_interface_user.dart'; export 'src/platform_interface/platform_interface_user_credential.dart'; diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/firebase_auth_multi_factor_exception.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/firebase_auth_multi_factor_exception.dart new file mode 100644 index 000000000000..d928fc4cc3ec --- /dev/null +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/firebase_auth_multi_factor_exception.dart @@ -0,0 +1,33 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:meta/meta.dart'; + +/// MultiFactor exception related to Firebase Authentication. Check the error code +/// and message for more details. +class FirebaseAuthMultiFactorException extends FirebaseAuthException + implements Exception { + // ignore: public_member_api_docs + @protected + FirebaseAuthMultiFactorException({ + String? message, + required String code, + String? email, + AuthCredential? credential, + String? phoneNumber, + String? tenantId, + required this.resolver, + }) : super( + message: message, + code: code, + email: email, + credential: credential, + phoneNumber: phoneNumber, + tenantId: tenantId, + ); + + final MultiFactorResolverPlatform resolver; +} diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_firebase_auth.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_firebase_auth.dart index b24ced1727ad..2f98493af40c 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_firebase_auth.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:io' show Platform; +import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_multi_factor.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -22,10 +23,14 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform { 'plugins.flutter.io/firebase_auth', ); + /// Map of [MethodChannelFirebaseAuth] that can be get with Firebase App Name. static Map - _methodChannelFirebaseAuthInstances = + methodChannelFirebaseAuthInstances = {}; + static Map _multiFactorInstances = + {}; + static final Map>> _authStateChangesListeners = >>{}; @@ -109,15 +114,22 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform { // ignore: close_sinks final streamController = _authStateChangesListeners[appName]!; MethodChannelFirebaseAuth instance = - _methodChannelFirebaseAuthInstances[appName]!; + methodChannelFirebaseAuthInstances[appName]!; + + MethodChannelMultiFactor? multiFactorInstance = + _multiFactorInstances[appName]; + if (multiFactorInstance == null) { + multiFactorInstance = MethodChannelMultiFactor(instance); + _multiFactorInstances[appName] = multiFactorInstance; + } final userMap = arguments['user']; if (userMap == null) { instance.currentUser = null; streamController.add(const _ValueWrapper.absent()); } else { - final MethodChannelUser user = - MethodChannelUser(instance, userMap.cast()); + final MethodChannelUser user = MethodChannelUser( + instance, multiFactorInstance, userMap.cast()); // TODO(rousselGit): should this logic be moved to the setter instead? instance.currentUser = user; @@ -138,7 +150,13 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform { // ignore: close_sinks userChangesStreamController = _userChangesListeners[appName]!; MethodChannelFirebaseAuth instance = - _methodChannelFirebaseAuthInstances[appName]!; + methodChannelFirebaseAuthInstances[appName]!; + MethodChannelMultiFactor? multiFactorInstance = + _multiFactorInstances[appName]; + if (multiFactorInstance == null) { + multiFactorInstance = MethodChannelMultiFactor(instance); + _multiFactorInstances[appName] = multiFactorInstance; + } final userMap = arguments['user']; if (userMap == null) { @@ -146,8 +164,8 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform { idTokenStreamController.add(const _ValueWrapper.absent()); userChangesStreamController.add(const _ValueWrapper.absent()); } else { - final MethodChannelUser user = - MethodChannelUser(instance, userMap.cast()); + final MethodChannelUser user = MethodChannelUser( + instance, multiFactorInstance, userMap.cast()); // TODO(rousselGit): should this logic be moved to the setter instead? instance.currentUser = user; @@ -170,7 +188,7 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform { /// Instances are cached and reused for incoming event handlers. @override FirebaseAuthPlatform delegateFor({required FirebaseApp app}) { - return _methodChannelFirebaseAuthInstances.putIfAbsent(app.name, () { + return methodChannelFirebaseAuthInstances.putIfAbsent(app.name, () { return MethodChannelFirebaseAuth(app: app); }); } @@ -181,7 +199,8 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform { String? languageCode, }) { if (currentUser != null) { - this.currentUser = MethodChannelUser(this, currentUser); + final multiFactor = MethodChannelMultiFactor(this); + this.currentUser = MethodChannelUser(this, multiFactor, currentUser); } this.languageCode = languageCode; @@ -574,7 +593,8 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform { @override Future verifyPhoneNumber({ - required String phoneNumber, + String? phoneNumber, + MultiFactorInfo? multiFactorInfo, required PhoneVerificationCompleted verificationCompleted, required PhoneVerificationFailed verificationFailed, required PhoneCodeSent codeSent, @@ -582,6 +602,7 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform { String? autoRetrievedSmsCodeForTesting, Duration timeout = const Duration(seconds: 30), int? forceResendingToken, + MultiFactorSession? multiFactorSession, }) async { if (defaultTargetPlatform == TargetPlatform.macOS) { throw UnimplementedError( @@ -593,10 +614,14 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform { final eventChannelName = await channel.invokeMethod( 'Auth#verifyPhoneNumber', _withChannelDefaults({ - 'phoneNumber': phoneNumber, + if (phoneNumber != null) 'phoneNumber': phoneNumber, + if (multiFactorInfo?.uid != null) + 'multiFactorInfo': multiFactorInfo?.uid, 'timeout': timeout.inMilliseconds, 'forceResendingToken': forceResendingToken, 'autoRetrievedSmsCodeForTesting': autoRetrievedSmsCodeForTesting, + if (multiFactorSession?.id != null) + 'multiFactorSessionId': multiFactorSession!.id, })); EventChannel(eventChannelName!) diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart new file mode 100644 index 000000000000..2012cb5a846f --- /dev/null +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart @@ -0,0 +1,151 @@ +import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_firebase_auth.dart'; +import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_user_credential.dart'; +import 'package:firebase_auth_platform_interface/src/method_channel/utils/pigeon_helper.dart'; +import 'package:firebase_auth_platform_interface/src/pigeon/messages.pigeon.dart'; + +class MethodChannelMultiFactor extends MultiFactorPlatform { + /// Constructs a new [MethodChannelMultiFactor] instance. + MethodChannelMultiFactor(FirebaseAuthPlatform auth) : super(auth); + + final _api = MultiFactorUserHostApi(); + + @override + Future getSession() async { + final pigeonObject = await _api.getSession(auth.app.name); + return MultiFactorSession(pigeonObject.id); + } + + @override + Future enroll( + MultiFactorAssertionPlatform assertion, { + String? displayName, + }) async { + final _assertion = assertion as MultiFactorAssertion; + + if (_assertion.credential is PhoneAuthCredential) { + final credential = _assertion.credential as PhoneAuthCredential; + final verificationId = credential.verificationId; + final verificationCode = credential.smsCode; + + if (verificationCode == null) { + throw ArgumentError('verificationCode must not be null'); + } + if (verificationId == null) { + throw ArgumentError('verificationId must not be null'); + } + + await _api.enrollPhone( + auth.app.name, + PigeonPhoneMultiFactorAssertion( + verificationId: verificationId, + verificationCode: verificationCode, + ), + displayName, + ); + } else { + throw UnimplementedError( + 'Credential type ${_assertion.credential} is not supported yet', + ); + } + } + + @override + Future unenroll({ + String? factorUid, + MultiFactorInfo? multiFactorInfo, + }) { + final uidToUnenroll = factorUid ?? multiFactorInfo?.uid; + if (uidToUnenroll == null) { + throw ArgumentError( + 'Either factorUid or multiFactorInfo must not be null', + ); + } + + return _api.unenroll( + auth.app.name, + uidToUnenroll, + ); + } + + @override + Future> getEnrolledFactors() async { + final data = await _api.getEnrolledFactors(auth.app.name); + return multiFactorInfoPigeonToObject(data); + } +} + +class MethodChannelMultiFactorResolver extends MultiFactorResolverPlatform { + MethodChannelMultiFactorResolver( + List hints, + MultiFactorSession session, + String resolverId, + MethodChannelFirebaseAuth auth, + ) : _resolverId = resolverId, + _auth = auth, + super(hints, session); + + final String _resolverId; + + final MethodChannelFirebaseAuth _auth; + final _api = MultiFactoResolverHostApi(); + + @override + Future resolveSignIn( + MultiFactorAssertionPlatform assertion, + ) async { + final _assertion = assertion as MultiFactorAssertion; + + if (_assertion.credential is PhoneAuthCredential) { + final credential = _assertion.credential as PhoneAuthCredential; + final verificationId = credential.verificationId; + final verificationCode = credential.smsCode; + + if (verificationCode == null) { + throw ArgumentError('verificationCode must not be null'); + } + if (verificationId == null) { + throw ArgumentError('verificationId must not be null'); + } + + final data = await _api.resolveSignIn( + _resolverId, + PigeonPhoneMultiFactorAssertion( + verificationId: verificationId, + verificationCode: verificationCode, + ), + ); + + MethodChannelUserCredential userCredential = + MethodChannelUserCredential(_auth, data.cast()); + + return userCredential; + } else { + throw UnimplementedError( + 'Credential type ${_assertion.credential} is not supported yet', + ); + } + } +} + +/// Represents an assertion that the Firebase Authentication server +/// can use to authenticate a user as part of a multi-factor flow. +class MultiFactorAssertion extends MultiFactorAssertionPlatform { + MultiFactorAssertion(this.credential) : super(); + + /// Associated credential to the assertion + final AuthCredential credential; +} + +/// Helper class used to generate PhoneMultiFactorAssertions. +class MethodChannelPhoneMultiFactorGenerator + extends PhoneMultiFactorGeneratorPlatform { + /// Transforms a PhoneAuthCredential into a [MultiFactorAssertion] + /// which can be used to confirm ownership of a phone second factor. + @override + MultiFactorAssertionPlatform getAssertion( + PhoneAuthCredential credential, + ) { + return MultiFactorAssertion(credential); + } +} diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_user.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_user.dart index 81dbc5bd18b8..5d7a1d14cbea 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_user.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_user.dart @@ -14,8 +14,9 @@ import 'utils/exception.dart'; /// Method Channel delegate for [UserPlatform] instances. class MethodChannelUser extends UserPlatform { /// Constructs a new [MethodChannelUser] instance. - MethodChannelUser(FirebaseAuthPlatform auth, Map data) - : super(auth, data); + MethodChannelUser(FirebaseAuthPlatform auth, MultiFactorPlatform multiFactor, + Map data) + : super(auth, multiFactor, data); /// Attaches generic default values to method channel arguments. Map _withChannelDefaults(Map other) { @@ -129,7 +130,7 @@ class MethodChannelUser extends UserPlatform { .invokeMapMethod( 'User#reload', _withChannelDefaults({})))!; - MethodChannelUser user = MethodChannelUser(auth, data); + MethodChannelUser user = MethodChannelUser(auth, super.multiFactor, data); auth.currentUser = user; auth.sendAuthChangesEvent(auth.app.name, user); } catch (e, stack) { @@ -188,7 +189,7 @@ class MethodChannelUser extends UserPlatform { }, )))!; - MethodChannelUser user = MethodChannelUser(auth, data); + MethodChannelUser user = MethodChannelUser(auth, super.multiFactor, data); auth.currentUser = user; auth.sendAuthChangesEvent(auth.app.name, user); } catch (e, stack) { @@ -208,7 +209,7 @@ class MethodChannelUser extends UserPlatform { }, )))!; - MethodChannelUser user = MethodChannelUser(auth, data); + MethodChannelUser user = MethodChannelUser(auth, super.multiFactor, data); auth.currentUser = user; auth.sendAuthChangesEvent(auth.app.name, user); } catch (e, stack) { @@ -228,7 +229,7 @@ class MethodChannelUser extends UserPlatform { }, )))!; - MethodChannelUser user = MethodChannelUser(auth, data); + MethodChannelUser user = MethodChannelUser(auth, super.multiFactor, data); auth.currentUser = user; auth.sendAuthChangesEvent(auth.app.name, user); } catch (e, stack) { @@ -248,7 +249,7 @@ class MethodChannelUser extends UserPlatform { }, )))!; - MethodChannelUser user = MethodChannelUser(auth, data); + MethodChannelUser user = MethodChannelUser(auth, super.multiFactor, data); auth.currentUser = user; auth.sendAuthChangesEvent(auth.app.name, user); } catch (e, stack) { diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_user_credential.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_user_credential.dart index 5b0e78b5a100..b41f68a27aba 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_user_credential.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_user_credential.dart @@ -4,6 +4,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_multi_factor.dart'; import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_user.dart'; /// Method Channel delegate for [UserCredentialPlatform]. @@ -30,7 +31,7 @@ class MethodChannelUserCredential extends UserCredentialPlatform { ), user: data['user'] == null ? null - : MethodChannelUser( - auth, Map.from(data['user'])), + : MethodChannelUser(auth, MethodChannelMultiFactor(auth), + Map.from(data['user'])), ); } diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/exception.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/exception.dart index d268bdfab28c..3097890269ac 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/exception.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/exception.dart @@ -3,7 +3,12 @@ // 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 'package:collection/collection.dart'; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_firebase_auth.dart'; +import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_multi_factor.dart'; +import 'package:firebase_auth_platform_interface/src/method_channel/utils/pigeon_helper.dart'; +import 'package:firebase_auth_platform_interface/src/pigeon/messages.pigeon.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/services.dart'; @@ -40,6 +45,10 @@ FirebaseException platformExceptionToFirebaseAuthException( if (details != null) { code = details['code'] ?? code; + if (code == 'second-factor-required') { + return parseMultiFactorError(details); + } + message = details['message'] ?? message; if (details['additionalData'] != null) { @@ -64,3 +73,60 @@ FirebaseException platformExceptionToFirebaseAuthException( credential: credential, ); } + +FirebaseAuthMultiFactorException parseMultiFactorError( + Map details) { + final code = details['code'] as String?; + final message = details['message'] as String?; + final additionalData = details['additionalData'] as Map?; + + if (additionalData == null) { + throw FirebaseAuthException( + code: "Can't parse multi factor error", + message: message, + ); + } + + final pigeonMultiFactorInfo = + (additionalData['multiFactorHints'] as List? ?? []) + .whereNotNull() + .map( + PigeonMultiFactorInfo.decode, + ) + .toList(); + + final multiFactorInfo = multiFactorInfoPigeonToObject( + pigeonMultiFactorInfo, + ); + + final auth = MethodChannelFirebaseAuth + .methodChannelFirebaseAuthInstances[additionalData['appName']]; + + if (auth == null) { + throw FirebaseAuthException( + code: code ?? 'Unknown', + message: message, + ); + } + + final sessionId = additionalData['multiFactorSessionId'] as String?; + final resolverId = additionalData['multiFactorResolverId'] as String?; + if (sessionId == null || resolverId == null) { + throw FirebaseAuthException( + code: "Can't parse multi factor error", + message: message, + ); + } + final multiFactorResolver = MethodChannelMultiFactorResolver( + multiFactorInfo, + MultiFactorSession(sessionId), + resolverId, + auth, + ); + + return FirebaseAuthMultiFactorException( + code: code ?? 'Unknown', + message: message, + resolver: multiFactorResolver, + ); +} diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/pigeon_helper.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/pigeon_helper.dart new file mode 100644 index 000000000000..8926775f69f1 --- /dev/null +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/pigeon_helper.dart @@ -0,0 +1,25 @@ +import 'package:collection/collection.dart'; +import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_platform_interface/src/pigeon/messages.pigeon.dart'; + +List multiFactorInfoPigeonToObject( + List pigeonMultiFactorInfo, +) { + return pigeonMultiFactorInfo.whereNotNull().map((e) { + if (e.phoneNumber != null) { + return PhoneMultiFactorInfo( + displayName: e.displayName, + enrollmentTimestamp: e.enrollmentTimestamp, + factorId: e.factorId, + uid: e.uid, + phoneNumber: e.phoneNumber!, + ); + } + return MultiFactorInfo( + displayName: e.displayName, + enrollmentTimestamp: e.enrollmentTimestamp, + factorId: e.factorId, + uid: e.uid, + ); + }).toList(); +} diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/pigeon/messages.pigeon.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/pigeon/messages.pigeon.dart new file mode 100644 index 000000000000..3f3459ea322c --- /dev/null +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/pigeon/messages.pigeon.dart @@ -0,0 +1,391 @@ +// Autogenerated from Pigeon (v3.2.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; + +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; + +class PigeonMultiFactorSession { + PigeonMultiFactorSession({ + required this.id, + }); + + String id; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['id'] = id; + return pigeonMap; + } + + static PigeonMultiFactorSession decode(Object message) { + final Map pigeonMap = message as Map; + return PigeonMultiFactorSession( + id: pigeonMap['id']! as String, + ); + } +} + +class PigeonPhoneMultiFactorAssertion { + PigeonPhoneMultiFactorAssertion({ + required this.verificationId, + required this.verificationCode, + }); + + String verificationId; + String verificationCode; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['verificationId'] = verificationId; + pigeonMap['verificationCode'] = verificationCode; + return pigeonMap; + } + + static PigeonPhoneMultiFactorAssertion decode(Object message) { + final Map pigeonMap = message as Map; + return PigeonPhoneMultiFactorAssertion( + verificationId: pigeonMap['verificationId']! as String, + verificationCode: pigeonMap['verificationCode']! as String, + ); + } +} + +class PigeonMultiFactorInfo { + PigeonMultiFactorInfo({ + this.displayName, + required this.enrollmentTimestamp, + required this.factorId, + required this.uid, + this.phoneNumber, + }); + + String? displayName; + double enrollmentTimestamp; + String factorId; + String uid; + String? phoneNumber; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['displayName'] = displayName; + pigeonMap['enrollmentTimestamp'] = enrollmentTimestamp; + pigeonMap['factorId'] = factorId; + pigeonMap['uid'] = uid; + pigeonMap['phoneNumber'] = phoneNumber; + return pigeonMap; + } + + static PigeonMultiFactorInfo decode(Object message) { + final Map pigeonMap = message as Map; + return PigeonMultiFactorInfo( + displayName: pigeonMap['displayName'] as String?, + enrollmentTimestamp: pigeonMap['enrollmentTimestamp']! as double, + factorId: pigeonMap['factorId']! as String, + uid: pigeonMap['uid']! as String, + phoneNumber: pigeonMap['phoneNumber'] as String?, + ); + } +} + +class _MultiFactorUserHostApiCodec extends StandardMessageCodec { + const _MultiFactorUserHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is PigeonMultiFactorInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is PigeonMultiFactorSession) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is PigeonPhoneMultiFactorAssertion) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return PigeonMultiFactorInfo.decode(readValue(buffer)!); + + case 129: + return PigeonMultiFactorSession.decode(readValue(buffer)!); + + case 130: + return PigeonPhoneMultiFactorAssertion.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class MultiFactorUserHostApi { + /// Constructor for [MultiFactorUserHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + MultiFactorUserHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _MultiFactorUserHostApiCodec(); + + Future enrollPhone( + String arg_appName, + PigeonPhoneMultiFactorAssertion arg_assertion, + String? arg_displayName) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MultiFactorUserHostApi.enrollPhone', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_appName, arg_assertion, arg_displayName]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future getSession(String arg_appName) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MultiFactorUserHostApi.getSession', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_appName]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as PigeonMultiFactorSession?)!; + } + } + + Future unenroll(String arg_appName, String? arg_factorUid) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MultiFactorUserHostApi.unenroll', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_appName, arg_factorUid]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future> getEnrolledFactors( + String arg_appName) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MultiFactorUserHostApi.getEnrolledFactors', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_appName]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as List?)! + .cast(); + } + } +} + +class _MultiFactoResolverHostApiCodec extends StandardMessageCodec { + const _MultiFactoResolverHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is PigeonMultiFactorInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is PigeonMultiFactorSession) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is PigeonPhoneMultiFactorAssertion) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return PigeonMultiFactorInfo.decode(readValue(buffer)!); + + case 129: + return PigeonMultiFactorSession.decode(readValue(buffer)!); + + case 130: + return PigeonPhoneMultiFactorAssertion.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class MultiFactoResolverHostApi { + /// Constructor for [MultiFactoResolverHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + MultiFactoResolverHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _MultiFactoResolverHostApiCodec(); + + Future> resolveSignIn(String arg_resolverId, + PigeonPhoneMultiFactorAssertion arg_assertion) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MultiFactoResolverHostApi.resolveSignIn', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_resolverId, arg_assertion]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as Map?)! + .cast(); + } + } +} + +class _GenerateInterfacesCodec extends StandardMessageCodec { + const _GenerateInterfacesCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is PigeonMultiFactorInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return PigeonMultiFactorInfo.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class GenerateInterfaces { + /// Constructor for [GenerateInterfaces]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GenerateInterfaces({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _GenerateInterfacesCodec(); + + Future generateInterfaces(PigeonMultiFactorInfo arg_info) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GenerateInterfaces.generateInterfaces', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_info]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart index 7e407dfca753..16b3f119cf49 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart @@ -503,6 +503,7 @@ abstract class FirebaseAuthPlatform extends PlatformInterface { /// /// Confirm the link is a sign-in email link before calling this method, /// using [isSignInWithEmailLink]. + /// /// A [FirebaseAuthException] maybe thrown with the following error code: /// - **expired-action-code**: @@ -636,13 +637,15 @@ abstract class FirebaseAuthPlatform extends PlatformInterface { /// [codeAutoRetrievalTimeout] Triggered when SMS auto-retrieval times out and /// provide a [verificationId]. Future verifyPhoneNumber({ - required String phoneNumber, + String? phoneNumber, + PhoneMultiFactorInfo? multiFactorInfo, required PhoneVerificationCompleted verificationCompleted, required PhoneVerificationFailed verificationFailed, required PhoneCodeSent codeSent, required PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout, Duration timeout = const Duration(seconds: 30), int? forceResendingToken, + MultiFactorSession? multiFactorSession, // ignore: invalid_use_of_visible_for_testing_member @visibleForTesting String? autoRetrievedSmsCodeForTesting, }) { diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_multi_factor.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_multi_factor.dart new file mode 100644 index 000000000000..90a9537e74ec --- /dev/null +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_multi_factor.dart @@ -0,0 +1,163 @@ +import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_multi_factor.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// {@template .platformInterfaceMultiFactor} +/// The platform defining the interface of multi-factor related +/// properties and operations pertaining to a [User]. +/// {@endtemplate} +abstract class MultiFactorPlatform extends PlatformInterface { + /// {@macro .platformInterfaceMultiFactor} + MultiFactorPlatform( + this.auth, + ) : super(token: _token); + + /// The [FirebaseAuthPlatform] instance. + final FirebaseAuthPlatform auth; + + static final Object _token = Object(); + + /// Enrolls a second factor as identified by the [MultiFactorAssertion] parameter for the current user. + /// + /// [displayName] can be used to provide a display name for the second factor. + Future enroll( + MultiFactorAssertionPlatform assertion, { + String? displayName, + }) { + throw UnimplementedError('enroll() is not implemented'); + } + + /// Returns a session identifier for a second factor enrollment operation. + Future getSession() { + throw UnimplementedError('getSession() is not implemented'); + } + + /// Unenrolls a second factor from this user. + /// + /// [factorUid] is the unique identifier of the second factor to unenroll. + /// [multiFactorInfo] is the [MultiFactorInfo] of the second factor to unenroll. + /// Only one of [factorUid] or [multiFactorInfo] should be provided. + Future unenroll({String? factorUid, MultiFactorInfo? multiFactorInfo}) { + throw UnimplementedError('unenroll() is not implemented'); + } + + /// Returns a list of the [MultiFactorInfo] already associated with this user. + Future> getEnrolledFactors() { + throw UnimplementedError('getEnrolledFactors() is not implemented'); + } +} + +/// Identifies the current session to enroll a second factor or to complete sign in when previously enrolled. +/// +/// It contains additional context on the existing user, notably the confirmation that the user passed the first factor challenge. +class MultiFactorSession { + MultiFactorSession(this.id); + + final String id; +} + +/// Represents an assertion that the Firebase Authentication server +/// can use to authenticate a user as part of a multi-factor flow. +class MultiFactorAssertionPlatform {} + +/// {@macro .platformInterfaceMultiFactorResolverPlatform} +/// Utility class that contains methods to resolve second factor +/// requirements on users that have opted into two-factor authentication. +/// {@endtemplate} +class MultiFactorResolverPlatform { + /// {@macro .platformInterfaceMultiFactorResolverPlatform} + const MultiFactorResolverPlatform( + this.hints, + this.session, + ); + + /// List of [MultiFactorInfo] which represents the available + /// second factors that can be used to complete the sign-in for the current session. + final List hints; + + /// A MultiFactorSession, an opaque session identifier for the current sign-in flow. + final MultiFactorSession session; + + /// Completes sign in with a second factor using an MultiFactorAssertion which + /// confirms that the user has successfully completed the second factor challenge. + Future resolveSignIn( + MultiFactorAssertionPlatform assertion, + ) { + throw UnimplementedError('resolveSignIn() is not implemented'); + } +} + +/// Represents a single second factor means for the user. +/// +/// See direct subclasses for type-specific information. +class MultiFactorInfo { + const MultiFactorInfo({ + required this.factorId, + required this.enrollmentTimestamp, + required this.displayName, + required this.uid, + }); + + /// User-given display name for this second factor. + final String? displayName; + + /// The enrollment timestamp for this second factor in seconds since epoch (UTC midnight on January 1, 1970). + final double enrollmentTimestamp; + + /// The factor id of this second factor. + final String factorId; + + /// The unique identifier for this second factor. + final String uid; +} + +/// Represents the information for a phone second factor. +class PhoneMultiFactorInfo extends MultiFactorInfo { + const PhoneMultiFactorInfo({ + required String? displayName, + required double enrollmentTimestamp, + required String factorId, + required String uid, + required this.phoneNumber, + }) : super( + displayName: displayName, + enrollmentTimestamp: enrollmentTimestamp, + factorId: factorId, + uid: uid, + ); + + /// The phone number associated with this second factor verification method. + final String phoneNumber; +} + +/// Helper class used to generate PhoneMultiFactorAssertions. +class PhoneMultiFactorGeneratorPlatform extends PlatformInterface { + static PhoneMultiFactorGeneratorPlatform? _instance; + + static final Object _token = Object(); + + PhoneMultiFactorGeneratorPlatform() : super(token: _token); + + /// The current default [PhoneMultiFactorGeneratorPlatform] instance. + /// + /// It will always default to [MethodChannelPhoneMultiFactorGenerator] + /// if no other implementation was provided. + static PhoneMultiFactorGeneratorPlatform get instance { + _instance ??= MethodChannelPhoneMultiFactorGenerator(); + return _instance!; + } + + /// Sets the [PhoneMultiFactorGeneratorPlatform.instance] + static set instance(PhoneMultiFactorGeneratorPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Transforms a PhoneAuthCredential into a [MultiFactorAssertion] + /// which can be used to confirm ownership of a phone second factor. + MultiFactorAssertionPlatform getAssertion( + PhoneAuthCredential credential, + ) { + throw UnimplementedError('getAssertion() is not implemented'); + } +} diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_recaptcha_verifier_factory.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_recaptcha_verifier_factory.dart index 4e802f497899..90cc8a963fce 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_recaptcha_verifier_factory.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_recaptcha_verifier_factory.dart @@ -80,6 +80,7 @@ abstract class RecaptchaVerifierFactoryPlatform extends PlatformInterface { /// Underlying implementations can use this method to create the underlying /// implementation of a Recaptcha Verifier. RecaptchaVerifierFactoryPlatform delegateFor({ + required FirebaseAuthPlatform auth, String? container, RecaptchaVerifierSize size = RecaptchaVerifierSize.normal, RecaptchaVerifierTheme theme = RecaptchaVerifierTheme.light, diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_user.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_user.dart index d0fc99ce58da..03db4601c3ff 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_user.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_user.dart @@ -11,7 +11,7 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; /// A user account. abstract class UserPlatform extends PlatformInterface { // ignore: public_member_api_docs - UserPlatform(this.auth, Map user) + UserPlatform(this.auth, this.multiFactor, Map user) : _user = user, super(token: _token); @@ -25,6 +25,8 @@ abstract class UserPlatform extends PlatformInterface { /// The [FirebaseAuthPlatform] instance. final FirebaseAuthPlatform auth; + final MultiFactorPlatform multiFactor; + final Map _user; /// The users display name. diff --git a/packages/firebase_auth/firebase_auth_platform_interface/pigeons/messages.dart b/packages/firebase_auth/firebase_auth_platform_interface/pigeons/messages.dart new file mode 100644 index 000000000000..039389881ea2 --- /dev/null +++ b/packages/firebase_auth/firebase_auth_platform_interface/pigeons/messages.dart @@ -0,0 +1,89 @@ +// ignore_for_file: one_member_abstracts + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/pigeon/messages.pigeon.dart', + // We export in the lib folder to expose the class to other packages. + dartTestOut: 'test/pigeon/test_api.dart', + javaOut: + '../firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/GeneratedAndroidFirebaseAuth.java', + javaOptions: JavaOptions( + package: 'io.flutter.plugins.firebase.auth', + className: 'GeneratedAndroidFirebaseAuth', + ), + objcHeaderOut: '../firebase_auth/ios/Classes/messages.g.h', + objcSourceOut: '../firebase_auth/ios/Classes/messages.g.m', + ), +) +class PigeonMultiFactorSession { + const PigeonMultiFactorSession({ + required this.id, + }); + + final String id; +} + +class PigeonPhoneMultiFactorAssertion { + const PigeonPhoneMultiFactorAssertion({ + required this.verificationId, + required this.verificationCode, + }); + + final String verificationId; + final String verificationCode; +} + +class PigeonMultiFactorInfo { + const PigeonMultiFactorInfo({ + this.displayName, + required this.enrollmentTimestamp, + required this.factorId, + required this.uid, + required this.phoneNumber, + }); + + final String? displayName; + final double enrollmentTimestamp; + final String factorId; + final String uid; + final String? phoneNumber; +} + +@HostApi(dartHostTestHandler: 'TestMultiFactorUserHostApi') +abstract class MultiFactorUserHostApi { + @async + void enrollPhone( + String appName, + PigeonPhoneMultiFactorAssertion assertion, + String? displayName, + ); + + @async + PigeonMultiFactorSession getSession(String appName); + + @async + void unenroll( + String appName, + String? factorUid, + ); + + @async + List getEnrolledFactors(String appName); +} + +@HostApi(dartHostTestHandler: 'TestMultiFactoResolverHostApi') +abstract class MultiFactoResolverHostApi { + @async + Map resolveSignIn( + String resolverId, + PigeonPhoneMultiFactorAssertion assertion, + ); +} + +/// Only used to generate the object interface that are use outside of the Pigeon interface +@HostApi() +abstract class GenerateInterfaces { + void generateInterfaces(PigeonMultiFactorInfo info); +} diff --git a/packages/firebase_auth/firebase_auth_platform_interface/pubspec.yaml b/packages/firebase_auth/firebase_auth_platform_interface/pubspec.yaml index ad7350f17043..3d5997ac2047 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/pubspec.yaml +++ b/packages/firebase_auth/firebase_auth_platform_interface/pubspec.yaml @@ -7,10 +7,11 @@ repository: https://github.com/firebase/flutterfire/tree/master/packages/firebas version: 6.3.2 environment: - sdk: ">=2.16.0 <3.0.0" - flutter: ">=1.9.1+hotfix.5" + sdk: '>=2.16.0 <3.0.0' + flutter: '>=1.9.1+hotfix.5' dependencies: + collection: ^1.16.0 firebase_core: ^1.10.0 flutter: sdk: flutter @@ -22,3 +23,4 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 + pigeon: ^3.2.0 diff --git a/packages/firebase_auth/firebase_auth_platform_interface/test/method_channel_tests/method_channel_firebase_auth_test.dart b/packages/firebase_auth/firebase_auth_platform_interface/test/method_channel_tests/method_channel_firebase_auth_test.dart index da278b2a63c7..029eced21f76 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/test/method_channel_tests/method_channel_firebase_auth_test.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/test/method_channel_tests/method_channel_firebase_auth_test.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_firebase_auth.dart'; +import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_multi_factor.dart'; import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_user.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/services.dart'; @@ -17,6 +18,9 @@ import '../mock.dart'; void main() { setupFirebaseAuthMocks(); + + late MultiFactorPlatform multiFactor; + late FirebaseAuthPlatform auth; final List log = []; const String regularTestEmail = 'test@email.com'; @@ -173,6 +177,9 @@ void main() { }); auth = MethodChannelFirebaseAuth.instance.delegateFor(app: app); + // TODO(Lyokone): mock properly + multiFactor = MethodChannelMultiFactor(auth); + user = kMockUser; }); @@ -194,7 +201,7 @@ void main() { test('setCurrentUser()', () async { expect(auth.currentUser, isNull); - MockUserPlatform userPlatform = MockUserPlatform(auth, user); + MockUserPlatform userPlatform = MockUserPlatform(auth, multiFactor, user); auth.currentUser = userPlatform; expect(auth.currentUser, isA()); expect(auth.currentUser!.uid, equals(kMockUid)); @@ -1138,8 +1145,9 @@ void main() { } class MockUserPlatform extends UserPlatform { - MockUserPlatform(FirebaseAuthPlatform auth, Map user) - : super(auth, user); + MockUserPlatform(FirebaseAuthPlatform auth, MultiFactorPlatform multiFactor, + Map user) + : super(auth, multiFactor, user); } class TestMethodChannelFirebaseAuth extends MethodChannelFirebaseAuth { diff --git a/packages/firebase_auth/firebase_auth_platform_interface/test/pigeon/test_api.dart b/packages/firebase_auth/firebase_auth_platform_interface/test/pigeon/test_api.dart new file mode 100644 index 000000000000..563124e6a3b0 --- /dev/null +++ b/packages/firebase_auth/firebase_auth_platform_interface/test/pigeon/test_api.dart @@ -0,0 +1,215 @@ +// Autogenerated from Pigeon (v3.2.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import +// ignore_for_file: avoid_relative_lib_imports +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../lib/src/pigeon/messages.pigeon.dart'; + +class _TestMultiFactorUserHostApiCodec extends StandardMessageCodec { + const _TestMultiFactorUserHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is PigeonMultiFactorInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is PigeonMultiFactorSession) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is PigeonPhoneMultiFactorAssertion) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return PigeonMultiFactorInfo.decode(readValue(buffer)!); + + case 129: + return PigeonMultiFactorSession.decode(readValue(buffer)!); + + case 130: + return PigeonPhoneMultiFactorAssertion.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestMultiFactorUserHostApi { + static const MessageCodec codec = _TestMultiFactorUserHostApiCodec(); + + Future enrollPhone(String appName, + PigeonPhoneMultiFactorAssertion assertion, String? displayName); + Future getSession(String appName); + Future unenroll(String appName, String? factorUid); + Future> getEnrolledFactors(String appName); + static void setup(TestMultiFactorUserHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MultiFactorUserHostApi.enrollPhone', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.MultiFactorUserHostApi.enrollPhone was null.'); + final List args = (message as List?)!; + final String? arg_appName = (args[0] as String?); + assert(arg_appName != null, + 'Argument for dev.flutter.pigeon.MultiFactorUserHostApi.enrollPhone was null, expected non-null String.'); + final PigeonPhoneMultiFactorAssertion? arg_assertion = + (args[1] as PigeonPhoneMultiFactorAssertion?); + assert(arg_assertion != null, + 'Argument for dev.flutter.pigeon.MultiFactorUserHostApi.enrollPhone was null, expected non-null PigeonPhoneMultiFactorAssertion.'); + final String? arg_displayName = (args[2] as String?); + await api.enrollPhone(arg_appName!, arg_assertion!, arg_displayName); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MultiFactorUserHostApi.getSession', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.MultiFactorUserHostApi.getSession was null.'); + final List args = (message as List?)!; + final String? arg_appName = (args[0] as String?); + assert(arg_appName != null, + 'Argument for dev.flutter.pigeon.MultiFactorUserHostApi.getSession was null, expected non-null String.'); + final PigeonMultiFactorSession output = + await api.getSession(arg_appName!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MultiFactorUserHostApi.unenroll', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.MultiFactorUserHostApi.unenroll was null.'); + final List args = (message as List?)!; + final String? arg_appName = (args[0] as String?); + assert(arg_appName != null, + 'Argument for dev.flutter.pigeon.MultiFactorUserHostApi.unenroll was null, expected non-null String.'); + final String? arg_factorUid = (args[1] as String?); + await api.unenroll(arg_appName!, arg_factorUid); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MultiFactorUserHostApi.getEnrolledFactors', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.MultiFactorUserHostApi.getEnrolledFactors was null.'); + final List args = (message as List?)!; + final String? arg_appName = (args[0] as String?); + assert(arg_appName != null, + 'Argument for dev.flutter.pigeon.MultiFactorUserHostApi.getEnrolledFactors was null, expected non-null String.'); + final List output = + await api.getEnrolledFactors(arg_appName!); + return {'result': output}; + }); + } + } + } +} + +class _TestMultiFactoResolverHostApiCodec extends StandardMessageCodec { + const _TestMultiFactoResolverHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is PigeonMultiFactorInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is PigeonMultiFactorSession) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is PigeonPhoneMultiFactorAssertion) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return PigeonMultiFactorInfo.decode(readValue(buffer)!); + + case 129: + return PigeonMultiFactorSession.decode(readValue(buffer)!); + + case 130: + return PigeonPhoneMultiFactorAssertion.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestMultiFactoResolverHostApi { + static const MessageCodec codec = + _TestMultiFactoResolverHostApiCodec(); + + Future> resolveSignIn( + String resolverId, PigeonPhoneMultiFactorAssertion assertion); + static void setup(TestMultiFactoResolverHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MultiFactoResolverHostApi.resolveSignIn', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.MultiFactoResolverHostApi.resolveSignIn was null.'); + final List args = (message as List?)!; + final String? arg_resolverId = (args[0] as String?); + assert(arg_resolverId != null, + 'Argument for dev.flutter.pigeon.MultiFactoResolverHostApi.resolveSignIn was null, expected non-null String.'); + final PigeonPhoneMultiFactorAssertion? arg_assertion = + (args[1] as PigeonPhoneMultiFactorAssertion?); + assert(arg_assertion != null, + 'Argument for dev.flutter.pigeon.MultiFactoResolverHostApi.resolveSignIn was null, expected non-null PigeonPhoneMultiFactorAssertion.'); + final Map output = + await api.resolveSignIn(arg_resolverId!, arg_assertion!); + return {'result': output}; + }); + } + } + } +} diff --git a/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_recaptcha_verifier_factory_test.dart b/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_recaptcha_verifier_factory_test.dart index 9b740357947e..9dec1897780f 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_recaptcha_verifier_factory_test.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_recaptcha_verifier_factory_test.dart @@ -68,7 +68,9 @@ void main() { group('delegateFor()', () { test('throws UnimplementedError error', () async { try { - recaptchaVerifierFactoryPlatform.delegateFor(); + recaptchaVerifierFactoryPlatform.delegateFor( + auth: FirebaseAuthPlatform.instance, + ); } on UnimplementedError catch (e) { expect(e.message, equals('delegateFor() is not implemented')); return; diff --git a/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_user_credential_test.dart b/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_user_credential_test.dart index 9abf92630a34..59ff94d8b2e5 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_user_credential_test.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_user_credential_test.dart @@ -38,7 +38,8 @@ void main() { profile: {}, isNewUser: false, ); - kMockUser = TestUserPlatform(auth, kMockUserData); + kMockUser = + TestUserPlatform(auth, TestMultiFactorPlatform(auth), kMockUserData); kMockCredential = EmailAuthProvider.credential( email: kMockEmail, password: kMockPassword); @@ -95,8 +96,16 @@ void main() { } class TestUserPlatform extends UserPlatform { - TestUserPlatform(FirebaseAuthPlatform auth, Map data) - : super(auth, data); + TestUserPlatform(FirebaseAuthPlatform auth, + MultiFactorPlatform multiFactorPlatform, Map data) + : super(auth, multiFactorPlatform, data); +} + +class TestMultiFactorPlatform extends MultiFactorPlatform { + TestMultiFactorPlatform(FirebaseAuthPlatform auth) + : super( + auth, + ); } class TestUserCredentialPlatform extends UserCredentialPlatform { diff --git a/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_user_test.dart b/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_user_test.dart index 599ea96dc03f..a00db12c7b5a 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_user_test.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_user_test.dart @@ -8,6 +8,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter_test/flutter_test.dart'; import '../mock.dart'; +import 'platform_interface_user_credential_test.dart'; void main() { setupFirebaseAuthMocks(); @@ -58,7 +59,8 @@ void main() { 'refreshToken': kMockRefreshToken, 'tenantId': kMockTenantId, }; - userPlatform = TestUserPlatform(auth, kMockUser); + userPlatform = + TestUserPlatform(auth, TestMultiFactorPlatform(auth), kMockUser); }); group('Constructor', () { @@ -283,6 +285,7 @@ void main() { } class TestUserPlatform extends UserPlatform { - TestUserPlatform(FirebaseAuthPlatform auth, Map data) - : super(auth, data); + TestUserPlatform(FirebaseAuthPlatform auth, MultiFactorPlatform multiFactor, + Map data) + : super(auth, multiFactor, data); } diff --git a/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart b/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart index efff7903872f..006f676a0391 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart @@ -6,6 +6,8 @@ import 'dart:async'; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_web/src/firebase_auth_web_multi_factor.dart'; +import 'package:firebase_auth_web/src/interop/utils/utils.dart'; import 'package:firebase_auth_web/src/utils/web_utils.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core_web/firebase_core_web.dart'; @@ -18,6 +20,7 @@ import 'src/firebase_auth_web_recaptcha_verifier_factory.dart'; import 'src/firebase_auth_web_user.dart'; import 'src/firebase_auth_web_user_credential.dart'; import 'src/interop/auth.dart' as auth_interop; +import 'src/interop/multi_factor.dart' as multi_factor; /// The web delegate implementation for [FirebaseAuth]. class FirebaseAuthWeb extends FirebaseAuthPlatform { @@ -40,7 +43,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { StreamController.broadcast(); // TODO(rrousselGit): close StreamSubscription - _delegate.onAuthStateChanged.map((auth_interop.User? webUser) { + delegate.onAuthStateChanged.map((auth_interop.User? webUser) { if (!_initialized.isCompleted) { _initialized.complete(); } @@ -48,7 +51,8 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { if (webUser == null) { return null; } else { - return UserWeb(this, webUser); + return UserWeb(this, + MultiFactorWeb(this, multi_factor.multiFactor(webUser)), webUser); } }).listen((UserWeb? webUser) { _authStateChangesListeners[app.name]!.add(webUser); @@ -56,11 +60,12 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { // TODO(rrousselGit): close StreamSubscription // Also triggers `userChanged` events - _delegate.onIdTokenChanged.map((auth_interop.User? webUser) { + delegate.onIdTokenChanged.map((auth_interop.User? webUser) { if (webUser == null) { return null; } else { - return UserWeb(this, webUser); + return UserWeb(this, + MultiFactorWeb(this, multi_factor.multiFactor(webUser)), webUser); } }).listen((UserWeb? webUser) { _idTokenChangesListeners[app.name]!.add(webUser); @@ -72,6 +77,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { static void registerWith(Registrar registrar) { FirebaseCoreWeb.registerService('auth'); FirebaseAuthPlatform.instance = FirebaseAuthWeb.instance; + PhoneMultiFactorGeneratorPlatform.instance = PhoneMultiFactorGeneratorWeb(); RecaptchaVerifierFactoryPlatform.instance = RecaptchaVerifierFactoryWeb.instance; } @@ -93,7 +99,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { /// instance of Auth from the web plugin auth_interop.Auth? _webAuth; - auth_interop.Auth get _delegate { + auth_interop.Auth get delegate { return _webAuth ??= auth_interop.getAuthInstance(core_interop.app(app.name)); } @@ -114,23 +120,26 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override UserPlatform? get currentUser { - auth_interop.User? webCurrentUser = _delegate.currentUser; + auth_interop.User? webCurrentUser = delegate.currentUser; if (webCurrentUser == null) { return null; } - return UserWeb(this, _delegate.currentUser!); + return UserWeb( + this, + MultiFactorWeb(this, multi_factor.multiFactor(delegate.currentUser!)), + delegate.currentUser!); } @override String? get tenantId { - return _delegate.tenantId; + return delegate.tenantId; } @override set tenantId(String? tenantId) { - _delegate.tenantId = tenantId; + delegate.tenantId = tenantId; } @override @@ -143,7 +152,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future applyActionCode(String code) async { try { - await _delegate.applyActionCode(code); + await delegate.applyActionCode(code); } catch (e) { throw getFirebaseAuthException(e); } @@ -152,7 +161,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future checkActionCode(String code) async { try { - return convertWebActionCodeInfo(await _delegate.checkActionCode(code))!; + return convertWebActionCodeInfo(await delegate.checkActionCode(code))!; } catch (e) { throw getFirebaseAuthException(e); } @@ -161,7 +170,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future confirmPasswordReset(String code, String newPassword) async { try { - await _delegate.confirmPasswordReset(code, newPassword); + await delegate.confirmPasswordReset(code, newPassword); } catch (e) { throw getFirebaseAuthException(e); } @@ -173,7 +182,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { try { return UserCredentialWeb( this, - await _delegate.createUserWithEmailAndPassword(email, password), + await delegate.createUserWithEmailAndPassword(email, password), ); } catch (e) { throw getFirebaseAuthException(e); @@ -183,7 +192,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future> fetchSignInMethodsForEmail(String email) async { try { - return await _delegate.fetchSignInMethodsForEmail(email); + return await delegate.fetchSignInMethodsForEmail(email); } catch (e) { throw getFirebaseAuthException(e); } @@ -192,7 +201,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future getRedirectResult() async { try { - return UserCredentialWeb(this, await _delegate.getRedirectResult()); + return UserCredentialWeb(this, await delegate.getRedirectResult()); } catch (e) { throw getFirebaseAuthException(e); } @@ -225,7 +234,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { ActionCodeSettings? actionCodeSettings, ]) async { try { - await _delegate.sendPasswordResetEmail( + await delegate.sendPasswordResetEmail( email, convertPlatformActionCodeSettings(actionCodeSettings)); } catch (e) { throw getFirebaseAuthException(e); @@ -238,7 +247,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { ActionCodeSettings? actionCodeSettings, ]) async { try { - await _delegate.sendSignInLinkToEmail( + await delegate.sendSignInLinkToEmail( email, convertPlatformActionCodeSettings(actionCodeSettings)); } catch (e) { throw getFirebaseAuthException(e); @@ -247,12 +256,12 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override String get languageCode { - return _delegate.languageCode; + return delegate.languageCode; } @override Future setLanguageCode(String? languageCode) async { - _delegate.languageCode = languageCode; + delegate.languageCode = languageCode; } @override @@ -263,14 +272,14 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { String? smsCode, bool? forceRecaptchaFlow, }) async { - _delegate.settings.appVerificationDisabledForTesting = + delegate.settings.appVerificationDisabledForTesting = appVerificationDisabledForTesting; } @override Future setPersistence(Persistence persistence) async { try { - return _delegate.setPersistence(persistence); + return delegate.setPersistence(persistence); } catch (e) { throw getFirebaseAuthException(e); } @@ -279,7 +288,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future signInAnonymously() async { try { - return UserCredentialWeb(this, await _delegate.signInAnonymously()); + return UserCredentialWeb(this, await delegate.signInAnonymously()); } catch (e) { throw getFirebaseAuthException(e); } @@ -291,7 +300,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { try { return UserCredentialWeb( this, - await _delegate + await delegate .signInWithCredential(convertPlatformCredential(credential)!)); } catch (e) { throw getFirebaseAuthException(e); @@ -302,7 +311,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { Future signInWithCustomToken(String token) async { try { return UserCredentialWeb( - this, await _delegate.signInWithCustomToken(token)); + this, await delegate.signInWithCustomToken(token)); } catch (e) { throw getFirebaseAuthException(e); } @@ -313,9 +322,9 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { String email, String password) async { try { return UserCredentialWeb( - this, await _delegate.signInWithEmailAndPassword(email, password)); + this, await delegate.signInWithEmailAndPassword(email, password)); } catch (e) { - throw getFirebaseAuthException(e); + throw getFirebaseAuthException(e, _webAuth); } } @@ -324,7 +333,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { String email, String emailLink) async { try { return UserCredentialWeb( - this, await _delegate.signInWithEmailLink(email, emailLink)); + this, await delegate.signInWithEmailLink(email, emailLink)); } catch (e) { throw getFirebaseAuthException(e); } @@ -340,7 +349,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { auth_interop.RecaptchaVerifier verifier = applicationVerifier.delegate; return ConfirmationResultWeb( - this, await _delegate.signInWithPhoneNumber(phoneNumber, verifier)); + this, await delegate.signInWithPhoneNumber(phoneNumber, verifier)); } catch (e) { throw getFirebaseAuthException(e); } @@ -351,7 +360,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { try { return UserCredentialWeb( this, - await _delegate.signInWithPopup(convertPlatformAuthProvider(provider)), + await delegate.signInWithPopup(convertPlatformAuthProvider(provider)), ); } catch (e) { throw getFirebaseAuthException(e); @@ -361,8 +370,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future signInWithRedirect(AuthProvider provider) async { try { - return _delegate - .signInWithRedirect(convertPlatformAuthProvider(provider)); + return delegate.signInWithRedirect(convertPlatformAuthProvider(provider)); } catch (e) { throw getFirebaseAuthException(e); } @@ -371,7 +379,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future signOut() async { try { - await _delegate.signOut(); + await delegate.signOut(); } catch (e) { throw getFirebaseAuthException(e); } @@ -383,7 +391,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { // The generic platform interface is with host and port split to // centralize logic between android/ios native, but web takes the // origin as a single string - _delegate.useAuthEmulator('http://$host:$port'); + delegate.useAuthEmulator('http://$host:$port'); } catch (e) { throw getFirebaseAuthException(e); } @@ -392,7 +400,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future verifyPasswordResetCode(String code) async { try { - return await _delegate.verifyPasswordResetCode(code); + return await delegate.verifyPasswordResetCode(code); } catch (e) { throw getFirebaseAuthException(e); } @@ -400,7 +408,8 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future verifyPhoneNumber({ - required String phoneNumber, + String? phoneNumber, + PhoneMultiFactorInfo? multiFactorInfo, required PhoneVerificationCompleted verificationCompleted, required PhoneVerificationFailed verificationFailed, required PhoneCodeSent codeSent, @@ -408,8 +417,40 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { String? autoRetrievedSmsCodeForTesting, Duration timeout = const Duration(seconds: 30), int? forceResendingToken, - }) { - throw UnimplementedError( - 'verifyPhoneNumber() is not supported on the web. Please use `signInWithPhoneNumber` instead.'); + MultiFactorSession? multiFactorSession, + }) async { + try { + Map? data; + if (multiFactorSession != null) { + final _webMultiFactorSession = + multiFactorSession as MultiFactorSessionWeb; + if (multiFactorInfo != null) { + data = { + 'multiFactorUid': multiFactorInfo.uid, + 'session': _webMultiFactorSession.webSession.jsObject, + }; + } else { + data = { + 'phoneNumber': phoneNumber, + 'session': _webMultiFactorSession.webSession.jsObject, + }; + } + } + + final phoneOptions = (data ?? phoneNumber)!; + + final provider = auth_interop.PhoneAuthProvider(_webAuth); + final verifier = RecaptchaVerifierFactoryWeb( + auth: this, + ).delegate; + + /// We add the passthrough method for LegacyJsObject + final verificationId = await provider.verifyPhoneNumber( + jsify(phoneOptions, (object) => object), verifier); + + codeSent(verificationId, null); + } catch (e) { + verificationFailed(getFirebaseAuthException(e)); + } } } diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_confirmation_result.dart b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_confirmation_result.dart index 68ace46db4ce..80fbf1d6cd3e 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_confirmation_result.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_confirmation_result.dart @@ -5,9 +5,10 @@ import 'dart:async'; -import 'interop/auth.dart' as auth_interop; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:firebase_auth_web/src/firebase_auth_web_user_credential.dart'; + +import 'interop/auth.dart' as auth_interop; import 'utils/web_utils.dart'; /// The web delegate implementation for [ConfirmationResultPlatform]. diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_multi_factor.dart b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_multi_factor.dart new file mode 100644 index 000000000000..d4ec8846d8a0 --- /dev/null +++ b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_multi_factor.dart @@ -0,0 +1,144 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_web/firebase_auth_web.dart'; +import 'package:firebase_auth_web/src/firebase_auth_web_user_credential.dart'; + +import 'interop/auth.dart' as auth; +import 'interop/multi_factor.dart' as multi_factor_interop; +import 'utils/web_utils.dart'; + +/// Web delegate implementation of [UserPlatform]. +class MultiFactorWeb extends MultiFactorPlatform { + MultiFactorWeb(FirebaseAuthPlatform auth, this._webMultiFactorUser) + : super(auth); + + final multi_factor_interop.MultiFactorUser _webMultiFactorUser; + + @override + Future getSession() async { + try { + return convertMultiFactorSession(await _webMultiFactorUser.session); + } catch (e) { + throw getFirebaseAuthException(e); + } + } + + @override + Future enroll( + MultiFactorAssertionPlatform assertion, { + String? displayName, + }) async { + try { + final webAssertion = assertion as MultiFactorAssertionWeb; + return await _webMultiFactorUser.enroll( + webAssertion.assertion, + displayName, + ); + } catch (e) { + throw getFirebaseAuthException(e); + } + } + + @override + Future unenroll({ + String? factorUid, + MultiFactorInfo? multiFactorInfo, + }) { + final uidToUnenroll = factorUid ?? multiFactorInfo?.uid; + if (uidToUnenroll == null) { + throw ArgumentError( + 'Either factorUid or multiFactorInfo must not be null', + ); + } + + return _webMultiFactorUser.unenroll( + uidToUnenroll, + ); + } + + @override + Future> getEnrolledFactors() async { + final data = _webMultiFactorUser.enrolledFactors; + return data + .map((e) => MultiFactorInfo( + factorId: e.factorId, + enrollmentTimestamp: + DateTime.parse(e.enrollmentTime).millisecondsSinceEpoch / + 1000, + displayName: e.displayName, + uid: e.uid, + )) + .toList(); + } +} + +class MultiFactorAssertionWeb extends MultiFactorAssertionPlatform { + MultiFactorAssertionWeb( + this.assertion, + ) : super(); + + final multi_factor_interop.MultiFactorAssertion assertion; +} + +class MultiFactorResolverWeb extends MultiFactorResolverPlatform { + MultiFactorResolverWeb( + List hints, + MultiFactorSession session, + this._auth, + this._webMultiFactorResolver, + ) : super(hints, session); + + final multi_factor_interop.MultiFactorResolver _webMultiFactorResolver; + final FirebaseAuthWeb _auth; + + @override + Future resolveSignIn( + MultiFactorAssertionPlatform assertion, + ) async { + final webAssertion = assertion as MultiFactorAssertionWeb; + + return UserCredentialWeb( + _auth, + await _webMultiFactorResolver.resolveSignIn(webAssertion.assertion), + ); + } +} + +class MultiFactorSessionWeb extends MultiFactorSession { + MultiFactorSessionWeb( + String id, + this.webSession, + ) : super(id); + + final multi_factor_interop.MultiFactorSession webSession; +} + +/// Helper class used to generate PhoneMultiFactorAssertions. +class PhoneMultiFactorGeneratorWeb extends PhoneMultiFactorGeneratorPlatform { + /// Transforms a PhoneAuthCredential into a [MultiFactorAssertion] + /// which can be used to confirm ownership of a phone second factor. + @override + MultiFactorAssertionPlatform getAssertion( + PhoneAuthCredential credential, + ) { + final verificationId = credential.verificationId; + final verificationCode = credential.smsCode; + + if (verificationCode == null) { + throw ArgumentError('verificationCode must not be null'); + } + if (verificationId == null) { + throw ArgumentError('verificationId must not be null'); + } + + final cred = + auth.PhoneAuthProvider.credential(verificationId, verificationCode); + + return MultiFactorAssertionWeb( + multi_factor_interop.PhoneMultiFactorGenerator.assertion(cred)); + } +} diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_recaptcha_verifier_factory.dart b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_recaptcha_verifier_factory.dart index 2dca05bcc5d5..73a475a7bd55 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_recaptcha_verifier_factory.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_recaptcha_verifier_factory.dart @@ -6,8 +6,10 @@ import 'dart:async'; import 'dart:html'; -import 'interop/auth.dart' as auth_interop; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_web/firebase_auth_web.dart'; + +import 'interop/auth.dart' as auth_interop; import 'utils/web_utils.dart'; const String _kInvisibleElementId = '__ff-recaptcha-container'; @@ -32,6 +34,7 @@ class RecaptchaVerifierFactoryWeb extends RecaptchaVerifierFactoryPlatform { /// Creates a new [RecaptchaVerifierFactoryWeb] with a container and parameters. RecaptchaVerifierFactoryWeb({ + required FirebaseAuthWeb auth, String? container, RecaptchaVerifierSize size = RecaptchaVerifierSize.normal, RecaptchaVerifierTheme theme = RecaptchaVerifierTheme.light, @@ -86,11 +89,16 @@ class RecaptchaVerifierFactoryWeb extends RecaptchaVerifierFactoryPlatform { element = container; } - _delegate = auth_interop.RecaptchaVerifier(element, parameters); + _delegate = auth_interop.RecaptchaVerifier( + element, + parameters, + auth.delegate, + ); } @override RecaptchaVerifierFactoryPlatform delegateFor({ + required FirebaseAuthPlatform auth, String? container, RecaptchaVerifierSize size = RecaptchaVerifierSize.normal, RecaptchaVerifierTheme theme = RecaptchaVerifierTheme.light, @@ -98,13 +106,16 @@ class RecaptchaVerifierFactoryWeb extends RecaptchaVerifierFactoryPlatform { RecaptchaVerifierOnError? onError, RecaptchaVerifierOnExpired? onExpired, }) { + final _webAuth = auth as FirebaseAuthWeb; return RecaptchaVerifierFactoryWeb( - container: container, - size: size, - theme: theme, - onSuccess: onSuccess, - onError: onError, - onExpired: onExpired); + auth: _webAuth, + container: container, + size: size, + theme: theme, + onSuccess: onSuccess, + onError: onError, + onExpired: onExpired, + ); } @override diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user.dart b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user.dart index 17a4cb3f4694..8ff789c0820b 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user.dart @@ -19,8 +19,9 @@ final DateFormat _dateFormat = DateFormat('EEE, d MMM yyyy HH:mm:ss', 'en_US'); /// Web delegate implementation of [UserPlatform]. class UserWeb extends UserPlatform { /// Creates a new [UserWeb] instance. - UserWeb(FirebaseAuthPlatform auth, this._webUser) - : super(auth, { + UserWeb( + FirebaseAuthPlatform auth, MultiFactorPlatform multiFactor, this._webUser) + : super(auth, multiFactor, { 'displayName': _webUser.displayName, 'email': _webUser.email, 'emailVerified': _webUser.emailVerified, @@ -171,7 +172,7 @@ class UserWeb extends UserPlatform { _assertIsSignedOut(auth); try { - return UserWeb(auth, await _webUser.unlink(providerId)); + return UserWeb(auth, multiFactor, await _webUser.unlink(providerId)); } catch (e) { throw getFirebaseAuthException(e); } diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user_credential.dart b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user_credential.dart index f96a360fa847..2c88b54c791b 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user_credential.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user_credential.dart @@ -3,10 +3,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'interop/auth.dart' as auth_interop; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_web/src/firebase_auth_web_multi_factor.dart'; import 'firebase_auth_web_user.dart'; +import 'interop/auth.dart' as auth_interop; +import 'interop/multi_factor.dart'; import 'utils/web_utils.dart'; /// Web delegate implementation of [UserCredentialPlatform]. @@ -21,6 +23,9 @@ class UserCredentialWeb extends UserCredentialPlatform { webUserCredential.additionalUserInfo, ), credential: convertWebOAuthCredential(webUserCredential.credential), - user: UserWeb(auth, webUserCredential.user!), + user: UserWeb( + auth, + MultiFactorWeb(auth, multiFactor(webUserCredential.user!)), + webUserCredential.user!), ); } diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart index 7b61a8d8049e..229e7748b290 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart @@ -9,10 +9,11 @@ import 'dart:async'; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; -import 'package:http_parser/http_parser.dart'; -import 'package:js/js.dart'; import 'package:firebase_core_web/firebase_core_web_interop.dart' hide jsify, dartify; +import 'package:http_parser/http_parser.dart'; +import 'package:js/js.dart'; + import 'auth_interop.dart' as auth_interop; import 'utils/utils.dart'; @@ -908,7 +909,7 @@ class PhoneAuthProvider /// Creates a new PhoneAuthProvider with the optional [Auth] instance /// in which sign-ins should occur. factory PhoneAuthProvider([Auth? auth]) => - PhoneAuthProvider.fromJsObject((auth != null) + PhoneAuthProvider.fromJsObject(auth != null ? auth_interop.PhoneAuthProviderJsImpl(auth.jsObject) : auth_interop.PhoneAuthProviderJsImpl()); @@ -923,17 +924,17 @@ class PhoneAuthProvider /// /// For abuse prevention, this method also requires an [ApplicationVerifier]. Future verifyPhoneNumber( - String phoneNumber, ApplicationVerifier applicationVerifier) => + dynamic phoneOptions, ApplicationVerifier applicationVerifier) => handleThenable(jsObject.verifyPhoneNumber( - phoneNumber, applicationVerifier.jsObject)); + phoneOptions, applicationVerifier.jsObject)); /// Creates a phone auth credential given the verification ID /// from [verifyPhoneNumber] and the [verificationCode] that was sent to the /// user's mobile device. - static auth_interop.OAuthCredential credential( + static auth_interop.PhoneAuthCredentialJsImpl credential( String verificationId, String verificationCode) => auth_interop.PhoneAuthProviderJsImpl.credential( - verificationId, verificationCode) as auth_interop.OAuthCredential; + verificationId, verificationCode); } /// A verifier for domain verification and abuse prevention. @@ -987,18 +988,16 @@ class RecaptchaVerifier /// print('Response expired'); /// } /// }); - factory RecaptchaVerifier(container, - [Map? parameters, App? app]) => - (parameters != null) - ? ((app != null) - ? RecaptchaVerifier.fromJsObject( - auth_interop.RecaptchaVerifierJsImpl( - container, jsify(parameters), app.jsObject)) - : RecaptchaVerifier.fromJsObject( - auth_interop.RecaptchaVerifierJsImpl( - container, jsify(parameters)))) - : RecaptchaVerifier.fromJsObject( - auth_interop.RecaptchaVerifierJsImpl(container)); + factory RecaptchaVerifier( + container, Map parameters, Auth auth) { + return RecaptchaVerifier.fromJsObject( + auth_interop.RecaptchaVerifierJsImpl( + container, + jsify(parameters), + auth.jsObject, + ), + ); + } /// Creates a new RecaptchaVerifier from a [jsObject]. RecaptchaVerifier.fromJsObject(auth_interop.RecaptchaVerifierJsImpl jsObject) diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart index 36e618ca2276..6f9ad2479dea 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart @@ -9,8 +9,9 @@ @JS('firebase_auth') library firebase_interop.auth; -import 'package:js/js.dart'; +import 'package:firebase_auth_web/src/interop/auth.dart'; import 'package:firebase_core_web/firebase_core_web_interop.dart'; +import 'package:js/js.dart'; @JS() external AuthJsImpl getAuth([AppJsImpl? app]); @@ -233,6 +234,19 @@ external PromiseJsImpl updateProfile( UserProfile profile, ); +/// https://firebase.google.com/docs/reference/js/auth.md#multifactor +@JS() +external MultiFactorUserJsImpl multiFactor( + UserJsImpl user, +); + +/// https://firebase.google.com/docs/reference/js/auth.md#multifactor +@JS() +external MultiFactorResolverJsImpl getMultiFactorResolver( + AuthJsImpl auth, + MultiFactorError error, +); + @JS('Auth') abstract class AuthJsImpl { external AppJsImpl get app; @@ -443,10 +457,10 @@ class PhoneAuthProviderJsImpl extends AuthProviderJsImpl { external factory PhoneAuthProviderJsImpl([AuthJsImpl? auth]); external static String get PROVIDER_ID; external PromiseJsImpl verifyPhoneNumber( - String phoneNumber, + dynamic /* PhoneInfoOptions | string */ phoneOptions, ApplicationVerifierJsImpl applicationVerifier, ); - external static AuthCredential credential( + external static PhoneAuthCredentialJsImpl credential( String verificationId, String verificationCode, ); @@ -461,10 +475,10 @@ abstract class ApplicationVerifierJsImpl { @JS('RecaptchaVerifier') class RecaptchaVerifierJsImpl extends ApplicationVerifierJsImpl { external factory RecaptchaVerifierJsImpl( - container, [ - Object? parameters, - AppJsImpl? app, - ]); + containerOrId, + Object parameters, + AuthJsImpl authExtern, + ); external void clear(); external PromiseJsImpl render(); } @@ -621,7 +635,7 @@ class AndroidSettings { }); } -/// https://firebase.google.com/docs/reference/js/firebase.auth#.UserCredential +/// https://firebase.google.com/docs/reference/js/auth.usercredential @JS() @anonymous class UserCredentialJsImpl { @@ -649,3 +663,91 @@ class AuthSettings { external set appVerificationDisabledForTesting(bool? b); // external factory AuthSettings({bool appVerificationDisabledForTesting}); } + +/// https://firebase.google.com/docs/reference/js/auth.multifactoruser.md#multifactoruser_interface +@JS() +@anonymous +class MultiFactorUserJsImpl { + external List get enrolledFactors; + external PromiseJsImpl enroll( + MultiFactorAssertionJsImpl assertion, String? displayName); + external PromiseJsImpl getSession(); + external PromiseJsImpl unenroll( + dynamic /* MultiFactorInfo | string */ option); +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorinfo +@JS() +@anonymous +class MultiFactorInfoJsImpl { + external String? get displayName; + external String get enrollmentTime; + external String get factorId; + external String get uid; +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorassertion +@JS() +@anonymous +class MultiFactorAssertionJsImpl { + external String get factorId; +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorerror +@JS('Error') +@anonymous +class MultiFactorError extends AuthError { + external dynamic get customData; +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorresolver +@JS() +@anonymous +class MultiFactorResolverJsImpl { + external List get hints; + external MultiFactorSessionJsImpl get session; + external PromiseJsImpl resolveSignIn( + MultiFactorAssertionJsImpl assertion); +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorresolver +@JS() +@anonymous +class MultiFactorSessionJsImpl {} + +/// https://firebase.google.com/docs/reference/js/auth.phonemultifactorinfo +@JS() +@anonymous +class PhoneMultiFactorInfoJsImpl extends MultiFactorInfoJsImpl { + external String get phoneNumber; +} + +/// https://firebase.google.com/docs/reference/js/auth.phonemultifactorenrollinfooptions +@JS() +@anonymous +class PhoneMultiFactorEnrollInfoOptionsJsImpl { + external String get phoneNumber; + external MultiFactorSessionJsImpl? get session; +} + +/// https://firebase.google.com/docs/reference/js/auth.phonemultifactorgenerator +@JS('PhoneMultiFactorGenerator') +class PhoneMultiFactorGeneratorJsImpl { + external static String get FACTOR_ID; + external static PhoneMultiFactorAssertionJsImpl? assertion( + PhoneAuthCredentialJsImpl credential); +} + +/// https://firebase.google.com/docs/reference/js/auth.phonemultifactorassertion +@JS() +@anonymous +class PhoneMultiFactorAssertionJsImpl extends MultiFactorAssertionJsImpl {} + +/// https://firebase.google.com/docs/reference/js/auth.phoneauthcredential +@JS() +@anonymous +class PhoneAuthCredentialJsImpl extends AuthCredential { + external static PhoneAuthCredentialJsImpl fromJSON( + dynamic /*object | string*/ json); + external Object toJSON(); +} diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/interop/multi_factor.dart b/packages/firebase_auth/firebase_auth_web/lib/src/interop/multi_factor.dart new file mode 100644 index 000000000000..888108b39b21 --- /dev/null +++ b/packages/firebase_auth/firebase_auth_web/lib/src/interop/multi_factor.dart @@ -0,0 +1,163 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: public_member_api_docs + +import 'package:firebase_core_web/firebase_core_web_interop.dart' + hide jsify, dartify; + +import 'auth.dart' as auth; +import 'auth_interop.dart' as auth_interop; + +/// Given an AppJSImp, return the Auth instance. +MultiFactorUser multiFactor(auth.User user) { + return MultiFactorUser.getInstance(auth_interop.multiFactor(user.jsObject)); +} + +/// Given an AppJSImp, return the Auth instance. +MultiFactorResolver getMultiFactorResolver( + auth.Auth auth, auth_interop.MultiFactorError error) { + return MultiFactorResolver.fromJsObject( + auth_interop.getMultiFactorResolver(auth.jsObject, error)); +} + +/// The Firebase MultiFactorUser service class. +/// +/// See: https://firebase.google.com/docs/reference/js/auth.md#multifactor. +class MultiFactorUser + extends JsObjectWrapper { + static final _expando = Expando(); + + /// Creates a new Auth from a [jsObject]. + static MultiFactorUser getInstance( + auth_interop.MultiFactorUserJsImpl jsObject) { + return _expando[jsObject] ??= MultiFactorUser._fromJsObject(jsObject); + } + + MultiFactorUser._fromJsObject(auth_interop.MultiFactorUserJsImpl jsObject) + : super.fromJsObject(jsObject); + + /// Returns a list of the user's enrolled second factors. + List get enrolledFactors => + jsObject.enrolledFactors.map(MultiFactorInfo.fromJsObject).toList(); + + /// Returns the session identifier for a second factor enrollment operation. + /// + /// This is used to identify the user trying to enroll a second factor. + Future get session => + handleThenable(jsObject.getSession()) + .then(MultiFactorSession.fromJsObject); + + /// Enrolls a second factor as identified by the [MultiFactorAssertion] for the user. + /// + /// On resolution, the user tokens are updated to reflect the change in the JWT payload. + /// Accepts an additional display name parameter used to identify the second factor to the end user. + /// Recent re-authentication is required for this operation to succeed. On successful enrollment, + /// existing Firebase sessions (refresh tokens) are revoked. When a new factor is enrolled, + /// an email notification is sent to the user’s email. + Future enroll(MultiFactorAssertion assertion, String? displayName) { + return handleThenable(jsObject.enroll(assertion.jsObject, displayName)); + } + + /// Unenrolls the specified second factor. + /// + /// To specify the factor to remove, pass a [MultiFactorInfo] object + /// (retrieved from [MultiFactorUser.enrolledFactors]) or the factor's UID string. + /// Sessions are not revoked when the account is unenrolled. + /// An email notification is likely to be sent to the user notifying them of the change. + /// Recent re-authentication is required for this operation to succeed. + /// When an existing factor is unenrolled, an email notification is sent to the user’s email. + Future unenroll(String multiFactorInfoId) { + return handleThenable(jsObject.unenroll(multiFactorInfoId)); + } +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorinfo +class MultiFactorInfo + extends JsObjectWrapper { + MultiFactorInfo.fromJsObject(T jsObject) : super.fromJsObject(jsObject); + + /// The user friendly name of the current second factor. + String? get displayName => jsObject.displayName; + + /// The enrollment date of the second factor formatted as a UTC string. + String get enrollmentTime => jsObject.enrollmentTime; + + /// The identifier of the second factor. + String get factorId => jsObject.factorId; + + /// The multi-factor enrollment ID. + String get uid => jsObject.uid; +} + +class PhoneMultiFactorInfo + extends MultiFactorInfo { + PhoneMultiFactorInfo.fromJsObject( + auth_interop.PhoneMultiFactorInfoJsImpl jsObject) + : super.fromJsObject(jsObject); + + /// The user friendly name of the current second factor. + String get phoneNumber => jsObject.phoneNumber; +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorsession.md#multifactorsession_interface +class MultiFactorSession + extends JsObjectWrapper { + MultiFactorSession.fromJsObject(auth.MultiFactorSessionJsImpl jsObject) + : super.fromJsObject(jsObject); +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorsession.md#multifactorsession_interface +class MultiFactorAssertion + extends JsObjectWrapper { + MultiFactorAssertion.fromJsObject(T jsObject) : super.fromJsObject(jsObject); + + String get factorId => jsObject.factorId; +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorsession.md#multifactorsession_interface +class PhoneMultiFactorAssertion + extends MultiFactorAssertion { + PhoneMultiFactorAssertion.fromJsObject( + auth.PhoneMultiFactorAssertionJsImpl jsObject) + : super.fromJsObject(jsObject); +} + +/// https://firebase.google.com/docs/reference/js/auth#getmultifactorresolver +class MultiFactorResolver + extends JsObjectWrapper { + MultiFactorResolver.fromJsObject(auth.MultiFactorResolverJsImpl jsObject) + : super.fromJsObject(jsObject); + + List get hints => jsObject.hints.map((e) { + if (e is auth_interop.PhoneMultiFactorInfoJsImpl) { + return PhoneMultiFactorInfo.fromJsObject(e); + } else { + return MultiFactorInfo.fromJsObject(e); + } + }).toList(); + MultiFactorSession get session => + MultiFactorSession.fromJsObject(jsObject.session); + + Future resolveSignIn(MultiFactorAssertion assertion) { + return handleThenable(jsObject.resolveSignIn(assertion.jsObject)) + .then(auth.UserCredential.fromJsObject); + } +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorsession.md#multifactorsession_interface +class PhoneMultiFactorGenerator + extends JsObjectWrapper { + PhoneMultiFactorGenerator.fromJsObject( + auth.PhoneMultiFactorGeneratorJsImpl jsObject) + : super.fromJsObject(jsObject); + + static PhoneMultiFactorAssertion assertion( + auth.PhoneAuthCredentialJsImpl credential) { + return PhoneMultiFactorAssertion.fromJsObject( + auth_interop.PhoneMultiFactorGeneratorJsImpl.assertion(credential)!); + } +} diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/interop/utils/utils.dart b/packages/firebase_auth/firebase_auth_web/lib/src/interop/utils/utils.dart index e23176107c13..4996b5e59449 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/interop/utils/utils.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/interop/utils/utils.dart @@ -12,6 +12,9 @@ dynamic dartify(Object jsObject) { } /// Returns the JS implementation from Dart Object. -dynamic jsify(Object dartObject) { - return core_interop.jsify(dartObject); +dynamic jsify( + Object dartObject, [ + Object? Function(Object? object)? customJsify, +]) { + return core_interop.jsify(dartObject, customJsify); } diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart b/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart index 25f2d94351c0..55b07c67112a 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart @@ -3,17 +3,25 @@ // 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 'dart:io'; + import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_web/firebase_auth_web.dart'; +import 'package:firebase_auth_web/src/firebase_auth_web_multi_factor.dart'; import 'package:firebase_core_web/firebase_core_web_interop.dart' as core_interop; import '../interop/auth.dart' as auth_interop; +import '../interop/multi_factor.dart' as multi_factor_interop; /// Given a web error, an [Exception] is returned. /// /// The firebase-dart wrapper exposes a [core_interop.FirebaseError], allowing us to /// use the code and message and convert it into an expected [FirebaseAuthException]. -FirebaseAuthException getFirebaseAuthException(Object exception) { +FirebaseAuthException getFirebaseAuthException( + Object exception, [ + auth_interop.Auth? auth, +]) { if (exception is! core_interop.FirebaseError) { return FirebaseAuthException( code: 'unknown', @@ -28,6 +36,54 @@ FirebaseAuthException getFirebaseAuthException(Object exception) { .replaceFirst(' (${firebaseError.code}).', '') .replaceFirst('Firebase: ', ''); + if (code == 'multi-factor-auth-required') { + final _auth = auth; + if (_auth == null) { + throw ArgumentError( + 'Multi-factor authentication is required, but the auth instance is null. ' + 'Please ensure that the auth instance is not null before calling ' + '`getFirebaseAuthException()`.', + ); + } + final resolverWeb = multi_factor_interop.getMultiFactorResolver( + _auth, + firebaseError as auth_interop.MultiFactorError, + ); + + return FirebaseAuthMultiFactorException( + code: code, + message: message, + email: firebaseError.email, + phoneNumber: firebaseError.phoneNumber, + tenantId: firebaseError.tenantId, + resolver: MultiFactorResolverWeb( + resolverWeb.hints.map((e) { + if (e is multi_factor_interop.PhoneMultiFactorInfo) { + return PhoneMultiFactorInfo( + displayName: e.displayName, + factorId: e.factorId, + enrollmentTimestamp: + HttpDate.parse(e.enrollmentTime).millisecondsSinceEpoch / + 1000, + uid: e.uid, + phoneNumber: e.phoneNumber, + ); + } + return MultiFactorInfo( + displayName: e.displayName, + factorId: e.factorId, + enrollmentTimestamp: + HttpDate.parse(e.enrollmentTime).millisecondsSinceEpoch / 1000, + uid: e.uid, + ); + }).toList(), + MultiFactorSessionWeb('web', resolverWeb.session), + FirebaseAuthWeb.instance, + resolverWeb, + ), + ); + } + return FirebaseAuthException( code: code, message: message, @@ -252,7 +308,7 @@ auth_interop.OAuthCredential? convertPlatformCredential( return auth_interop.PhoneAuthProvider.credential( credential.verificationId!, credential.smsCode!, - ); + ) as auth_interop.OAuthCredential; } if (credential is OAuthCredential) { @@ -290,3 +346,9 @@ String convertRecaptchaVerifierTheme(RecaptchaVerifierTheme theme) { return 'light'; } } + +/// Converts a [multi_factor_interop.MultiFactorSession] into a [MultiFactorSession]. +MultiFactorSession convertMultiFactorSession( + multi_factor_interop.MultiFactorSession multiFactorSession) { + return MultiFactorSessionWeb('web', multiFactorSession); +} diff --git a/packages/firebase_core/analysis_options.yaml b/packages/firebase_core/analysis_options.yaml index 98ea76545605..9afc3598d90a 100644 --- a/packages/firebase_core/analysis_options.yaml +++ b/packages/firebase_core/analysis_options.yaml @@ -7,7 +7,8 @@ include: ../../analysis_options.yaml analyzer: # TODO(Lyokone): not working if added on the root analysis file exclude: - - firebase_core_platform_interface/lib/src/messages.pigeon.dart + - firebase_core_platform_interface/lib/src/pigeon/messages.pigeon.dart + - firebase_core_platform_interface/lib/src/pigeon/test_api.dart linter: rules: diff --git a/packages/firebase_core/firebase_core/android/src/main/java/io/flutter/plugins/firebase/core/GeneratedAndroidFirebaseCore.java b/packages/firebase_core/firebase_core/android/src/main/java/io/flutter/plugins/firebase/core/GeneratedAndroidFirebaseCore.java index e77c2e3de68a..09edfc8a2d14 100644 --- a/packages/firebase_core/firebase_core/android/src/main/java/io/flutter/plugins/firebase/core/GeneratedAndroidFirebaseCore.java +++ b/packages/firebase_core/firebase_core/android/src/main/java/io/flutter/plugins/firebase/core/GeneratedAndroidFirebaseCore.java @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// Autogenerated from Pigeon (v3.2.0), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.firebase.core; diff --git a/packages/firebase_core/firebase_core/ios/Classes/messages.g.h b/packages/firebase_core/firebase_core/ios/Classes/messages.g.h index 0025f149d34f..70cdb4f374a9 100644 --- a/packages/firebase_core/firebase_core/ios/Classes/messages.g.h +++ b/packages/firebase_core/firebase_core/ios/Classes/messages.g.h @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// Autogenerated from Pigeon (v3.2.0), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @protocol FlutterBinaryMessenger; diff --git a/packages/firebase_core/firebase_core/ios/Classes/messages.g.m b/packages/firebase_core/firebase_core/ios/Classes/messages.g.m index 6919bd5dd91f..1c22807964d1 100644 --- a/packages/firebase_core/firebase_core/ios/Classes/messages.g.m +++ b/packages/firebase_core/firebase_core/ios/Classes/messages.g.m @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// Autogenerated from Pigeon (v3.2.0), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.g.h" #if TARGET_OS_OSX @@ -319,17 +319,15 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void FirebaseAppHostApiSetup(id binaryMessenger, NSObject *api) { { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.FirebaseAppHostApi." - @"setAutomaticDataCollectionEnabled" - binaryMessenger:binaryMessenger - codec:FirebaseAppHostApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticDataCollectionEnabled" + binaryMessenger:binaryMessenger + codec:FirebaseAppHostApiGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector (setAutomaticDataCollectionEnabledAppName:enabled:completion:)], @"FirebaseAppHostApi api (%@) doesn't respond to " - @"@selector(setAutomaticDataCollectionEnabledAppName:enabled:" - @"completion:)", + @"@selector(setAutomaticDataCollectionEnabledAppName:enabled:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; @@ -346,17 +344,16 @@ void FirebaseAppHostApiSetup(id binaryMessenger, } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.FirebaseAppHostApi." - @"setAutomaticResourceManagementEnabled" - binaryMessenger:binaryMessenger - codec:FirebaseAppHostApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticResourceManagementEnabled" + binaryMessenger:binaryMessenger + codec:FirebaseAppHostApiGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector (setAutomaticResourceManagementEnabledAppName:enabled:completion:)], @"FirebaseAppHostApi api (%@) doesn't respond to " - @"@selector(setAutomaticResourceManagementEnabledAppName:enabled:" - @"completion:)", + @"@selector(setAutomaticResourceManagementEnabledAppName:enabled:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; @@ -378,10 +375,10 @@ void FirebaseAppHostApiSetup(id binaryMessenger, binaryMessenger:binaryMessenger codec:FirebaseAppHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(deleteAppName:completion:)], - @"FirebaseAppHostApi api (%@) doesn't respond to " - @"@selector(deleteAppName:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector(deleteAppName:completion:)], + @"FirebaseAppHostApi api (%@) doesn't respond to @selector(deleteAppName:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSString *arg_appName = GetNullableObjectAtIndex(args, 0); diff --git a/packages/firebase_core/firebase_core_platform_interface/lib/src/pigeon/messages.pigeon.dart b/packages/firebase_core/firebase_core_platform_interface/lib/src/pigeon/messages.pigeon.dart index 4194cd85fbbd..26d47bccc725 100644 --- a/packages/firebase_core/firebase_core_platform_interface/lib/src/pigeon/messages.pigeon.dart +++ b/packages/firebase_core/firebase_core_platform_interface/lib/src/pigeon/messages.pigeon.dart @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// Autogenerated from Pigeon (v3.2.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name // @dart = 2.12 @@ -158,15 +158,11 @@ class FirebaseCoreHostApi { static const MessageCodec codec = _FirebaseCoreHostApiCodec(); - Future initializeApp( - String arg_appName, - PigeonFirebaseOptions arg_initializeAppRequest, - ) async { + Future initializeApp(String arg_appName, + PigeonFirebaseOptions arg_initializeAppRequest) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseCoreHostApi.initializeApp', - codec, - binaryMessenger: _binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseCoreHostApi.initializeApp', codec, + binaryMessenger: _binaryMessenger); final Map? replyMap = await channel.send([arg_appName, arg_initializeAppRequest]) as Map?; @@ -195,10 +191,8 @@ class FirebaseCoreHostApi { Future> initializeCore() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseCoreHostApi.initializeCore', - codec, - binaryMessenger: _binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseCoreHostApi.initializeCore', codec, + binaryMessenger: _binaryMessenger); final Map? replyMap = await channel.send(null) as Map?; if (replyMap == null) { @@ -227,10 +221,8 @@ class FirebaseCoreHostApi { Future optionsFromResource() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseCoreHostApi.optionsFromResource', - codec, - binaryMessenger: _binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseCoreHostApi.optionsFromResource', codec, + binaryMessenger: _binaryMessenger); final Map? replyMap = await channel.send(null) as Map?; if (replyMap == null) { @@ -273,14 +265,11 @@ class FirebaseAppHostApi { static const MessageCodec codec = _FirebaseAppHostApiCodec(); Future setAutomaticDataCollectionEnabled( - String arg_appName, - bool arg_enabled, - ) async { + String arg_appName, bool arg_enabled) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticDataCollectionEnabled', - codec, - binaryMessenger: _binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticDataCollectionEnabled', + codec, + binaryMessenger: _binaryMessenger); final Map? replyMap = await channel .send([arg_appName, arg_enabled]) as Map?; if (replyMap == null) { @@ -302,14 +291,11 @@ class FirebaseAppHostApi { } Future setAutomaticResourceManagementEnabled( - String arg_appName, - bool arg_enabled, - ) async { + String arg_appName, bool arg_enabled) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticResourceManagementEnabled', - codec, - binaryMessenger: _binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticResourceManagementEnabled', + codec, + binaryMessenger: _binaryMessenger); final Map? replyMap = await channel .send([arg_appName, arg_enabled]) as Map?; if (replyMap == null) { @@ -332,10 +318,8 @@ class FirebaseAppHostApi { Future delete(String arg_appName) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseAppHostApi.delete', - codec, - binaryMessenger: _binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseAppHostApi.delete', codec, + binaryMessenger: _binaryMessenger); final Map? replyMap = await channel.send([arg_appName]) as Map?; if (replyMap == null) { diff --git a/packages/firebase_core/firebase_core_platform_interface/lib/src/pigeon/test_api.dart b/packages/firebase_core/firebase_core_platform_interface/lib/src/pigeon/test_api.dart index fd9515996e4f..c3e6c521e543 100644 --- a/packages/firebase_core/firebase_core_platform_interface/lib/src/pigeon/test_api.dart +++ b/packages/firebase_core/firebase_core_platform_interface/lib/src/pigeon/test_api.dart @@ -1,10 +1,10 @@ -// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// Autogenerated from Pigeon (v3.2.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis // ignore_for_file: avoid_relative_lib_imports // @dart = 2.12 +import 'dart:async'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; - import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -45,41 +45,29 @@ abstract class TestFirebaseCoreHostApi { static const MessageCodec codec = _TestFirebaseCoreHostApiCodec(); Future initializeApp( - String appName, - PigeonFirebaseOptions initializeAppRequest, - ); + String appName, PigeonFirebaseOptions initializeAppRequest); Future> initializeCore(); Future optionsFromResource(); - static void setup( - TestFirebaseCoreHostApi? api, { - BinaryMessenger? binaryMessenger, - }) { + static void setup(TestFirebaseCoreHostApi? api, + {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseCoreHostApi.initializeApp', - codec, - binaryMessenger: binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseCoreHostApi.initializeApp', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert( - message != null, - 'Argument for dev.flutter.pigeon.FirebaseCoreHostApi.initializeApp was null.', - ); + assert(message != null, + 'Argument for dev.flutter.pigeon.FirebaseCoreHostApi.initializeApp was null.'); final List args = (message as List?)!; final String? arg_appName = (args[0] as String?); - assert( - arg_appName != null, - 'Argument for dev.flutter.pigeon.FirebaseCoreHostApi.initializeApp was null, expected non-null String.', - ); + assert(arg_appName != null, + 'Argument for dev.flutter.pigeon.FirebaseCoreHostApi.initializeApp was null, expected non-null String.'); final PigeonFirebaseOptions? arg_initializeAppRequest = (args[1] as PigeonFirebaseOptions?); - assert( - arg_initializeAppRequest != null, - 'Argument for dev.flutter.pigeon.FirebaseCoreHostApi.initializeApp was null, expected non-null PigeonFirebaseOptions.', - ); + assert(arg_initializeAppRequest != null, + 'Argument for dev.flutter.pigeon.FirebaseCoreHostApi.initializeApp was null, expected non-null PigeonFirebaseOptions.'); final PigeonInitializeResponse output = await api.initializeApp(arg_appName!, arg_initializeAppRequest!); return {'result': output}; @@ -88,10 +76,8 @@ abstract class TestFirebaseCoreHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseCoreHostApi.initializeCore', - codec, - binaryMessenger: binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseCoreHostApi.initializeCore', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -105,10 +91,8 @@ abstract class TestFirebaseCoreHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseCoreHostApi.optionsFromResource', - codec, - binaryMessenger: binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseCoreHostApi.optionsFromResource', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -131,100 +115,72 @@ abstract class TestFirebaseAppHostApi { Future setAutomaticDataCollectionEnabled(String appName, bool enabled); Future setAutomaticResourceManagementEnabled( - String appName, - bool enabled, - ); + String appName, bool enabled); Future delete(String appName); - static void setup( - TestFirebaseAppHostApi? api, { - BinaryMessenger? binaryMessenger, - }) { + static void setup(TestFirebaseAppHostApi? api, + {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticDataCollectionEnabled', - codec, - binaryMessenger: binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticDataCollectionEnabled', + codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert( - message != null, - 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticDataCollectionEnabled was null.', - ); + assert(message != null, + 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticDataCollectionEnabled was null.'); final List args = (message as List?)!; final String? arg_appName = (args[0] as String?); - assert( - arg_appName != null, - 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticDataCollectionEnabled was null, expected non-null String.', - ); + assert(arg_appName != null, + 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticDataCollectionEnabled was null, expected non-null String.'); final bool? arg_enabled = (args[1] as bool?); - assert( - arg_enabled != null, - 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticDataCollectionEnabled was null, expected non-null bool.', - ); + assert(arg_enabled != null, + 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticDataCollectionEnabled was null, expected non-null bool.'); await api.setAutomaticDataCollectionEnabled( - arg_appName!, - arg_enabled!, - ); + arg_appName!, arg_enabled!); return {}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticResourceManagementEnabled', - codec, - binaryMessenger: binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticResourceManagementEnabled', + codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert( - message != null, - 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticResourceManagementEnabled was null.', - ); + assert(message != null, + 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticResourceManagementEnabled was null.'); final List args = (message as List?)!; final String? arg_appName = (args[0] as String?); - assert( - arg_appName != null, - 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticResourceManagementEnabled was null, expected non-null String.', - ); + assert(arg_appName != null, + 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticResourceManagementEnabled was null, expected non-null String.'); final bool? arg_enabled = (args[1] as bool?); - assert( - arg_enabled != null, - 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticResourceManagementEnabled was null, expected non-null bool.', - ); + assert(arg_enabled != null, + 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.setAutomaticResourceManagementEnabled was null, expected non-null bool.'); await api.setAutomaticResourceManagementEnabled( - arg_appName!, - arg_enabled!, - ); + arg_appName!, arg_enabled!); return {}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FirebaseAppHostApi.delete', - codec, - binaryMessenger: binaryMessenger, - ); + 'dev.flutter.pigeon.FirebaseAppHostApi.delete', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert( - message != null, - 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.delete was null.', - ); + assert(message != null, + 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.delete was null.'); final List args = (message as List?)!; final String? arg_appName = (args[0] as String?); - assert( - arg_appName != null, - 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.delete was null, expected non-null String.', - ); + assert(arg_appName != null, + 'Argument for dev.flutter.pigeon.FirebaseAppHostApi.delete was null, expected non-null String.'); await api.delete(arg_appName!); return {}; }); diff --git a/packages/flutterfire_ui/pubspec.yaml b/packages/flutterfire_ui/pubspec.yaml index 86d84eacfc2a..309190d83923 100644 --- a/packages/flutterfire_ui/pubspec.yaml +++ b/packages/flutterfire_ui/pubspec.yaml @@ -33,12 +33,10 @@ dependencies: dev_dependencies: fake_cloud_firestore: ^1.2.2 - firebase_auth_mocks: ^0.8.4 - flutter_test: sdk: flutter + mocktail: ^0.3.0 - # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. @@ -50,6 +48,7 @@ flutter: assets: - assets/icons/ - assets/countries.json + # To add assets to your package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg diff --git a/packages/flutterfire_ui/test/auth/widgets/email_form_test.dart b/packages/flutterfire_ui/test/auth/widgets/email_form_test.dart index ee6e9b1be9e0..87b07c77db1d 100644 --- a/packages/flutterfire_ui/test/auth/widgets/email_form_test.dart +++ b/packages/flutterfire_ui/test/auth/widgets/email_form_test.dart @@ -1,12 +1,15 @@ -import 'package:firebase_auth_mocks/firebase_auth_mocks.dart'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutterfire_ui/auth.dart'; import 'package:flutterfire_ui/src/auth/widgets/internal/loading_button.dart'; import 'package:flutterfire_ui/src/auth/widgets/internal/universal_button.dart'; +import 'package:mocktail/mocktail.dart'; import '../../test_utils.dart'; +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + void main() { group('EmailForm', () { late Widget widget; diff --git a/tests/test_driver/firebase_auth/firebase_auth_e2e.dart b/tests/test_driver/firebase_auth/firebase_auth_e2e.dart index 772af69b9c30..a3e5fd2780a3 100644 --- a/tests/test_driver/firebase_auth/firebase_auth_e2e.dart +++ b/tests/test_driver/firebase_auth/firebase_auth_e2e.dart @@ -2,15 +2,15 @@ // 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 'package:drive/drive.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; -import 'package:drive/drive.dart'; import '../firebase_default_options.dart'; - -import 'test_utils.dart'; import 'firebase_auth_instance_e2e.dart' as instance_tests; +import 'firebase_auth_multi_factor_e2e.dart' as multi_factor_tests; import 'firebase_auth_user_e2e.dart' as user_tests; +import 'test_utils.dart'; void setupTests() { group('firebase_auth', () { @@ -54,5 +54,6 @@ void setupTests() { instance_tests.setupTests(); user_tests.setupTests(); + multi_factor_tests.setupTests(); }); } diff --git a/tests/test_driver/firebase_auth/firebase_auth_multi_factor_e2e.dart b/tests/test_driver/firebase_auth/firebase_auth_multi_factor_e2e.dart new file mode 100644 index 000000000000..75e53f0f9e75 --- /dev/null +++ b/tests/test_driver/firebase_auth/firebase_auth_multi_factor_e2e.dart @@ -0,0 +1,404 @@ +// Copyright 2020, the Chromium 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 'dart:async'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_utils.dart'; + +void setupTests() { + group( + '$MultiFactor', + () { + String email = generateRandomEmail(); + + group('multiFactor', () { + test('should return an empty enrolled factor', () async { + // Setup + User? user; + UserCredential userCredential; + + userCredential = + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); + user = userCredential.user; + + final multiFactor = user!.multiFactor; + + // Assertions + expect((await multiFactor.getEnrolledFactors()).length, 0); + }); + }); + + group('session', () { + test('should return an empty enrolled factor', () async { + // Setup + User? user; + UserCredential userCredential; + + userCredential = + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); + user = userCredential.user; + + final multiFactor = user!.multiFactor; + + final session = await multiFactor.getSession(); + + // Assertions + expect(session.id, isNotNull); + }); + }); + + group('enrollFactor', () { + test( + 'should enroll and unenroll factor', + () async { + String testPhoneNumber = '+441444555666'; + User? user; + UserCredential userCredential; + + userCredential = + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); + user = userCredential.user; + + await user!.sendEmailVerification(); + final oobCode = (await emulatorOutOfBandCode( + email, + EmulatorOobCodeType.verifyEmail, + ))!; + + await emulatorVerifyEmail( + oobCode.oobCode!, + ); + + final multiFactor = user.multiFactor; + final session = await multiFactor.getSession(); + + Future getCredential() async { + Completer completer = Completer(); + + unawaited( + FirebaseAuth.instance.verifyPhoneNumber( + phoneNumber: testPhoneNumber, + multiFactorSession: session, + verificationCompleted: (PhoneAuthCredential credential) { + if (!completer.isCompleted) { + return completer.completeError( + Exception( + 'verificationCompleted should not have been called', + ), + ); + } + }, + verificationFailed: (FirebaseException e) { + if (!completer.isCompleted) { + return completer.completeError( + Exception( + 'verificationFailed should not have been called', + ), + ); + } + }, + codeSent: (String verificationId, int? resetToken) { + completer.complete(verificationId); + }, + codeAutoRetrievalTimeout: (String foo) { + if (!completer.isCompleted) { + return completer.completeError( + Exception( + 'codeAutoRetrievalTimeout should not have been called', + ), + ); + } + }, + ), + ); + + return completer.future as FutureOr; + } + + final verificationId = await getCredential(); + + final smsCode = await emulatorPhoneVerificationCode( + testPhoneNumber, + ); + + final credential = PhoneAuthProvider.credential( + verificationId: verificationId, + smsCode: smsCode!, + ); + + expect(credential, isA()); + + await user.multiFactor.enroll( + PhoneMultiFactorGenerator.getAssertion( + credential, + ), + displayName: 'My phone number', + ); + + final enrolledFactors = await multiFactor.getEnrolledFactors(); + + // Assertions + expect(enrolledFactors.length, 1); + expect(enrolledFactors.first.displayName, 'My phone number'); + + await user.multiFactor.unenroll( + multiFactorInfo: enrolledFactors.first, + ); + + final enrolledFactorsAfter = await multiFactor.getEnrolledFactors(); + + // Assertions + expect(enrolledFactorsAfter.length, 0); + }, + skip: kIsWeb || defaultTargetPlatform != TargetPlatform.android, + ); + + test( + 'should not enroll factor if email not verifed', + () async { + String testPhoneNumber = '+448444555666'; + User? user; + UserCredential userCredential; + final email = generateRandomEmail(); + + userCredential = + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); + user = userCredential.user; + + final multiFactor = user!.multiFactor; + final session = await multiFactor.getSession(); + + Future getCredential() async { + Completer completer = Completer(); + + unawaited( + FirebaseAuth.instance.verifyPhoneNumber( + phoneNumber: testPhoneNumber, + multiFactorSession: session, + verificationCompleted: (PhoneAuthCredential credential) { + if (!completer.isCompleted) { + return completer.completeError( + Exception('Should not have been called'), + ); + } + }, + verificationFailed: (FirebaseException e) { + completer.complete(e); + }, + codeSent: (String verificationId, int? resetToken) { + if (!completer.isCompleted) { + return completer.completeError( + Exception('Should not have been called'), + ); + } + }, + codeAutoRetrievalTimeout: (String foo) { + if (!completer.isCompleted) { + return completer.completeError( + Exception('Should not have been called'), + ); + } + }, + ), + ); + + return completer.future as FutureOr; + } + + final exception = await getCredential(); + + expect(exception, isNotNull); + }, + ); + }); + + group('signIn', () { + test( + 'should signin with 2 factors', + () async { + String testPhoneNumber = '+449444555666'; + User? user; + UserCredential userCredential; + + userCredential = + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); + user = userCredential.user; + + await user!.sendEmailVerification(); + final oobCode = (await emulatorOutOfBandCode( + email, + EmulatorOobCodeType.verifyEmail, + ))!; + + await emulatorVerifyEmail( + oobCode.oobCode!, + ); + + final multiFactor = user.multiFactor; + final session = await multiFactor.getSession(); + + Future getCredential() async { + Completer completer = Completer(); + + unawaited( + FirebaseAuth.instance.verifyPhoneNumber( + phoneNumber: testPhoneNumber, + multiFactorSession: session, + verificationCompleted: (PhoneAuthCredential credential) { + if (!completer.isCompleted) { + return completer.completeError( + Exception('Should not have been called'), + ); + } + }, + verificationFailed: (FirebaseException e) { + if (!completer.isCompleted) { + return completer.completeError( + Exception('Should not have been called'), + ); + } + }, + codeSent: (String verificationId, int? resetToken) { + completer.complete(verificationId); + }, + codeAutoRetrievalTimeout: (String foo) { + if (!completer.isCompleted) { + return completer.completeError( + Exception('Should not have been called'), + ); + } + }, + ), + ); + + return completer.future as FutureOr; + } + + final verificationId = await getCredential(); + + final smsCode = await emulatorPhoneVerificationCode( + testPhoneNumber, + ); + + final credential = PhoneAuthProvider.credential( + verificationId: verificationId, + smsCode: smsCode!, + ); + + expect(credential, isA()); + + await user.multiFactor.enroll( + PhoneMultiFactorGenerator.getAssertion( + credential, + ), + displayName: 'My phone number', + ); + + await FirebaseAuth.instance.signOut(); + + Exception? exception; + + try { + userCredential = + await FirebaseAuth.instance.signInWithEmailAndPassword( + email: email, + password: testPassword, + ); + } catch (e) { + exception = e as Exception; + } + + expect(exception, isA()); + + if (exception == null) { + throw Exception('Should not be null'); + } + + final FirebaseAuthMultiFactorException multiFactorException = + exception as FirebaseAuthMultiFactorException; + + Future getCredentialSignIn() async { + Completer completer = Completer(); + + unawaited( + FirebaseAuth.instance.verifyPhoneNumber( + multiFactorInfo: multiFactorException.resolver.hints.first + as PhoneMultiFactorInfo, + multiFactorSession: multiFactorException.resolver.session, + verificationCompleted: (PhoneAuthCredential credential) { + if (!completer.isCompleted) { + return completer.completeError( + Exception('Should not have been called'), + ); + } + }, + verificationFailed: (FirebaseException e) { + if (!completer.isCompleted) { + return completer.completeError( + Exception('Should not have been called'), + ); + } + }, + codeSent: (String verificationId, int? resetToken) { + completer.complete(verificationId); + }, + codeAutoRetrievalTimeout: (String foo) { + if (!completer.isCompleted) { + return completer.completeError( + Exception('Should not have been called'), + ); + } + }, + ), + ); + + return completer.future as FutureOr; + } + + final verificationIdSignIn = await getCredentialSignIn(); + + final smsCodeSignIn = await emulatorPhoneVerificationCode( + testPhoneNumber, + ); + + final credentialSignIn = PhoneAuthProvider.credential( + verificationId: verificationIdSignIn, + smsCode: smsCodeSignIn!, + ); + + expect(credentialSignIn, isA()); + + await exception.resolver.resolveSignIn( + PhoneMultiFactorGenerator.getAssertion( + credentialSignIn, + ), + ); + + expect(FirebaseAuth.instance.currentUser, isNotNull); + }, + ); + }); + }, + skip: kIsWeb || defaultTargetPlatform != TargetPlatform.android, + ); +} diff --git a/tests/test_driver/firebase_auth/test_utils.dart b/tests/test_driver/firebase_auth/test_utils.dart index c63601a5b3ec..7089d86bc9be 100644 --- a/tests/test_driver/firebase_auth/test_utils.dart +++ b/tests/test_driver/firebase_auth/test_utils.dart @@ -105,6 +105,17 @@ Future emulatorPhoneVerificationCode(String phoneNumber) async { )['code']; } +/// Verify an email with an oobCode +/// +/// Check [emulatorOutOfBandCode] to get an oobCode. +Future emulatorVerifyEmail(String oobCode) async { + await http.get( + Uri.parse( + 'http://$testEmulatorHost:$testEmulatorPort/emulator/action?mode=verifyEmail&lang=en&oobCode=$oobCode&apiKey=fake-api-key', + ), + ); +} + /// Retrieve a out of band authentication code from the emulator. Useful for testing /// APIs such as email verification and password resetting. Future emulatorOutOfBandCode(