Kakao·LINE 로그인을 Firebase OIDC 제공자로 연결 · 계정 연결
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
/// 인증 계약 — 제공자 인터페이스와 결과 타입.
|
/// 인증 계약 — 제공자 인터페이스와 결과 타입.
|
||||||
///
|
///
|
||||||
/// 원칙: **"키 미투입"은 예외가 아니라 상태다.** Apple·카카오·LINE 은 발주사 사업자
|
/// 원칙: **"키 미투입"은 예외가 아니라 상태다.** Firebase 설정 파일이나 Apple
|
||||||
/// 정보로 각 개발자 콘솔에 앱을 등록해야 키가 나온다. "아직 준비가 안 됐다"이지
|
/// 웹 인증 값처럼 빌드에 들어가야 할 값이 없을 때가 그렇다. "아직 준비가 안 됐다"이지
|
||||||
/// "지원하지 않는다"가 아니므로 예외를 던지지 않고 [AuthNotConfigured] 로 돌려준다.
|
/// "지원하지 않는다"가 아니므로 예외를 던지지 않고 [AuthNotConfigured] 로 돌려준다.
|
||||||
/// 화면은 이걸 안내 다이얼로그로 보여 준다.
|
/// 화면은 이걸 안내 다이얼로그로 보여 준다.
|
||||||
library;
|
library;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
|
|
||||||
import '../../core/firebase/firebase_boot.dart';
|
import '../../core/firebase/firebase_boot.dart';
|
||||||
import 'auth.dart';
|
import 'auth.dart';
|
||||||
|
import 'firebase_account_linker.dart';
|
||||||
import 'social_providers.dart';
|
import 'social_providers.dart';
|
||||||
import '../../core/i18n/app_locale.dart';
|
import '../../core/i18n/app_locale.dart';
|
||||||
|
|
||||||
@@ -56,18 +57,26 @@ class AuthRepository extends ChangeNotifier {
|
|||||||
/// 사용자가 닿는 경로가 없다 — 읽는 분기가 없는 기능 플래그로 닫는 것보다 확실하다.
|
/// 사용자가 닿는 경로가 없다 — 읽는 분기가 없는 기능 플래그로 닫는 것보다 확실하다.
|
||||||
/// QA 경로는 [qaEmailProvider] 하나뿐이다.
|
/// QA 경로는 [qaEmailProvider] 하나뿐이다.
|
||||||
///
|
///
|
||||||
/// Apple·카카오·LINE 은 발주사 키가 없어 「키 대기」 상태다. 눌러도 예외가 아니라
|
/// 카카오·LINE 은 Firebase OIDC 제공자다. 콘솔 설정 전이면 Firebase 가
|
||||||
/// [AuthNotConfigured] 가 돌아오고 화면이 안내 다이얼로그를 띄운다.
|
/// `operation-not-allowed`(E-AUTH-11)를 돌려준다.
|
||||||
static List<SocialAuthProvider> _defaultProviders() => [
|
static List<SocialAuthProvider> _defaultProviders() => [
|
||||||
const GoogleSignInProvider(),
|
const GoogleSignInProvider(),
|
||||||
AppleSignInProvider(
|
AppleSignInProvider(
|
||||||
webAuthenticationOptions: appleWebAuthenticationOptionsFromEnvironment(),
|
webAuthenticationOptions: appleWebAuthenticationOptionsFromEnvironment(),
|
||||||
),
|
),
|
||||||
const KeylessSocialProvider(AuthProviderKind.kakao, [
|
// 동의 항목: 카카오 콘솔의 닉네임·이메일, LINE 의 profile·email(이메일 권한 신청 승인 후).
|
||||||
'카카오 네이티브 앱 키',
|
OidcSignInProvider(
|
||||||
'카카오 개발자 콘솔 앱 등록(사업자 정보 필요)',
|
AuthProviderKind.kakao,
|
||||||
]),
|
providerId: FirebaseAccountLinker.providerIds[AuthProviderKind.kakao]!,
|
||||||
lineProvider,
|
// 이메일(account_email)은 비즈 앱 전환 후에만 요청할 수 있다. 전환 전에 요청하면
|
||||||
|
// KOE205 로 로그인 자체가 막힌다 — 전환되면 여기에 다시 넣는다.
|
||||||
|
scopes: const ['profile_nickname'],
|
||||||
|
),
|
||||||
|
OidcSignInProvider(
|
||||||
|
AuthProviderKind.line,
|
||||||
|
providerId: FirebaseAccountLinker.providerIds[AuthProviderKind.line]!,
|
||||||
|
scopes: const ['profile', 'email'],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// QA 계정(qa-test@fanuzu.co.kr) 전용 이메일 로그인 — **디버그 빌드에서만**.
|
/// QA 계정(qa-test@fanuzu.co.kr) 전용 이메일 로그인 — **디버그 빌드에서만**.
|
||||||
|
|||||||
@@ -56,13 +56,13 @@ final class FirebaseAccountLinker implements AccountLinker {
|
|||||||
}
|
}
|
||||||
final missing = impl.missingConfig;
|
final missing = impl.missingConfig;
|
||||||
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
|
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
|
||||||
// 키 대기 제공자(카카오·LINE)는 자격증명을 만들 수 없다.
|
if (impl is! CredentialProvider && impl is! OidcSignInProvider) {
|
||||||
if (impl is! CredentialProvider) {
|
|
||||||
return AuthNotConfigured(provider, const ['자격증명 구현']);
|
return AuthNotConfigured(provider, const ['자격증명 구현']);
|
||||||
}
|
}
|
||||||
final source = impl as CredentialProvider;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (impl is OidcSignInProvider) return await _linkOidc(user, impl);
|
||||||
|
final source = impl as CredentialProvider;
|
||||||
final obtained = await source.obtainCredential();
|
final obtained = await source.obtainCredential();
|
||||||
// 사용자가 소셜 창을 닫았다. 실패로 세지 않는다.
|
// 사용자가 소셜 창을 닫았다. 실패로 세지 않는다.
|
||||||
if (obtained == null) return const AuthCancelled();
|
if (obtained == null) return const AuthCancelled();
|
||||||
@@ -84,6 +84,7 @@ final class FirebaseAccountLinker implements AccountLinker {
|
|||||||
final result = await user.linkWithCredential(obtained.credential);
|
final result = await user.linkWithCredential(obtained.credential);
|
||||||
return AuthSucceeded(_accountOf(result.user, provider));
|
return AuthSucceeded(_accountOf(result.user, provider));
|
||||||
} on fb.FirebaseAuthException catch (error) {
|
} on fb.FirebaseAuthException catch (error) {
|
||||||
|
if (isWebFlowCancelled(error)) return const AuthCancelled();
|
||||||
return AuthFailed(
|
return AuthFailed(
|
||||||
authCatalogCode(error),
|
authCatalogCode(error),
|
||||||
authMessage(error, flow: AuthFlow.link),
|
authMessage(error, flow: AuthFlow.link),
|
||||||
@@ -121,6 +122,31 @@ final class FirebaseAccountLinker implements AccountLinker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 카카오·LINE(OIDC) 연결.
|
||||||
|
///
|
||||||
|
/// ponytail: OIDC 웹 흐름은 자격증명을 먼저 내주지 않고 연결까지 한 번에 끝낸다.
|
||||||
|
/// 그래서 「동일 이메일만 연동」을 **붙인 뒤** 판정하고 어긋나면 바로 뗀다.
|
||||||
|
/// 떼기가 실패하면 남의 이메일 계정이 붙은 채 남는다 — 막아야 하면 Functions 의
|
||||||
|
/// blocking 함수(beforeSignIn)로 서버에서 거른다.
|
||||||
|
Future<AuthOutcome> _linkOidc(fb.User user, OidcSignInProvider impl) async {
|
||||||
|
final mine = user.email;
|
||||||
|
final result = await user.linkWithProvider(impl.authProvider);
|
||||||
|
// 연결된 제공자 정보에 그 계정의 이메일이 실린다(동의 항목에 이메일이 있을 때).
|
||||||
|
final theirs = _emailOf(result.user, impl.providerId);
|
||||||
|
if (mine != null && theirs != null && !sameAccountIdentity(mine, theirs)) {
|
||||||
|
await user.unlink(impl.providerId);
|
||||||
|
return AuthFailed('E-AUTH-08', appL10n.authDifferentEmail);
|
||||||
|
}
|
||||||
|
return AuthSucceeded(_accountOf(result.user, impl.provider));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _emailOf(fb.User? user, String providerId) {
|
||||||
|
for (final info in user?.providerData ?? const <fb.UserInfo>[]) {
|
||||||
|
if (info.providerId == providerId) return info.email;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
SocialAuthProvider? _implOf(AuthProviderKind provider) {
|
SocialAuthProvider? _implOf(AuthProviderKind provider) {
|
||||||
for (final candidate in _providers) {
|
for (final candidate in _providers) {
|
||||||
if (candidate.provider == provider) return candidate;
|
if (candidate.provider == provider) return candidate;
|
||||||
|
|||||||
@@ -199,31 +199,87 @@ final class AppleSignInProvider
|
|||||||
kIsWeb || defaultTargetPlatform == TargetPlatform.android;
|
kIsWeb || defaultTargetPlatform == TargetPlatform.android;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 발주사 키를 기다리는 제공자 — 카카오·LINE.
|
/// 카카오·LINE — Firebase Auth 의 OpenID Connect 제공자(`oidc.kakao` · `oidc.line`).
|
||||||
///
|
///
|
||||||
/// 예외를 던지지 않는다. [AuthNotConfigured] 로 "무엇이 없어서 못 쓰는지"를 돌려주고
|
/// 두 회사 모두 OIDC 표준(issuer `https://kauth.kakao.com` · `https://access.line.me`)을
|
||||||
/// 로그인 화면이 그걸 안내 다이얼로그로 띄운다. 키가 들어오면 이 자리에 실 구현체를
|
/// 지원해 Firebase 가 코드 교환·ID 토큰 검증까지 맡는다. 앱은 SDK 없이
|
||||||
/// 넣고 [AuthRepository] 목록만 바꾸면 된다 — 화면·라우터는 그대로다.
|
/// `signInWithProvider` 하나로 끝난다 — Android 는 Custom Tabs, iOS 는
|
||||||
final class KeylessSocialProvider implements SocialAuthProvider {
|
/// SFSafariViewController 로 인가 화면을 띄우고 Firebase 핸들러
|
||||||
const KeylessSocialProvider(this.provider, this.missingConfig);
|
/// (`https://<프로젝트>.firebaseapp.com/__/auth/handler`)로 돌아온다.
|
||||||
|
/// iOS 콜백 스킴은 Google 로그인용 REVERSED_CLIENT_ID 를 같이 쓴다.
|
||||||
|
///
|
||||||
|
/// 콘솔에서 제공자를 켜지 않았으면 Firebase 가 `operation-not-allowed` 를 돌려주고
|
||||||
|
/// [authCatalogCode] 가 E-AUTH-11 로 옮긴다 — 앱이 따로 플래그를 들 필요가 없다.
|
||||||
|
///
|
||||||
|
/// [CredentialProvider] 가 아니다. 웹 흐름이 자격증명 발급과 로그인·연결을 한 번에
|
||||||
|
/// 끝내서 자격증명만 먼저 받아 볼 수 없다. 계정 연결은 [FirebaseAccountLinker] 가
|
||||||
|
/// [authProvider] 로 `linkWithProvider` 를 부른다.
|
||||||
|
final class OidcSignInProvider implements SocialAuthProvider {
|
||||||
|
const OidcSignInProvider(
|
||||||
|
this.provider, {
|
||||||
|
required this.providerId,
|
||||||
|
this.scopes = const [],
|
||||||
|
this.firebaseAuth,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final SocialProvider provider;
|
final SocialProvider provider;
|
||||||
|
|
||||||
@override
|
/// Firebase 콘솔에 등록한 제공자 ID. `oidc.` 접두는 콘솔이 강제한다.
|
||||||
final List<String> missingConfig;
|
final String providerId;
|
||||||
|
|
||||||
|
/// `openid` 외에 더 받을 항목. Firebase 가 `openid` 는 알아서 붙인다.
|
||||||
|
/// **콘솔에 켠 동의 항목과 맞아야 한다** — 카카오는 설정에 없는 scope 를 요청하면
|
||||||
|
/// KOE205 로 로그인 자체를 막는다.
|
||||||
|
final List<String> scopes;
|
||||||
|
|
||||||
|
/// 테스트 주입용. 기본은 [fb.FirebaseAuth.instance].
|
||||||
|
final fb.FirebaseAuth? firebaseAuth;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get isLive => false;
|
bool get isLive => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<AuthOutcome> signIn() async =>
|
List<String> get missingConfig =>
|
||||||
AuthNotConfigured(provider, missingConfig);
|
FirebaseBoot.isLive ? const [] : FirebaseBoot.missingLabels;
|
||||||
|
|
||||||
|
fb.OAuthProvider get authProvider {
|
||||||
|
final p = fb.OAuthProvider(providerId);
|
||||||
|
for (final scope in scopes) {
|
||||||
|
p.addScope(scope);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> signOut() async {}
|
Future<AuthOutcome> signIn() async {
|
||||||
|
final missing = missingConfig;
|
||||||
|
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
|
||||||
|
try {
|
||||||
|
final result = await (firebaseAuth ?? fb.FirebaseAuth.instance)
|
||||||
|
.signInWithProvider(authProvider);
|
||||||
|
return AuthSucceeded(_toAccount(result.user, provider));
|
||||||
|
} on fb.FirebaseAuthException catch (error) {
|
||||||
|
if (isWebFlowCancelled(error)) return const AuthCancelled();
|
||||||
|
return AuthFailed(
|
||||||
|
authCatalogCode(error),
|
||||||
|
authMessage(error, flow: AuthFlow.signIn),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> signOut() async {
|
||||||
|
// 카카오·LINE 쪽 세션은 브라우저 쿠키라 앱이 지울 수 없다. Firebase 세션은
|
||||||
|
// AuthSession 이 닫는다.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 사용자가 인가 웹 화면을 닫았다. Android 는 `canceled`, iOS 는 `cancelled` 철자다.
|
||||||
|
bool isWebFlowCancelled(fb.FirebaseAuthException error) =>
|
||||||
|
error.code == 'web-context-canceled' ||
|
||||||
|
error.code == 'web-context-cancelled';
|
||||||
|
|
||||||
/// Apple Service ID와 callback URL은 비밀값이 아니지만 환경별 값이다.
|
/// Apple Service ID와 callback URL은 비밀값이 아니지만 환경별 값이다.
|
||||||
/// 저장소에 임시 상수를 박지 않고 빌드 시 `--dart-define`으로만 받는다.
|
/// 저장소에 임시 상수를 박지 않고 빌드 시 `--dart-define`으로만 받는다.
|
||||||
apple.WebAuthenticationOptions? appleWebAuthenticationOptionsFromEnvironment() {
|
apple.WebAuthenticationOptions? appleWebAuthenticationOptionsFromEnvironment() {
|
||||||
@@ -337,14 +393,6 @@ final class EmailPasswordProvider implements EmailAuthProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// LINE — 발주사 키 대기.
|
|
||||||
///
|
|
||||||
/// 별첨 B v1.4 확정 4종에 LINE 이 있고 네이버·X 는 없다.
|
|
||||||
const lineProvider = KeylessSocialProvider(AuthProviderKind.line, [
|
|
||||||
'LINE Login 채널 ID / 채널 시크릿',
|
|
||||||
'LINE Developers 콘솔 채널 등록(사업자 정보 필요)',
|
|
||||||
]);
|
|
||||||
|
|
||||||
/// SDK 코드를 에러 카탈로그 ID 로 옮긴다.
|
/// SDK 코드를 에러 카탈로그 ID 로 옮긴다.
|
||||||
///
|
///
|
||||||
/// **계정 존재 여부를 드러내지 않는다.** 「등록되지 않은 이메일」과 「비밀번호
|
/// **계정 존재 여부를 드러내지 않는다.** 「등록되지 않은 이메일」과 「비밀번호
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import 'package:fancake/core/firebase/firebase_boot.dart';
|
||||||
|
import 'package:fancake/features/auth/auth.dart';
|
||||||
|
import 'package:fancake/features/auth/auth_repository.dart';
|
||||||
|
import 'package:fancake/features/auth/firebase_account_linker.dart';
|
||||||
|
import 'package:fancake/features/auth/social_providers.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart' as fb;
|
||||||
|
import 'package:firebase_auth_mocks/firebase_auth_mocks.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// 카카오·LINE OIDC 제공자 — 콘솔 설정 전이라 실 로그인은 못 하고,
|
||||||
|
/// Firebase 호출 전후 계약(결과 매핑·취소·연결)만 확인한다.
|
||||||
|
void main() {
|
||||||
|
setUp(() => FirebaseBoot.debugSetMode(FirebaseMode.live, reason: 'OIDC 테스트'));
|
||||||
|
tearDown(
|
||||||
|
() => FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: 'OIDC 테스트 끝'),
|
||||||
|
);
|
||||||
|
|
||||||
|
OidcSignInProvider kakao(fb.FirebaseAuth auth) => OidcSignInProvider(
|
||||||
|
AuthProviderKind.kakao,
|
||||||
|
providerId: 'oidc.kakao',
|
||||||
|
firebaseAuth: auth,
|
||||||
|
);
|
||||||
|
|
||||||
|
test('기본 목록의 카카오·LINE 은 Firebase OIDC 제공자다 — 「준비 중」이 아니다', () {
|
||||||
|
final providers = {
|
||||||
|
for (final p in AuthRepository().providers) p.provider: p,
|
||||||
|
};
|
||||||
|
for (final kind in [AuthProviderKind.kakao, AuthProviderKind.line]) {
|
||||||
|
final p = providers[kind];
|
||||||
|
expect(p, isA<OidcSignInProvider>(), reason: kind.name);
|
||||||
|
expect(p!.isLive, isTrue);
|
||||||
|
expect(
|
||||||
|
(p as OidcSignInProvider).providerId,
|
||||||
|
FirebaseAccountLinker.providerIds[kind],
|
||||||
|
);
|
||||||
|
expect(p.authProvider.providerId, 'oidc.${kind.name}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Firebase 설정이 없으면 SDK 를 부르지 않고 미설정으로 닫는다', () async {
|
||||||
|
FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: '설정 없음');
|
||||||
|
final outcome = await kakao(_ThrowingAuth('should-not-call')).signIn();
|
||||||
|
expect(outcome, isA<AuthNotConfigured>());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('성공하면 카카오 계정으로 로그인된다', () async {
|
||||||
|
final auth = MockFirebaseAuth(mockUser: MockUser(uid: 'k-1'));
|
||||||
|
final outcome = await kakao(auth).signIn();
|
||||||
|
expect(outcome, isA<AuthSucceeded>());
|
||||||
|
expect((outcome as AuthSucceeded).account.provider, AuthProviderKind.kakao);
|
||||||
|
expect(outcome.account.uid, 'k-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('인가 화면을 닫으면 Android·iOS 철자 모두 취소로 본다', () async {
|
||||||
|
for (final code in ['web-context-canceled', 'web-context-cancelled']) {
|
||||||
|
final outcome = await kakao(_ThrowingAuth(code)).signIn();
|
||||||
|
expect(outcome, isA<AuthCancelled>(), reason: code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('콘솔에서 제공자를 안 켰으면 E-AUTH-11', () async {
|
||||||
|
final outcome = await kakao(
|
||||||
|
_ThrowingAuth('operation-not-allowed'),
|
||||||
|
).signIn();
|
||||||
|
expect((outcome as AuthFailed).code, 'E-AUTH-11');
|
||||||
|
});
|
||||||
|
|
||||||
|
group('계정 연결', () {
|
||||||
|
test('같은 UID 에 oidc.kakao 를 붙인다', () async {
|
||||||
|
final user = _LinkingUser(linkedEmail: 'fan@fanuzu.co.kr');
|
||||||
|
final auth = MockFirebaseAuth(signedIn: true, mockUser: user);
|
||||||
|
final linker = FirebaseAccountLinker(
|
||||||
|
providers: [kakao(auth)],
|
||||||
|
firebaseAuth: auth,
|
||||||
|
);
|
||||||
|
|
||||||
|
final outcome = await linker.link(AuthProviderKind.kakao);
|
||||||
|
|
||||||
|
expect(outcome, isA<AuthSucceeded>());
|
||||||
|
expect(linker.linkedAccounts.keys, contains(AuthProviderKind.kakao));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('다른 이메일의 카카오 계정은 붙였다가 바로 떼고 E-AUTH-08', () async {
|
||||||
|
final user = _LinkingUser(linkedEmail: 'someone@else.com');
|
||||||
|
final auth = MockFirebaseAuth(signedIn: true, mockUser: user);
|
||||||
|
final linker = FirebaseAccountLinker(
|
||||||
|
providers: [kakao(auth)],
|
||||||
|
firebaseAuth: auth,
|
||||||
|
);
|
||||||
|
|
||||||
|
final outcome = await linker.link(AuthProviderKind.kakao);
|
||||||
|
|
||||||
|
expect((outcome as AuthFailed).code, 'E-AUTH-08');
|
||||||
|
expect(
|
||||||
|
linker.linkedAccounts.keys,
|
||||||
|
isNot(contains(AuthProviderKind.kakao)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ThrowingAuth extends MockFirebaseAuth {
|
||||||
|
_ThrowingAuth(this.code);
|
||||||
|
final String code;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<fb.UserCredential> signInWithProvider(fb.AuthProvider provider) =>
|
||||||
|
throw fb.FirebaseAuthException(code: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Google 로 가입한 사용자. 연결하면 [linkedEmail] 을 가진 제공자 정보가 붙는다.
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class _LinkingUser extends MockUser {
|
||||||
|
_LinkingUser({required this.linkedEmail})
|
||||||
|
: super(
|
||||||
|
uid: 'u-1',
|
||||||
|
email: 'fan@fanuzu.co.kr',
|
||||||
|
providerData: [_info('google.com', 'fan@fanuzu.co.kr')],
|
||||||
|
);
|
||||||
|
|
||||||
|
final String linkedEmail;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<fb.UserCredential> linkWithProvider(fb.AuthProvider provider) async {
|
||||||
|
final result = await super.linkWithProvider(provider);
|
||||||
|
providerData
|
||||||
|
..removeLast()
|
||||||
|
..add(_info(provider.providerId, linkedEmail));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fb.UserInfo _info(String providerId, String email) =>
|
||||||
|
// ignore: invalid_use_of_protected_member
|
||||||
|
fb.UserInfo.fromJson({
|
||||||
|
'uid': 'p-$providerId',
|
||||||
|
'email': email,
|
||||||
|
'providerId': providerId,
|
||||||
|
'isAnonymous': false,
|
||||||
|
'isEmailVerified': true,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user