diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b7504a3..f28dbc4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,4 +13,6 @@ close # ## ๐Ÿ’ฌ๋ฆฌ๋ทฐ ์š”๊ตฌ์‚ฌํ•ญ + ## ๋‹ค์Œ ์ž‘์—… ๊ณ„ํš + diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a0a7822..f958fb1 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -4,7 +4,6 @@ plugins { id("com.android.application") id("kotlin-android") id("com.google.gms.google-services") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } @@ -53,3 +52,4 @@ android { flutter { source = "../.." } + diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 89176ef..6d09106 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -19,3 +19,14 @@ subprojects { tasks.register("clean") { delete(rootProject.layout.buildDirectory) } + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.google.gms:google-services:4.3.15") // โœ… ๊ด„ํ˜ธ ์ฃผ์˜ + } +} + diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..4fdea37 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"code-l-b109b","appId":"1:971644319685:android:34198e5a313e5294b53dac","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"code-l-b109b","appId":"1:971644319685:ios:bde2639222525193b53dac","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"code-l-b109b","configurations":{"android":"1:971644319685:android:34198e5a313e5294b53dac","ios":"1:971644319685:ios:bde2639222525193b53dac"}}}}}} \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..294b470 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,130 @@ +PODS: + - Firebase/Auth (11.10.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 11.10.0) + - Firebase/CoreOnly (11.10.0): + - FirebaseCore (~> 11.10.0) + - firebase_auth (5.5.2): + - Firebase/Auth (= 11.10.0) + - firebase_core + - Flutter + - firebase_core (3.13.0): + - Firebase/CoreOnly (= 11.10.0) + - Flutter + - FirebaseAppCheckInterop (11.11.0) + - FirebaseAuth (11.10.0): + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseAuthInterop (~> 11.0) + - FirebaseCore (~> 11.10.0) + - FirebaseCoreExtension (~> 11.10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GTMSessionFetcher/Core (< 5.0, >= 3.4) + - RecaptchaInterop (~> 101.0) + - FirebaseAuthInterop (11.11.0) + - FirebaseCore (11.10.0): + - FirebaseCoreInternal (~> 11.10.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreExtension (11.10.0): + - FirebaseCore (~> 11.10.0) + - FirebaseCoreInternal (11.10.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - Flutter (1.0.0) + - fluttertoast (0.0.2): + - Flutter + - GoogleUtilities/AppDelegateSwizzler (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.0.2): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.0.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMSessionFetcher/Core (4.4.0) + - kakao_flutter_sdk_common (1.9.7-3): + - Flutter + - RecaptchaInterop (101.0.0) + - screen_protector (1.2.1): + - Flutter + - ScreenProtectorKit (~> 1.3.1) + - ScreenProtectorKit (1.3.1) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - Flutter (from `Flutter`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - kakao_flutter_sdk_common (from `.symlinks/plugins/kakao_flutter_sdk_common/ios`) + - screen_protector (from `.symlinks/plugins/screen_protector/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + +SPEC REPOS: + trunk: + - Firebase + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - GoogleUtilities + - GTMSessionFetcher + - RecaptchaInterop + - ScreenProtectorKit + +EXTERNAL SOURCES: + firebase_auth: + :path: ".symlinks/plugins/firebase_auth/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + Flutter: + :path: Flutter + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + kakao_flutter_sdk_common: + :path: ".symlinks/plugins/kakao_flutter_sdk_common/ios" + screen_protector: + :path: ".symlinks/plugins/screen_protector/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + +SPEC CHECKSUMS: + Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 + firebase_auth: e37065f3f80ff90580c13ad0e5a48e3bb8d2ad77 + firebase_core: 432718558359a8c08762151b5f49bb0f093eb6e0 + FirebaseAppCheckInterop: f23709c9ce92d810aa53ff4ce12ad3e666a3c7be + FirebaseAuth: c4146bdfdc87329f9962babd24dae89373f49a32 + FirebaseAuthInterop: ac22ed402c2f4e3a8c63ebd3278af9a06073c1be + FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7 + FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac + FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + GTMSessionFetcher: 75b671f9e551e4c49153d4c4f8659ef4f559b970 + kakao_flutter_sdk_common: 3dc8492c202af7853585d151490b1c5c6b7576cb + RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba + screen_protector: 6f92086bd2f2f4b54f54913289b9d1310610140b + ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + +PODFILE CHECKSUM: f8c2dcdfb50bb67645580d28a6bf814fca30bdec + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 225c36d..5246466 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A0C3D7BA37E297BF670F5764 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + AFD21D852DB512DE00452D52 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; }; AFE368912DBBC8390072E7EF /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; B955AE8AC425CE69815F5900 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C7F447F3DE684CF75971C579 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; @@ -147,6 +148,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + AFD21D852DB512DE00452D52 /* RunnerDebug.entitlements */, E1FBF0C22DBBDA7A0017C51F /* Runner.entitlements */, AFE368912DBBC8390072E7EF /* Runner.entitlements */, E1FBF0C02DAE7C460017C51F /* GoogleService-Info.plist */, @@ -666,6 +668,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..f6353c7 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 971644319685-jo8kiqh779jhevlh53d3cbj3lrdavovf.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.971644319685-jo8kiqh779jhevlh53d3cbj3lrdavovf + ANDROID_CLIENT_ID + 971644319685-vnlopiq93h2n0cukes3chrm8n51t2836.apps.googleusercontent.com + API_KEY + AIzaSyA6aPcY0Qv4eCO4Q_KLxF0KMDQ9M9wdZlw + GCM_SENDER_ID + 971644319685 + PLIST_VERSION + 1 + BUNDLE_ID + com.codel.codel + PROJECT_ID + code-l-b109b + STORAGE_BUCKET + code-l-b109b.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:971644319685:ios:bde2639222525193b53dac + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index b2fbcc6..f425cd0 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,15 +2,8 @@ -CFBundleURLTypes - - - CFBundleURLSchemes - - kakaoe17acdc68adf7250d61dd15f8ee185c1 - - - + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -29,10 +22,43 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + kakaoe17acdc68adf7250d61dd15f8ee185c1 + app-1-971644319685-ios-bde2639222525193b53dac + + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.googleusercontent.apps.971644319685-jo8kiqh779jhevlh53d3cbj3lrdavovf + + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + app-1-971644319685-ios-bde2639222525193b53dac + + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + remote-notification + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -50,9 +76,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/ios/Runner/RunnerDebug.entitlements b/ios/Runner/RunnerDebug.entitlements new file mode 100644 index 0000000..903def2 --- /dev/null +++ b/ios/Runner/RunnerDebug.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/lib/auth/data/repositories/default_phone_number_repository.dart b/lib/auth/data/repositories/default_phone_number_repository.dart new file mode 100644 index 0000000..b3c6dd5 --- /dev/null +++ b/lib/auth/data/repositories/default_phone_number_repository.dart @@ -0,0 +1,47 @@ +import 'dart:developer'; + +import 'package:firebase_auth/firebase_auth.dart'; + +import '../../domain/repositories/phone_number_repository.dart'; + +class DefaultPhoneNumberRepository implements PhoneNumberRepository { + final FirebaseAuth _auth = FirebaseAuth.instance; + + @override + Future verifyPhoneNumber( + String phoneNumber, Function(String) onCodeSent) async { + await _auth.verifyPhoneNumber( + phoneNumber: phoneNumber, + timeout: const Duration(seconds: 120), + verificationCompleted: (PhoneAuthCredential credential) async { + log(name: 'DefaultPhoneNumberRepository : verifyPhoneNumber', 'ํ•ธ๋“œํฐ ์ž๋™ ์ธ์ฆ ์™„๋ฃŒ'); + await _auth.signInWithCredential(credential); + }, + verificationFailed: (FirebaseAuthException e) { + log(name: 'DefaultPhoneNumberRepository : verifyPhoneNumber', '์ธ์ฆ ๋ฒˆํ˜ธ ์‹คํŒจ: ${e.message}'); + }, + codeSent: (String verificationId, int? forceResendingToken) async { + log(name: 'DefaultPhoneNumberRepository : verifyPhoneNumber', '์ธ์ฆ ๋ฒˆํ˜ธ ์ „์†ก'); + onCodeSent(verificationId); + }, + codeAutoRetrievalTimeout: (String verificationId) { + log(name: 'DefaultPhoneNumberRepository : verifyPhoneNumber', "์ธ์ฆ ๋ฒˆํ˜ธ ์‹œ๊ฐ„ ์ดˆ๊ณผ"); + }, + ); + } + + @override + Future signInWithSmsCode(String verificationId, String smsCode) async { + try { + final credential = PhoneAuthProvider.credential( + verificationId: verificationId, + smsCode: smsCode, + ); + await _auth.signInWithCredential(credential); + log(name: 'DefaultPhoneNumberRepository : signInWithSmsCode', '๋กœ๊ทธ์ธ ์„ฑ๊ณต'); + } on FirebaseAuthException catch (e) { + log(name: 'DefaultPhoneNumberRepository : signInWithSmsCode', '๋กœ๊ทธ์ธ ์‹คํŒจ: ${e.code} - ${e.message}'); + rethrow; + } + } +} diff --git a/lib/auth/domain/model/identity/phone_number.dart b/lib/auth/domain/model/identity/phone_number.dart new file mode 100644 index 0000000..f4a3117 --- /dev/null +++ b/lib/auth/domain/model/identity/phone_number.dart @@ -0,0 +1,24 @@ + +class PhoneNumber { + final String value; + + PhoneNumber._(this.value); + + factory PhoneNumber.fromLocal(String local) { + if (!local.startsWith('0')) { + throw FormatException("๋กœ์ปฌ ๋ฒˆํ˜ธ๋Š” 0์œผ๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"); + } + final formatted = "+82${local.substring(1)}"; + return PhoneNumber._(formatted); + } + + factory PhoneNumber.fromInternational(String international) { + if (!international.startsWith('+82')) { + throw FormatException("๊ตญ์ œ๋ฒˆํ˜ธ ํ˜•์‹์ด ์•„๋‹™๋‹ˆ๋‹ค"); + } + return PhoneNumber._(international); + } + + @override + String toString() => value; +} diff --git a/lib/auth/domain/repositories/.gitkeep b/lib/auth/domain/repositories/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/auth/domain/repositories/phone_number_repository.dart b/lib/auth/domain/repositories/phone_number_repository.dart new file mode 100644 index 0000000..52f9eac --- /dev/null +++ b/lib/auth/domain/repositories/phone_number_repository.dart @@ -0,0 +1,5 @@ +abstract class PhoneNumberRepository { + Future verifyPhoneNumber( + String phoneNumber, Function(String) onCodeSent); + Future signInWithSmsCode(String verificationId, String smsCode); +} diff --git a/lib/auth/domain/usecases/.gitkeep b/lib/auth/domain/usecases/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/auth/domain/usecases/phone_number_verification_usecase.dart b/lib/auth/domain/usecases/phone_number_verification_usecase.dart new file mode 100644 index 0000000..c8c6c47 --- /dev/null +++ b/lib/auth/domain/usecases/phone_number_verification_usecase.dart @@ -0,0 +1,55 @@ +import 'dart:developer'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; + +import '../model/identity/phone_number.dart'; +import '../repositories/phone_number_repository.dart'; + +class PhoneNumberVerificationUseCase { + final PhoneNumberRepository _authRepository; + + PhoneNumberVerificationUseCase(this._authRepository); + + Future sendCode( + PhoneNumber phoneNumber, + Function(String) onCodeSent, + ) async { + await _authRepository.verifyPhoneNumber(phoneNumber.value, onCodeSent); + } + + Future verifyCode(String verificationId, String smsCode) async { + await _authRepository.signInWithSmsCode(verificationId, smsCode); + } + + Future isRegistered(PhoneNumber phoneNumber) async { + final firestore = FirebaseFirestore.instance; + + final querySnapshot = await firestore + .collection('users') + .where('phoneNumber', isEqualTo: phoneNumber.value) + .get(); + + return querySnapshot.docs.isNotEmpty; + } + + Future saveUser() async { + final user = FirebaseAuth.instance.currentUser; + + if (user != null) { + final uid = user.uid; + final phone = PhoneNumber.fromInternational(user.phoneNumber!); + + await FirebaseFirestore.instance + .collection('users') + .doc(uid) + .set({ + 'phoneNumber': phone, + 'createdAt': FieldValue.serverTimestamp(), + }); + log(name: 'PhoneNumberVerificationUseCase : saveUser', 'firestore์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ ์™„๋ฃŒ'); + } else { + log(name: 'PhoneNumberVerificationUseCase : saveUser', 'firestore์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ ์‹คํŒจ'); + } + } +} diff --git a/lib/auth/presentation/pages/identity/identity_verification_page.dart b/lib/auth/presentation/pages/identity/identity_verification_page.dart new file mode 100644 index 0000000..7e83b13 --- /dev/null +++ b/lib/auth/presentation/pages/identity/identity_verification_page.dart @@ -0,0 +1,138 @@ +import 'package:code_l/auth/presentation/pages/identity/providers.dart'; +import 'package:code_l/auth/presentation/pages/identity/widgets/identity_verification_app_bar.dart'; +import 'package:code_l/auth/presentation/widgets/auth_confirm_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../core/utills/design/app_colors.dart'; +import '../../../../core/utills/design/app_gaps.dart'; +import '../../../../core/utills/design/app_typography.dart'; +import '../login/login_page.dart'; + +class PhoneVerificationPage extends ConsumerWidget { + const PhoneVerificationPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final viewModel = ref.watch(phoneVerificationViewModelProvider.notifier); + final state = ref.watch(phoneVerificationViewModelProvider); + + return Scaffold( + appBar: IdentityVerificationAppBar(), + bottomNavigationBar: Padding( + padding: const EdgeInsets.all(20.0), + child: AuthConfirmButton( + enabled: state.codeSent, + onPressed: () { + if (viewModel.formKey.currentState!.validate()) { + viewModel.verifyCode(() { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const LoginPage()), + ); + }); + } + }, + ), + ), + body: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Form( + key: viewModel.formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: AppGaps.gap40), + Text("ํœด๋Œ€์ „ํ™” ๋ฒˆํ˜ธ\n์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค", style: AppTypography.header1), + Text( + "์›ํ™œํ•œ ์„œ๋น„์Šค ์ด์šฉ์„ ์œ„ํ•ด ๋ฒˆํ˜ธ์ธ์ฆ์„ ํ•ด์ฃผ์„ธ์š”", + style: AppTypography.body2.copyWith( + color: AppColors.grey600, + ), + ), + SizedBox(height: AppGaps.gap40), + SizedBox(height: AppGaps.gap40), + TextFormField( + controller: viewModel.phoneController, + keyboardType: TextInputType.phone, + decoration: InputDecoration( + border: UnderlineInputBorder( + borderSide: BorderSide( + color: AppColors.grey400, + width: 0.6, + ), + ), + hintText: 'ํœด๋Œ€์ „ํ™” ๋ฒˆํ˜ธ', + suffixIcon: ElevatedButton( + onPressed: () { + if (viewModel.formKey.currentState!.validate()) { + viewModel.sendSmsCode(); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: AppColors.white, + textStyle: AppTypography.subtitle2.copyWith( + color: AppColors.white, + ), + shape: RoundedRectangleBorder(), + padding: const EdgeInsets.symmetric( + horizontal: AppGaps.gap12, + vertical: AppGaps.gap12, + ), + ), + child: const Text("์ธ์ฆ ์š”์ฒญ"), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'ํœด๋Œ€์ „ํ™” ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.'; + } + if (!RegExp( + r'^01[0-9]-?\d{3,4}-?\d{4}$', + ).hasMatch(value)) { + return '์œ ํšจํ•œ ํœด๋Œ€์ „ํ™” ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.'; + } + return null; + }, + ), + + const SizedBox(height: 15), + + if (state.codeSent) ...[ + const SizedBox(height: 20), + // ์ธ์ฆ ์ฝ”๋“œ ์ž…๋ ฅ ํ•„๋“œ + TextFormField( + controller: viewModel.smsCodeController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + border: UnderlineInputBorder( + borderSide: BorderSide( + color: AppColors.grey400, + width: 0.6, + ), + ), + hintText: '์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.', + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '์ธ์ฆ๋ฒˆํ˜ธ ์ž…๋ ฅ'; + } + if (!RegExp(r'^\d{6}$').hasMatch(value)) { + return '6์ž๋ฆฌ ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.'; + } + return null; + }, + ), + ], + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/auth/presentation/pages/identity/identity_verification_viewmodel.dart b/lib/auth/presentation/pages/identity/identity_verification_viewmodel.dart new file mode 100644 index 0000000..4252b72 --- /dev/null +++ b/lib/auth/presentation/pages/identity/identity_verification_viewmodel.dart @@ -0,0 +1,60 @@ +import 'package:code_l/auth/presentation/pages/identity/providers.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +import '../../../domain/model/identity/phone_number.dart'; +import 'phone_verification_state.dart'; +import '../../../domain/usecases/phone_number_verification_usecase.dart'; + +class PhoneVerificationViewModel extends Notifier { + late final PhoneNumberVerificationUseCase _verifyPhoneNumberUseCase; + final formKey = GlobalKey(); + final phoneController = TextEditingController(); + final smsCodeController = TextEditingController(); + + @override + PhoneVerificationState build() { + _verifyPhoneNumberUseCase = ref.read(verifyPhoneNumberUseCaseProvider); + return PhoneVerificationState(); + } + + Future sendSmsCode() async { + if (formKey.currentState!.validate()) { + final number = PhoneNumber.fromLocal(phoneController.text); + + final isRegistered = await _verifyPhoneNumberUseCase.isRegistered(number); + if (isRegistered) { + Fluttertoast.showToast(msg: "์ด๋ฏธ ๋“ฑ๋ก๋œ ๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค"); + return; + } + + await _verifyPhoneNumberUseCase.sendCode( + number, + (verificationId) { + state = state.copyWith( + verificationId: verificationId, + codeSent: true, + ); + }, + ); + + Fluttertoast.showToast(msg: "์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ์ „์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + } + } + + Future verifyCode(Function onSuccess) async { + try { + await _verifyPhoneNumberUseCase.verifyCode( + state.verificationId, + smsCodeController.text, + ); + + Fluttertoast.showToast(msg: '์ธ์ฆ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค'); + await _verifyPhoneNumberUseCase.saveUser(); + onSuccess(); + } catch (e) { + Fluttertoast.showToast(msg: "์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"); + } + } +} diff --git a/lib/auth/presentation/pages/identity/phone_verification_state.dart b/lib/auth/presentation/pages/identity/phone_verification_state.dart new file mode 100644 index 0000000..048a2b8 --- /dev/null +++ b/lib/auth/presentation/pages/identity/phone_verification_state.dart @@ -0,0 +1,20 @@ + +class PhoneVerificationState { + final bool codeSent; + final String verificationId; + + PhoneVerificationState({ + this.codeSent = false, + this.verificationId = '', + }); + + PhoneVerificationState copyWith({ + bool? codeSent, + String? verificationId, + }) { + return PhoneVerificationState( + codeSent: codeSent ?? this.codeSent, + verificationId: verificationId ?? this.verificationId, + ); + } +} diff --git a/lib/auth/presentation/pages/identity/providers.dart b/lib/auth/presentation/pages/identity/providers.dart new file mode 100644 index 0000000..b6c2919 --- /dev/null +++ b/lib/auth/presentation/pages/identity/providers.dart @@ -0,0 +1,20 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../data/repositories/default_phone_number_repository.dart'; +import 'phone_verification_state.dart'; +import '../../../domain/repositories/phone_number_repository.dart'; +import '../../../domain/usecases/phone_number_verification_usecase.dart'; +import 'identity_verification_viewmodel.dart'; + +final verifyPhoneNumberUseCaseProvider = Provider((ref) { + return PhoneNumberVerificationUseCase(ref.read(authRepositoryProvider)); +}); + +final phoneVerificationViewModelProvider = +NotifierProvider( + PhoneVerificationViewModel.new, +); + +final authRepositoryProvider = Provider((ref) { + return DefaultPhoneNumberRepository(); +}); diff --git a/lib/auth/presentation/pages/identity/widgets/identity_verification_app_bar.dart b/lib/auth/presentation/pages/identity/widgets/identity_verification_app_bar.dart new file mode 100644 index 0000000..07b29ff --- /dev/null +++ b/lib/auth/presentation/pages/identity/widgets/identity_verification_app_bar.dart @@ -0,0 +1,38 @@ +import 'package:code_l/core/utills/design/app_gaps.dart'; +import 'package:flutter/material.dart'; + +import '../../../../../core/utills/design/app_colors.dart'; +import '../../../../../core/utills/design/app_typography.dart'; + +class IdentityVerificationAppBar extends StatelessWidget + implements PreferredSizeWidget { + const IdentityVerificationAppBar({super.key}); + + @override + Size get preferredSize => Size.fromHeight(56); + @override + Widget build(BuildContext context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: AppGaps.gap36), + Text( + "ํœด๋Œ€์ „ํ™” ๋ฒˆํ˜ธ ์ธ์ฆ", + style: AppTypography.subtitle2.copyWith(color: AppColors.grey900), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..e783b66 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,71 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAGaxpjP5Jn2P1J-O5W5DmFCQgqcIhuxyw', + appId: '1:971644319685:android:34198e5a313e5294b53dac', + messagingSenderId: '971644319685', + projectId: 'code-l-b109b', + storageBucket: 'code-l-b109b.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyA6aPcY0Qv4eCO4Q_KLxF0KMDQ9M9wdZlw', + appId: '1:971644319685:ios:bde2639222525193b53dac', + messagingSenderId: '971644319685', + projectId: 'code-l-b109b', + storageBucket: 'code-l-b109b.firebasestorage.app', + androidClientId: '971644319685-vnlopiq93h2n0cukes3chrm8n51t2836.apps.googleusercontent.com', + iosClientId: '971644319685-jo8kiqh779jhevlh53d3cbj3lrdavovf.apps.googleusercontent.com', + iosBundleId: 'com.codel.codel', + ); + +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 732a124..656c2c0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f url: "https://pub.dev" source: hosted - version: "80.0.0" + version: "82.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: f4c21c94eb4623b183c1014a470196b3910701bea9b926e6c91270d756e6fc60 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.4.1" args: dependency: transitive description: @@ -97,6 +97,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: "89a5e32716794b6a8d0ec1b5dfda988194e92daedaa3f3bed66fa0d0a595252e" + url: "https://pub.dev" + source: hosted + version: "5.6.6" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "9f012844eb59be6827ed97415875c5a29ccacd28bc79bf85b4680738251a33df" + url: "https://pub.dev" + source: hosted + version: "6.6.6" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: b8b754269be0e907acd9ff63ad60f66b84c78d330ca1d7e474f86c9527ddc803 + url: "https://pub.dev" + source: hosted + version: "4.4.6" collection: dependency: transitive description: @@ -314,10 +338,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -328,6 +352,14 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" + url: "https://pub.dev" + source: hosted + version: "8.2.12" gif: dependency: "direct main" description: @@ -444,10 +476,10 @@ packages: dependency: "direct main" description: name: json_serializable - sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a" + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c url: "https://pub.dev" source: hosted - version: "6.9.4" + version: "6.9.5" kakao_flutter_sdk_auth: dependency: transitive description: @@ -668,10 +700,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5fa8cb0..43c4870 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,11 +41,13 @@ dependencies: firebase_auth: ^5.5.2 firebase_core: ^3.13.0 http: ^1.3.0 + fluttertoast: ^8.2.12 kakao_flutter_sdk_user: ^1.9.7+3 flutter_dotenv: ^5.2.1 screen_protector: ^1.4.2+1 sign_in_with_apple: ^7.0.1 image_picker: ^1.1.2 + cloud_firestore: ^5.6.6 gif: ^2.3.0 dev_dependencies: diff --git a/test/widget_test.dart b/test/widget_test.dart index 7f18f08..3855f07 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -11,20 +11,20 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:code_l/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); + // testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // // Build our app and trigger a frame. + // await tester.pumpWidget(const MyApp()); + // + // // Verify that our counter starts at 0. + // expect(find.text('0'), findsOneWidget); + // expect(find.text('1'), findsNothing); + // + // // Tap the '+' icon and trigger a frame. + // await tester.tap(find.byIcon(Icons.add)); + // await tester.pump(); + // + // // Verify that our counter has incremented. + // expect(find.text('0'), findsNothing); + // expect(find.text('1'), findsOneWidget); + // }); }