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 createState() => _SignUpDialogState(); } class _SignUpDialogState extends State { // 구글 로그인 객체 final GoogleSignIn _googleSignIn = GoogleSignIn(scopes: ['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 _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 _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, ], ); // 2. Firebase credential 생성 final oauthProvider = OAuthProvider('apple.com'); final credential = oauthProvider.credential( idToken: appleCredential.identityToken, accessToken: appleCredential.authorizationCode, ); // 3. Firebase 인증 final userCredential = await FirebaseAuth.instance.signInWithCredential(credential); final user = userCredential.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 _showTermsModal() async { return showDialog( 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' /* 동의 */), ), ], ); }, ); } }