소셜 로그인 인증 경로 · 세션 전이 · Apple 로그인 콜백 · 인증 테스트
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../core/firebase/firebase_boot.dart';
|
||||
@@ -6,13 +8,22 @@ import 'social_providers.dart';
|
||||
|
||||
/// 로그인 상태 보관소. 라우터가 이걸 듣고 로그인/셸을 가른다.
|
||||
class AuthRepository extends ChangeNotifier {
|
||||
AuthRepository({List<SocialAuthProvider>? providers, bool? releaseMode})
|
||||
: providers = providers ?? _defaultProviders(),
|
||||
// 기본값은 실제 빌드 모드. 테스트에서만 주입해 릴리스 동작을 재현한다
|
||||
// (kReleaseMode 는 컴파일 상수라 테스트에서 뒤집을 수 없다).
|
||||
_releaseMode = releaseMode ?? kReleaseMode;
|
||||
AuthRepository({
|
||||
List<SocialAuthProvider>? providers,
|
||||
AuthSession? session,
|
||||
bool? releaseMode,
|
||||
}) : providers = providers ?? _defaultProviders(),
|
||||
_session = session,
|
||||
_account = session?.currentAccount,
|
||||
// 기본값은 실제 빌드 모드. 테스트에서만 주입해 릴리스 동작을 재현한다
|
||||
// (kReleaseMode 는 컴파일 상수라 테스트에서 뒤집을 수 없다).
|
||||
_releaseMode = releaseMode ?? kReleaseMode {
|
||||
_sessionSubscription = session?.accountChanges.listen(_replaceAccount);
|
||||
}
|
||||
|
||||
final bool _releaseMode;
|
||||
final AuthSession? _session;
|
||||
StreamSubscription<AuthAccount?>? _sessionSubscription;
|
||||
|
||||
/// 로그인 화면에 그릴 제공자 목록. 순서는 시안 S-A-LOGIN 버튼 순서다.
|
||||
final List<SocialAuthProvider> providers;
|
||||
@@ -21,27 +32,38 @@ class AuthRepository extends ChangeNotifier {
|
||||
AuthAccount? get account => _account;
|
||||
bool get isSignedIn => _account != null;
|
||||
|
||||
static List<SocialAuthProvider> _defaultProviders() => const [
|
||||
KeylessSocialProvider(SocialProvider.kakao, [
|
||||
static List<SocialAuthProvider> _defaultProviders() => [
|
||||
const KeylessSocialProvider(SocialProvider.kakao, [
|
||||
'카카오 네이티브 앱 키',
|
||||
'카카오 개발자 콘솔 앱 등록(사업자 정보 필요)',
|
||||
]),
|
||||
KeylessSocialProvider(SocialProvider.naver, [
|
||||
const KeylessSocialProvider(SocialProvider.naver, [
|
||||
'네이버 client ID / client secret',
|
||||
'네이버 개발자센터 앱 등록(사업자 정보 필요)',
|
||||
]),
|
||||
AppleSignInProvider(),
|
||||
GoogleSignInProvider(),
|
||||
AppleSignInProvider(
|
||||
webAuthenticationOptions: appleWebAuthenticationOptionsFromEnvironment(),
|
||||
),
|
||||
const GoogleSignInProvider(),
|
||||
// 이메일 가입 구현체는 여기 없다. CD-18 미확정(Q2) — 회신이 오면 이 목록에 추가한다.
|
||||
// 자세한 사유는 auth.dart 상단 「이메일 가입에 대하여」 참고.
|
||||
];
|
||||
|
||||
Future<AuthOutcome> signIn(SocialProvider provider) async {
|
||||
final impl = providers.firstWhere((p) => p.provider == provider);
|
||||
final outcome = await impl.signIn();
|
||||
late final AuthOutcome outcome;
|
||||
try {
|
||||
outcome = await impl.signIn();
|
||||
} on Object catch (error) {
|
||||
// SDK가 문서화하지 않은 플랫폼 예외를 던져도 로그인 화면이 busy 상태에
|
||||
// 고착되지 않게 사용자용 결과로 닫는다. 원문 예외·토큰은 화면에 노출하지 않는다.
|
||||
debugPrint(
|
||||
'[AuthRepository] ${provider.name} sign-in failed: ${error.runtimeType}',
|
||||
);
|
||||
outcome = const AuthFailed('E-AUTH-03', '소셜 로그인에 실패했습니다.');
|
||||
}
|
||||
if (outcome is AuthSucceeded) {
|
||||
_account = outcome.account;
|
||||
notifyListeners();
|
||||
_replaceAccount(outcome.account);
|
||||
}
|
||||
return outcome;
|
||||
}
|
||||
@@ -71,10 +93,49 @@ class AuthRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> signOut() async {
|
||||
for (final p in providers) {
|
||||
await p.signOut();
|
||||
final currentProvider = _account?.provider;
|
||||
SocialAuthProvider? provider;
|
||||
if (currentProvider != null) {
|
||||
for (final candidate in providers) {
|
||||
if (candidate.provider == currentProvider) {
|
||||
provider = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_account = null;
|
||||
|
||||
try {
|
||||
await Future.wait([
|
||||
if (provider != null) provider.signOut(),
|
||||
if (_session != null) _session.signOut(),
|
||||
], eagerError: false);
|
||||
} finally {
|
||||
// 원격 SDK 정리가 실패해도 로컬 라우터는 로그인 상태에 고착되지 않는다.
|
||||
_replaceAccount(null);
|
||||
}
|
||||
}
|
||||
|
||||
void _replaceAccount(AuthAccount? next) {
|
||||
if (_sameAccount(_account, next)) return;
|
||||
_account = next;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool _sameAccount(AuthAccount? a, AuthAccount? b) =>
|
||||
identical(a, b) ||
|
||||
(a != null &&
|
||||
b != null &&
|
||||
a.uid == b.uid &&
|
||||
a.provider == b.provider &&
|
||||
a.displayName == b.displayName &&
|
||||
a.email == b.email &&
|
||||
a.isMock == b.isMock);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final subscription = _sessionSubscription;
|
||||
_sessionSubscription = null;
|
||||
if (subscription != null) unawaited(subscription.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user