Kakao 로그인(카카오 SDK · 서버 커스텀 토큰)과 LINE 로그인(Firebase OIDC) 연결 · SNS 연동 · 인증 취소 처리
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/// 인증 계약 — 제공자 인터페이스와 결과 타입.
|
||||
///
|
||||
/// 원칙: **"키 미투입"은 예외가 아니라 상태다.** Apple·카카오·LINE 은 발주사 사업자
|
||||
/// 정보로 각 개발자 콘솔에 앱을 등록해야 키가 나온다. "아직 준비가 안 됐다"이지
|
||||
/// 원칙: **"키 미투입"은 예외가 아니라 상태다.** Firebase 설정 파일이나 Apple
|
||||
/// 웹 인증 값처럼 빌드에 들어가야 할 값이 없을 때가 그렇다. "아직 준비가 안 됐다"이지
|
||||
/// "지원하지 않는다"가 아니므로 예외를 던지지 않고 [AuthNotConfigured] 로 돌려준다.
|
||||
/// 화면은 이걸 안내 다이얼로그로 보여 준다.
|
||||
library;
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../core/firebase/firebase_boot.dart';
|
||||
import 'auth.dart';
|
||||
import 'firebase_account_linker.dart';
|
||||
import 'social_providers.dart';
|
||||
|
||||
/// 로그인 상태 보관소. 라우터가 이걸 듣고 로그인/셸을 가른다.
|
||||
@@ -44,18 +45,26 @@ class AuthRepository extends ChangeNotifier {
|
||||
/// 사용자가 닿는 경로가 없다 — 읽는 분기가 없는 기능 플래그로 닫는 것보다 확실하다.
|
||||
/// QA 경로는 [qaEmailProvider] 하나뿐이다.
|
||||
///
|
||||
/// Apple·카카오·LINE 은 발주사 키가 없어 「키 대기」 상태다. 눌러도 예외가 아니라
|
||||
/// [AuthNotConfigured] 가 돌아오고 화면이 안내 다이얼로그를 띄운다.
|
||||
/// 카카오는 공식 SDK + 서버 커스텀 토큰([KakaoSignInProvider]), LINE 은 Firebase OIDC
|
||||
/// 제공자다. OIDC 를 콘솔에서 켜지 않았으면 Firebase 가 `operation-not-allowed`(E-AUTH-11)를
|
||||
/// 돌려준다.
|
||||
static List<SocialAuthProvider> _defaultProviders() => [
|
||||
const GoogleSignInProvider(),
|
||||
AppleSignInProvider(
|
||||
webAuthenticationOptions: appleWebAuthenticationOptionsFromEnvironment(),
|
||||
),
|
||||
const KeylessSocialProvider(AuthProviderKind.kakao, [
|
||||
'카카오 네이티브 앱 키',
|
||||
'카카오 개발자 콘솔 앱 등록(사업자 정보 필요)',
|
||||
]),
|
||||
lineProvider,
|
||||
// 동의 항목은 카카오 콘솔 설정을 따른다(지금은 닉네임). 이메일(account_email)은 비즈 앱
|
||||
// 전환 후 콘솔에서 켜면 서버가 동의·인증된 값만 받는다.
|
||||
KakaoSignInProvider(),
|
||||
// LINE 동의 항목: profile·email(이메일 권한 신청 승인 후).
|
||||
OidcSignInProvider(
|
||||
AuthProviderKind.line,
|
||||
providerId: FirebaseAccountLinker.providerIds[AuthProviderKind.line]!,
|
||||
scopes: const ['profile', 'email'],
|
||||
// LINE 앱 자동 로그인으로 넘어가면 콜백이 Custom Tab 밖에서 열려 로그인이
|
||||
// 깨진다(Android). LINE Login 공식 파라미터로 끈다.
|
||||
androidParameters: const {'disable_auto_login': 'true'},
|
||||
),
|
||||
];
|
||||
|
||||
/// QA 계정(qa-test@fanuzu.co.kr) 전용 이메일 로그인 — **디버그 빌드에서만**.
|
||||
|
||||
@@ -26,7 +26,8 @@ final class FirebaseAccountLinker implements AccountLinker {
|
||||
AuthProviderKind.google: 'google.com',
|
||||
AuthProviderKind.apple: 'apple.com',
|
||||
// 카카오·LINE 은 Firebase 기본 제공자가 아니라 OIDC 커스텀 제공자로 붙는다.
|
||||
// 접두사 `oidc.` 는 Firebase 콘솔 등록 시 강제되는 형식이다.
|
||||
// 접두사 `oidc.` 는 Firebase 콘솔 등록 시 강제되는 형식이다. 카카오는 로그인을
|
||||
// 서버 커스텀 토큰으로 옮겼지만, 서버(fnAuthKakao)가 같은 `oidc.kakao` 로 연결을 남긴다.
|
||||
AuthProviderKind.kakao: 'oidc.kakao',
|
||||
AuthProviderKind.line: 'oidc.line',
|
||||
AuthProviderKind.email: 'password',
|
||||
@@ -55,13 +56,16 @@ final class FirebaseAccountLinker implements AccountLinker {
|
||||
}
|
||||
final missing = impl.missingConfig;
|
||||
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
|
||||
// 키 대기 제공자(카카오·LINE)는 자격증명을 만들 수 없다.
|
||||
if (impl is! CredentialProvider) {
|
||||
if (impl is! CredentialProvider &&
|
||||
impl is! OidcSignInProvider &&
|
||||
impl is! KakaoSignInProvider) {
|
||||
return AuthNotConfigured(provider, const ['자격증명 구현']);
|
||||
}
|
||||
final source = impl as CredentialProvider;
|
||||
|
||||
try {
|
||||
if (impl is KakaoSignInProvider) return await impl.link(user);
|
||||
if (impl is OidcSignInProvider) return await _linkOidc(user, impl);
|
||||
final source = impl as CredentialProvider;
|
||||
final obtained = await source.obtainCredential();
|
||||
// 사용자가 소셜 창을 닫았다. 실패로 세지 않는다.
|
||||
if (obtained == null) return const AuthCancelled();
|
||||
@@ -83,6 +87,7 @@ final class FirebaseAccountLinker implements AccountLinker {
|
||||
final result = await user.linkWithCredential(obtained.credential);
|
||||
return AuthSucceeded(_accountOf(result.user, provider));
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
if (isWebFlowCancelled(error)) return const AuthCancelled();
|
||||
return AuthFailed(
|
||||
authCatalogCode(error),
|
||||
authMessage(error, flow: AuthFlow.link),
|
||||
@@ -120,6 +125,33 @@ 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 untilAbandoned(
|
||||
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 const AuthFailed('E-AUTH-08', '가입하신 이메일과 다른 계정입니다.');
|
||||
}
|
||||
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) {
|
||||
for (final candidate in _providers) {
|
||||
if (candidate.provider == provider) return candidate;
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cloud_functions/cloud_functions.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart' as fb;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart' as gsi;
|
||||
import 'package:kakao_flutter_sdk_user/kakao_flutter_sdk_user.dart' as kakao;
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart' as apple;
|
||||
|
||||
import '../../core/data/backend.dart';
|
||||
import '../../core/firebase/firebase_boot.dart';
|
||||
import 'auth.dart';
|
||||
|
||||
@@ -198,29 +204,278 @@ final class AppleSignInProvider
|
||||
kIsWeb || defaultTargetPlatform == TargetPlatform.android;
|
||||
}
|
||||
|
||||
/// 발주사 키를 기다리는 제공자 — 카카오·LINE.
|
||||
/// LINE — Firebase Auth 의 OpenID Connect 제공자(`oidc.line`). 카카오도 이 경로였으나
|
||||
/// Android 앱 전환 문제로 [KakaoSignInProvider] 로 옮겼다(클래스는 제공자 무관이라 그대로 둔다).
|
||||
///
|
||||
/// 예외를 던지지 않는다. [AuthNotConfigured] 로 "무엇이 없어서 못 쓰는지"를 돌려주고
|
||||
/// 로그인 화면이 그걸 안내 다이얼로그로 띄운다. 키가 들어오면 이 자리에 실 구현체를
|
||||
/// 넣고 [AuthRepository] 목록만 바꾸면 된다 — 화면·라우터는 그대로다.
|
||||
final class KeylessSocialProvider implements SocialAuthProvider {
|
||||
const KeylessSocialProvider(this.provider, this.missingConfig);
|
||||
/// 두 회사 모두 OIDC 표준(issuer `https://kauth.kakao.com` · `https://access.line.me`)을
|
||||
/// 지원해 Firebase 가 코드 교환·ID 토큰 검증까지 맡는다. 앱은 SDK 없이
|
||||
/// `signInWithProvider` 하나로 끝난다 — Android 는 Custom Tabs, iOS 는
|
||||
/// SFSafariViewController 로 인가 화면을 띄우고 Firebase 핸들러
|
||||
/// (`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.androidParameters = const {},
|
||||
this.firebaseAuth,
|
||||
});
|
||||
|
||||
@override
|
||||
final SocialProvider provider;
|
||||
|
||||
@override
|
||||
final List<String> missingConfig;
|
||||
/// Firebase 콘솔에 등록한 제공자 ID. `oidc.` 접두는 콘솔이 강제한다.
|
||||
final String providerId;
|
||||
|
||||
/// `openid` 외에 더 받을 항목. Firebase 가 `openid` 는 알아서 붙인다.
|
||||
/// **콘솔에 켠 동의 항목과 맞아야 한다** — 카카오는 설정에 없는 scope 를 요청하면
|
||||
/// KOE205 로 로그인 자체를 막는다.
|
||||
final List<String> scopes;
|
||||
|
||||
/// Android 에서만 인가 URL 에 붙일 파라미터. Android Custom Tab 은 다른 앱으로
|
||||
/// 넘어갔다 돌아오면 콜백이 일반 Chrome 탭에서 열려 Firebase 핸들러가
|
||||
/// 「missing initial state」로 끝난다 — 앱 전환을 막는 공식 파라미터를 여기 둔다.
|
||||
/// iOS 는 정상이라 건드리지 않는다.
|
||||
final Map<String, String> androidParameters;
|
||||
|
||||
/// 테스트 주입용. 기본은 [fb.FirebaseAuth.instance].
|
||||
final fb.FirebaseAuth? firebaseAuth;
|
||||
|
||||
@override
|
||||
bool get isLive => false;
|
||||
bool get isLive => true;
|
||||
|
||||
@override
|
||||
Future<AuthOutcome> signIn() async =>
|
||||
AuthNotConfigured(provider, missingConfig);
|
||||
List<String> get missingConfig =>
|
||||
FirebaseBoot.isLive ? const [] : FirebaseBoot.missingLabels;
|
||||
|
||||
fb.OAuthProvider get authProvider {
|
||||
final p = fb.OAuthProvider(providerId);
|
||||
for (final scope in scopes) {
|
||||
p.addScope(scope);
|
||||
}
|
||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||
p.setCustomParameters(androidParameters);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signOut() async {}
|
||||
Future<AuthOutcome> signIn() async {
|
||||
final missing = missingConfig;
|
||||
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
|
||||
try {
|
||||
final result = await untilAbandoned(
|
||||
(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 이 닫는다.
|
||||
}
|
||||
}
|
||||
|
||||
/// 카카오 네이티브 앱 키. `config/kakao.env` 를 `--dart-define-from-file` 로 넘긴다
|
||||
/// (같은 파일을 Android manifestPlaceholders · iOS xcconfig 가 읽는다).
|
||||
const kakaoNativeAppKey = String.fromEnvironment('FANUZU_KAKAO_NATIVE_APP_KEY');
|
||||
|
||||
/// 카카오 — 공식 SDK 로그인 + 서버 커스텀 토큰(`fnAuthKakao`). iOS·Android 같은 경로다.
|
||||
///
|
||||
/// Firebase OIDC(`oidc.kakao`) 웹 흐름은 Android 에서 「카카오톡으로 로그인」을 고르면
|
||||
/// 콜백이 Custom Tab 밖(일반 Chrome)에서 열려 「missing initial state」로 깨진다. 카카오에는
|
||||
/// 앱 전환을 막는 파라미터가 없어서, SDK 가 카카오톡(없으면 카카오계정)으로 로그인해 받은
|
||||
/// 액세스 토큰을 서버가 카카오 API 로 검증하고 커스텀 토큰을 내준다.
|
||||
///
|
||||
/// 서버는 사용자 제공자 정보에 `oidc.kakao` 를 그대로 남긴다 — OIDC 시절 가입자는 같은 UID 로
|
||||
/// 들어오고, [FirebaseAccountLinker.linkedAccounts] 도 지금처럼 「연결됨」을 그린다.
|
||||
final class KakaoSignInProvider implements SocialAuthProvider {
|
||||
KakaoSignInProvider({
|
||||
Future<String> Function()? accessToken,
|
||||
Future<Map<String, dynamic>> Function(Map<String, dynamic> payload)?
|
||||
exchange,
|
||||
this.firebaseAuth,
|
||||
this.nativeAppKey = kakaoNativeAppKey,
|
||||
}) : _accessToken = accessToken ?? _sdkAccessToken,
|
||||
_exchange = exchange ?? _callServer;
|
||||
|
||||
final Future<String> Function() _accessToken;
|
||||
final Future<Map<String, dynamic>> Function(Map<String, dynamic>) _exchange;
|
||||
|
||||
/// 테스트 주입용. 기본은 [fb.FirebaseAuth.instance].
|
||||
final fb.FirebaseAuth? firebaseAuth;
|
||||
final String nativeAppKey;
|
||||
|
||||
static Future<void>? _init;
|
||||
|
||||
@override
|
||||
SocialProvider get provider => SocialProvider.kakao;
|
||||
|
||||
@override
|
||||
bool get isLive => true;
|
||||
|
||||
@override
|
||||
List<String> get missingConfig => [
|
||||
if (!FirebaseBoot.isLive) ...FirebaseBoot.missingLabels,
|
||||
if (nativeAppKey.isEmpty) '카카오 네이티브 앱 키',
|
||||
];
|
||||
|
||||
fb.FirebaseAuth get _auth => firebaseAuth ?? fb.FirebaseAuth.instance;
|
||||
|
||||
/// 카카오톡이 있으면 카카오톡, 없거나 실패하면 카카오계정(공식 가이드 순서).
|
||||
/// 사용자 취소(`CANCELED`)는 계정 로그인으로 넘기지 않고 그대로 던진다.
|
||||
static Future<String> _sdkAccessToken() async {
|
||||
await (_init ??= kakao.KakaoSdk.init(nativeAppKey: kakaoNativeAppKey));
|
||||
final api = kakao.UserApi.instance;
|
||||
if (await kakao.isKakaoTalkInstalled()) {
|
||||
try {
|
||||
return (await api.loginWithKakaoTalk()).accessToken;
|
||||
} on PlatformException catch (error) {
|
||||
if (error.code == 'CANCELED') rethrow;
|
||||
}
|
||||
}
|
||||
return (await api.loginWithKakaoAccount()).accessToken;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> _callServer(
|
||||
Map<String, dynamic> payload,
|
||||
) async {
|
||||
final result = await FirebaseFunctions.instanceFor(
|
||||
region: Backend.region,
|
||||
).httpsCallable('fnAuthKakao').call<Object?>(payload);
|
||||
final data = result.data;
|
||||
return data is Map ? Map<String, dynamic>.from(data) : const {};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthOutcome> signIn() => _run(AuthFlow.signIn, () async {
|
||||
final data = await _exchange({'accessToken': await _accessToken()});
|
||||
final result = await _auth.signInWithCustomToken(data['token'] as String);
|
||||
return AuthSucceeded(_toAccount(result.user, provider));
|
||||
});
|
||||
|
||||
/// SNS 연동 — 로그인한 UID 에 카카오를 붙인다. 「동일 이메일만 연동」은 서버가 붙이기 전에 본다.
|
||||
Future<AuthOutcome> link(fb.User user) => _run(AuthFlow.link, () async {
|
||||
await _exchange({'accessToken': await _accessToken(), 'link': true});
|
||||
await user.reload();
|
||||
return AuthSucceeded(_toAccount(_auth.currentUser ?? user, provider));
|
||||
});
|
||||
|
||||
Future<AuthOutcome> _run(
|
||||
AuthFlow flow,
|
||||
Future<AuthOutcome> Function() body,
|
||||
) async {
|
||||
final missing = missingConfig;
|
||||
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
|
||||
try {
|
||||
return await body();
|
||||
} on PlatformException catch (error) {
|
||||
if (error.code == 'CANCELED') return const AuthCancelled();
|
||||
debugPrint('[KakaoSignInProvider] sdk failed: ${error.code}');
|
||||
return const AuthFailed('E-AUTH-03', '소셜 로그인에 실패했습니다.');
|
||||
} on kakao.KakaoAuthException catch (error) {
|
||||
// 동의 화면에서 「취소」.
|
||||
if (error.error == kakao.AuthErrorCause.accessDenied) {
|
||||
return const AuthCancelled();
|
||||
}
|
||||
return const AuthFailed('E-AUTH-03', '소셜 로그인에 실패했습니다.');
|
||||
} on FirebaseFunctionsException catch (error) {
|
||||
final details = error.details;
|
||||
final code = details is Map && details['code'] is String
|
||||
? details['code'] as String
|
||||
: 'E-AUTH-03';
|
||||
return AuthFailed(code, switch (code) {
|
||||
_ when details is Map && details['reason'] == 'different-email' =>
|
||||
'가입하신 이메일과 다른 계정입니다.',
|
||||
'E-AUTH-08' => '다른 FANUZU 계정에 이미 연결된 소셜 계정입니다.',
|
||||
'E-AUTH-09' => '이미 연결된 계정입니다.',
|
||||
_ =>
|
||||
flow == AuthFlow.link
|
||||
? '계정 연결에 실패했습니다.'
|
||||
: '소셜 로그인에 실패했습니다.',
|
||||
});
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
return AuthFailed(authCatalogCode(error), authMessage(error, flow: flow));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
// SDK 를 한 번도 띄우지 않았으면 지울 카카오 토큰도 없다.
|
||||
if (_init == null) return;
|
||||
try {
|
||||
await kakao.UserApi.instance.logout();
|
||||
} on Object catch (error) {
|
||||
// 토큰이 이미 만료·폐기됐어도 SDK 는 로컬 토큰을 지운다. 로그아웃을 막을 이유가 없다.
|
||||
debugPrint('[KakaoSignInProvider] logout: ${error.runtimeType}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자가 인가 웹 화면을 닫았다. Android 는 `canceled`, iOS 는 `cancelled` 철자다.
|
||||
bool isWebFlowCancelled(fb.FirebaseAuthException error) =>
|
||||
error.code == 'web-context-canceled' ||
|
||||
error.code == 'web-context-cancelled';
|
||||
|
||||
/// Android 에서 웹 인가 흐름이 결과 없이 버려졌으면 취소(`web-context-canceled`)로 끝낸다.
|
||||
///
|
||||
/// 콜백이 Custom Tab 밖(일반 Chrome)에서 열리면 Firebase 의 대기 Task 가 영영
|
||||
/// 끝나지 않아 로그인 버튼이 잠긴다. 앱이 포그라운드로 돌아온 뒤 [grace] 안에
|
||||
/// 결과가 없으면 사용자가 흐름을 버린 것으로 보고 기존 취소 경로([isWebFlowCancelled])로
|
||||
/// 넘긴다. 정상 완료는 돌아온 직후 토큰 교환 한 번이라 [grace] 안에 끝난다.
|
||||
/// iOS 는 SFSafariViewController 가 취소를 스스로 알려 주므로 그대로 둔다.
|
||||
///
|
||||
/// ponytail: 고정 대기 시간. 망이 아주 느려 [grace] 를 넘기면 취소로 보이지만
|
||||
/// Firebase 세션은 나중에 열리므로 authStateChanges 가 따라잡는다.
|
||||
Future<T> untilAbandoned<T>(
|
||||
Future<T> flow, {
|
||||
Duration grace = const Duration(seconds: 8),
|
||||
}) {
|
||||
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) return flow;
|
||||
final done = Completer<T>();
|
||||
Timer? timer;
|
||||
final listener = AppLifecycleListener(
|
||||
onStateChange: (state) {
|
||||
timer?.cancel();
|
||||
if (state != AppLifecycleState.resumed) return;
|
||||
timer = Timer(grace, () {
|
||||
if (done.isCompleted) return;
|
||||
done.completeError(
|
||||
fb.FirebaseAuthException(code: 'web-context-canceled'),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
flow.then(
|
||||
(value) {
|
||||
if (!done.isCompleted) done.complete(value);
|
||||
},
|
||||
onError: (Object error, StackTrace stack) {
|
||||
if (!done.isCompleted) done.completeError(error, stack);
|
||||
},
|
||||
);
|
||||
return done.future.whenComplete(() {
|
||||
timer?.cancel();
|
||||
listener.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
/// Apple Service ID와 callback URL은 비밀값이 아니지만 환경별 값이다.
|
||||
@@ -336,14 +591,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 로 옮긴다.
|
||||
///
|
||||
/// **계정 존재 여부를 드러내지 않는다.** 「등록되지 않은 이메일」과 「비밀번호
|
||||
|
||||
Reference in New Issue
Block a user