152 lines
5.4 KiB
Dart
152 lines
5.4 KiB
Dart
import 'package:firebase_auth/firebase_auth.dart' as fb;
|
|
import 'package:google_sign_in/google_sign_in.dart' as gsi;
|
|
import 'package:sign_in_with_apple/sign_in_with_apple.dart' as apple;
|
|
|
|
import '../../core/firebase/firebase_boot.dart';
|
|
import 'auth.dart';
|
|
|
|
/// Google 로그인 — 실 코드 경로.
|
|
///
|
|
/// Firebase 설정 파일이 없으면 SDK 를 부르기 전에 [AuthNotConfigured] 로 끊는다.
|
|
/// 파일이 투입되면(S2) 이 클래스는 수정 없이 그대로 동작한다.
|
|
final class GoogleSignInProvider implements SocialAuthProvider {
|
|
const GoogleSignInProvider();
|
|
|
|
@override
|
|
SocialProvider get provider => SocialProvider.google;
|
|
|
|
@override
|
|
bool get isLive => true;
|
|
|
|
// 화면에 그대로 노출되는 목록이라 리포 경로가 아니라 사람이 읽는 이름을 쓴다.
|
|
// 실제 파일 경로는 FirebaseBoot.detail 로 로그에만 남는다.
|
|
@override
|
|
List<String> get missingConfig =>
|
|
FirebaseBoot.isLive ? const [] : FirebaseBoot.missingLabels;
|
|
|
|
@override
|
|
Future<AuthOutcome> signIn() async {
|
|
final missing = missingConfig;
|
|
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
|
|
|
|
try {
|
|
final signIn = gsi.GoogleSignIn.instance;
|
|
// clientId/serverClientId 는 설정 파일에서 읽힌다 — 여기서 하드코딩하지 않는다.
|
|
await signIn.initialize();
|
|
final account = await signIn.authenticate();
|
|
final idToken = account.authentication.idToken;
|
|
if (idToken == null) {
|
|
return const AuthFailed('E-AUTH-03', '소셜 로그인에 실패했습니다.');
|
|
}
|
|
final credential = fb.GoogleAuthProvider.credential(idToken: idToken);
|
|
final result = await fb.FirebaseAuth.instance.signInWithCredential(credential);
|
|
return AuthSucceeded(_toAccount(result.user, provider));
|
|
} on gsi.GoogleSignInException catch (error) {
|
|
if (error.code == gsi.GoogleSignInExceptionCode.canceled) {
|
|
return const AuthCancelled();
|
|
}
|
|
return AuthFailed('E-AUTH-03', error.description ?? error.code.name);
|
|
} on fb.FirebaseAuthException catch (error) {
|
|
return AuthFailed('E-AUTH-03', error.message ?? error.code);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> signOut() async {
|
|
if (!FirebaseBoot.isLive) return;
|
|
await gsi.GoogleSignIn.instance.signOut();
|
|
await fb.FirebaseAuth.instance.signOut();
|
|
}
|
|
}
|
|
|
|
/// Apple 로그인 — 실 코드 경로.
|
|
///
|
|
/// iOS 는 Xcode 에서 「Sign in with Apple」 Capability 를, Android·Web 은
|
|
/// Apple Developer 콘솔의 Service ID + redirect URI 를 각각 요구한다.
|
|
/// 둘 다 스토어 계정 명의가 정해져야(Q5) 발급할 수 있어 지금은 설정 파일 게이트에서 끊긴다.
|
|
final class AppleSignInProvider implements SocialAuthProvider {
|
|
const AppleSignInProvider();
|
|
|
|
@override
|
|
SocialProvider get provider => SocialProvider.apple;
|
|
|
|
@override
|
|
bool get isLive => true;
|
|
|
|
@override
|
|
List<String> get missingConfig => FirebaseBoot.isLive
|
|
? const []
|
|
: [...FirebaseBoot.missingLabels, 'Apple 개발자 설정'];
|
|
|
|
@override
|
|
Future<AuthOutcome> signIn() async {
|
|
final missing = missingConfig;
|
|
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
|
|
|
|
if (!await apple.SignInWithApple.isAvailable()) {
|
|
return const AuthFailed('E-AUTH-03', '이 기기에서는 Apple 로그인을 쓸 수 없습니다.');
|
|
}
|
|
try {
|
|
final credential = await apple.SignInWithApple.getAppleIDCredential(
|
|
scopes: const [
|
|
apple.AppleIDAuthorizationScopes.email,
|
|
apple.AppleIDAuthorizationScopes.fullName,
|
|
],
|
|
);
|
|
final idToken = credential.identityToken;
|
|
if (idToken == null) {
|
|
return const AuthFailed('E-AUTH-03', '소셜 로그인에 실패했습니다.');
|
|
}
|
|
final oauth = fb.OAuthProvider('apple.com').credential(
|
|
idToken: idToken,
|
|
accessToken: credential.authorizationCode,
|
|
);
|
|
final result = await fb.FirebaseAuth.instance.signInWithCredential(oauth);
|
|
return AuthSucceeded(_toAccount(result.user, provider));
|
|
} on apple.SignInWithAppleAuthorizationException catch (error) {
|
|
if (error.code == apple.AuthorizationErrorCode.canceled) {
|
|
return const AuthCancelled();
|
|
}
|
|
return AuthFailed('E-AUTH-03', error.message);
|
|
} on fb.FirebaseAuthException catch (error) {
|
|
return AuthFailed('E-AUTH-03', error.message ?? error.code);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> signOut() async {
|
|
if (!FirebaseBoot.isLive) return;
|
|
await fb.FirebaseAuth.instance.signOut();
|
|
}
|
|
}
|
|
|
|
/// 앱 키가 없어 아직 붙이지 못한 제공자 — 카카오·네이버.
|
|
///
|
|
/// 예외를 던지지 않는다. [AuthNotConfigured] 로 "무엇이 없어서 못 쓰는지"를 돌려준다.
|
|
/// 키가 들어오면 이 자리에 실 구현체를 넣고 [AuthRepository] 목록만 바꾸면 된다.
|
|
final class KeylessSocialProvider implements SocialAuthProvider {
|
|
const KeylessSocialProvider(this.provider, this.missingConfig);
|
|
|
|
@override
|
|
final SocialProvider provider;
|
|
|
|
@override
|
|
final List<String> missingConfig;
|
|
|
|
@override
|
|
bool get isLive => false;
|
|
|
|
@override
|
|
Future<AuthOutcome> signIn() async => AuthNotConfigured(provider, missingConfig);
|
|
|
|
@override
|
|
Future<void> signOut() async {}
|
|
}
|
|
|
|
AuthAccount _toAccount(fb.User? user, SocialProvider provider) => AuthAccount(
|
|
uid: user?.uid ?? '',
|
|
provider: provider,
|
|
displayName: user?.displayName,
|
|
email: user?.email,
|
|
);
|