allscore_app/lib/dialogs/signup_dialog.dart

398 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import '../views/login/signup_page.dart';
/* 구글 로그인 */
import 'package:google_sign_in/google_sign_in.dart';
import 'package:firebase_auth/firebase_auth.dart';
/* 애플 로그인 */
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'dart:io' show Platform;
import 'dart:math';
import 'dart:convert';
import 'package:crypto/crypto.dart';
/* 안내 모달창 */
import 'response_dialog.dart';
/* 우리의 Api 모듈 */
import '../plugins/api.dart';
/* 설정 */
import '../config/config.dart';
// 회원가입 방법 선택 모달 위젯
class SignUpDialog extends StatefulWidget {
const SignUpDialog({Key? key}) : super(key: key);
@override
State<SignUpDialog> createState() => _SignUpDialogState();
}
class _SignUpDialogState extends State<SignUpDialog> {
// 구글 로그인 객체
final GoogleSignIn _googleSignIn = GoogleSignIn(scopes: <String>['email']);
// 로딩 상태 관리
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
padding: const EdgeInsets.all(20),
width: 300,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 제목
const Text(
'Sign Up Method',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 24),
// 이메일 회원가입 버튼
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {
Navigator.pop(context);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const SignUpPage()),
(route) => false,
);
},
child: const Text(
'Sign up with Email',
style: TextStyle(fontSize: 16),
),
),
),
const SizedBox(height: 12),
// 구글 회원가입 버튼
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/icons8-google-logo-48.png'),
fit: BoxFit.contain,
),
),
),
label: const Text(
'Sign up with Google',
style: TextStyle(fontSize: 16),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: _googleSignUp,
),
),
const SizedBox(height: 12),
// 애플 회원가입 버튼 - iOS에서만 표시
if (!Platform.isAndroid)
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(
Icons.apple,
size: 24,
color: Colors.black,
),
label: const Text(
'Sign up with Apple',
style: TextStyle(fontSize: 16),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {
_appleSignUp();
},
),
),
],
),
),
),
// 로딩 인디케이터
if (_isLoading)
Container(
color: Colors.black54,
alignment: Alignment.center,
child: const CircularProgressIndicator(color: Colors.white),
),
],
);
}
// ─────────────────────────────────────────
// (D3) 구글 회원가입
// ─────────────────────────────────────────
Future<void> _googleSignUp() async {
setState(() => _isLoading = true);
final agreed = await _showTermsModal();
if (agreed != true) {
setState(() => _isLoading = false);
return;
}
try {
// (2) 구글 계정 선택
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) {
// 사용자가 회원가입 창에서 취소
return;
}
// (3) 구글 인증 정보
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
// (4) FirebaseAuth Credential
final AuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// (5) FirebaseAuth로 "회원가입" 시도
final UserCredential userCredential = await FirebaseAuth.instance.signInWithCredential(credential);
final User? user = userCredential.user;
if (user == null) {
showResponseDialog(context, 'Error' /* 오류 */, 'Google account authentication failed.'
/* 구글계정 인증에 실패했습니다. */);
return;
}
// (6) idToken 추출 후, 서버에 회원가입 요청
final idToken = await user.getIdToken();
final requestBody = {
'id_token': idToken,
};
final response = await Api.serverRequest(uri: '/user/google/signup', body: requestBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
// 회원가입 성공 안내
showResponseDialog(context, 'Sign-up Complete' /* 회원가입 완료 */, 'Google sign-up has been completed.'
/* 구글 회원가입이 완료되었습니다. */);
} else {
// 실패 시
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error' /* 오류 */;
final msgContent = resp['response_info']?['msg_content'] ?? 'Failed to sign up.'
/* 회원가입에 실패했습니다. */;
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, 'Error' /* 오류 */, 'Google sign-up request failed.'
/* 구글 회원가입 요청 실패 */);
}
} catch (e) {
showResponseDialog(context, 'Error' /* 오류 */, 'An error occurred during Google sign-up.\n$e'
/* 구글 회원가입 중 오류가 발생했습니다.\n$e */);
} finally {
setState(() => _isLoading = false);
}
}
// ─────────────────────────────────────────
// (D4) 애플 회원가입
// ─────────────────────────────────────────
Future<void> _appleSignUp() async {
// 안드로이드 기기 체크
if (Platform.isAndroid) {
showResponseDialog(
context,
'사용 불가', // 제목
'애플 회원가입은 안드로이드 기기에서 제공되지 않습니다.', // 내용
);
return;
}
setState(() => _isLoading = true);
final agreed = await _showTermsModal();
if (agreed != true) {
setState(() => _isLoading = false);
return;
}
try {
// 1. 애플 로그인 credential 획득
final rawNonce = generateNonce();
final nonce = sha256ofString(rawNonce);
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
print('appleCredential: $appleCredential');
// 2. Firebase credential 생성
final oauthProvider = OAuthProvider('apple.com');
print('oauthProvider: $oauthProvider');
final credential = oauthProvider.credential(
idToken: appleCredential.identityToken,
accessToken: appleCredential.authorizationCode,
);
print('credential: $credential');
// 3. Firebase 인증
final userCredential = await FirebaseAuth.instance.signInWithCredential(credential);
print('userCredential: $userCredential');
final user = userCredential.user;
print('user: $user');
if (user == null) {
showResponseDialog(context, 'Error', 'Apple account authentication failed.');
return;
}
// 4. 서버에 회원가입 요청
final idToken = await user.getIdToken();
final requestBody = {
'id_token': idToken,
};
final response = await Api.serverRequest(uri: '/user/apple/signup', body: requestBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
showResponseDialog(
context,
'Sign-up Complete',
'Apple sign-up has been completed.'
);
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error';
final msgContent = resp['response_info']?['msg_content'] ?? 'Failed to sign up.';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(
context,
'Error',
'Apple sign-up request failed.'
);
}
} catch (e) {
showResponseDialog(
context,
'Error',
'An error occurred during Apple sign-up.\n$e'
);
} finally {
setState(() => _isLoading = false);
}
}
// nonce 생성 유틸리티 함수들 추가
String generateNonce([int length = 32]) {
const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
final random = Random.secure();
return List.generate(length, (_) => charset[random.nextInt(charset.length)]).join();
}
String sha256ofString(String input) {
final bytes = utf8.encode(input);
final digest = sha256.convert(bytes);
return digest.toString();
}
// ─────────────────────────────────────────
// (E) 약관 모달 (개인정보 수집 동의)
// ─────────────────────────────────────────
Future<bool?> _showTermsModal() async {
return showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (ctx) {
return AlertDialog(
backgroundColor: Colors.white,
title: const Text(
'Privacy Collection and Usage Agreement'
/* 개인정보 수집 및 이용 동의서 */,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
Config.termsOfService,
style: const TextStyle(fontSize: 14),
),
],
),
),
actions: [
TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(ctx, false),
child: Text('Disagree'
/* 거부 */),
),
TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(ctx, true),
child: Text('Agree'
/* 동의 */),
),
],
);
},
);
}
}