From 8110e25cb98c66dd0ce120fdb1c169cf23a5a7fc Mon Sep 17 00:00:00 2001 From: BisratHailu Date: Thu, 5 Feb 2026 23:03:52 +0300 Subject: [PATCH] feat(auth): Add google sign-in option --- android/app/build.gradle.kts | 23 +- android/app/google-services.json | 51 ++ android/app/src/main/AndroidManifest.xml | 2 +- .../lms/app}/MainActivity.kt | 2 +- android/settings.gradle.kts | 6 +- assets/icons/a_1.svg | 5 + assets/icons/a_2.svg | 5 + assets/icons/b1.svg | 10 - assets/icons/b_1.svg | 5 + assets/icons/b_2.svg | 5 + firebase.json | 1 + ios/Runner.xcodeproj/project.pbxproj | 12 +- lib/app/app.dart | 4 + lib/app/app.locator.dart | 4 + lib/firebase_options.dart | 70 ++ lib/models/assessment.dart | 28 +- lib/models/assessment.g.dart | 20 +- lib/models/option.dart | 8 +- lib/models/option.g.dart | 6 +- lib/models/question.dart | 36 - lib/models/question.g.dart | 27 - lib/models/user_model.dart | 36 +- lib/models/user_model.g.dart | 26 +- lib/services/api_service.dart | 201 ++++- lib/services/authentication_service.dart | 99 ++- lib/services/dio_service.dart | 20 +- lib/services/google_auth_service.dart | 21 + lib/services/image_downloader_service.dart | 33 + lib/ui/common/app_constants.dart | 9 +- lib/ui/common/enmus.dart | 2 +- lib/ui/common/ui_helpers.dart | 14 + lib/ui/views/assessment/assessment_view.dart | 14 +- .../assessment/assessment_viewmodel.dart | 151 ++-- .../screens/assessment_form_screen.dart | 33 +- .../screens/assessment_intro_screen.dart | 14 +- .../screens/assessment_loading_screen.dart | 17 +- .../screens/assessment_result_screen.dart | 21 +- .../screens/start_lesson_screen.dart | 28 +- lib/ui/views/home/home_viewmodel.dart | 88 ++- lib/ui/views/learn/learn_view.dart | 2 +- .../views/learn_lesson/learn_lesson_view.dart | 1 - lib/ui/views/login/login_viewmodel.dart | 97 ++- .../screens/login_with_email_screen.dart | 20 +- .../onboarding/onboarding_viewmodel.dart | 50 +- .../screens/country_region_form_screen.dart | 7 +- lib/ui/views/profile/profile_view.dart | 8 +- lib/ui/views/profile/profile_viewmodel.dart | 66 +- .../profile_detail/profile_detail_view.dart | 214 ++++-- .../profile_detail_view.form.dart | 34 + .../profile_detail_viewmodel.dart | 168 ++++- lib/ui/views/progress/progress_viewmodel.dart | 6 +- lib/ui/views/register/register_viewmodel.dart | 125 ++-- lib/ui/views/startup/startup_viewmodel.dart | 7 +- lib/ui/views/welcome/welcome_viewmodel.dart | 27 +- lib/ui/widgets/birthday_selector.dart | 1 - lib/ui/widgets/custom_dropdown.dart | 37 +- lib/ui/widgets/learn_app_bar.dart | 16 +- lib/ui/widgets/learn_lesson_tile.dart | 1 - lib/ui/widgets/profile_image.dart | 5 +- lib/ui/widgets/refresh_button.dart | 19 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 84 ++- pubspec.yaml | 6 +- test/helpers/test_helpers.dart | 21 + test/helpers/test_helpers.mocks.dart | 707 +++++++++++------- test/services/google_auth_service_test.dart | 11 + .../image_downloader_service_test.dart | 11 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 69 files changed, 2067 insertions(+), 849 deletions(-) create mode 100644 android/app/google-services.json rename android/app/src/main/kotlin/com/{example/yimaru_app => yimaru/lms/app}/MainActivity.kt (75%) create mode 100644 assets/icons/a_1.svg create mode 100644 assets/icons/a_2.svg delete mode 100644 assets/icons/b1.svg create mode 100644 assets/icons/b_1.svg create mode 100644 assets/icons/b_2.svg create mode 100644 firebase.json create mode 100644 lib/firebase_options.dart delete mode 100644 lib/models/question.dart delete mode 100644 lib/models/question.g.dart create mode 100644 lib/services/google_auth_service.dart create mode 100644 lib/services/image_downloader_service.dart create mode 100644 lib/ui/widgets/refresh_button.dart create mode 100644 test/services/google_auth_service_test.dart create mode 100644 test/services/image_downloader_service_test.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 19de2ad..005d2b7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,12 +1,12 @@ plugins { - id("com.android.application") id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("com.android.application") + id("com.google.gms.google-services") id("dev.flutter.flutter-gradle-plugin") } android { - namespace = "com.example.yimaru_app" + namespace = "com.yimaru.lms.app" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -15,25 +15,24 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } } + defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.yimaru_app" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion + applicationId = "com.yimaru.lms.app" versionCode = flutter.versionCode versionName = flutter.versionName + targetSdk = flutter.targetSdkVersion + } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.getByName("debug") } } diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..cd2ee71 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,51 @@ +{ + "project_info": { + "project_number": "574860813475", + "project_id": "yimaru-lms-e834e", + "storage_bucket": "yimaru-lms-e834e.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:574860813475:android:cd7fa6cf3a0527d97acb16", + "android_client_info": { + "package_name": "com.yimaru.lms.app" + } + }, + "oauth_client": [ + { + "client_id": "574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.yimaru.lms.app", + "certificate_hash": "fc91f52846d27c62bba3e16bc98982fb9953eca1" + } + }, + { + "client_id": "574860813475-631s3mo8ha2qc2jeb5e2aosn0967niik.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.yimaru.lms.app", + "certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a" + } + } + ], + "api_key": [ + { + "current_key": "AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1dc40a9..e39aa8a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ + + + + diff --git a/assets/icons/a_2.svg b/assets/icons/a_2.svg new file mode 100644 index 0000000..9bdbd2e --- /dev/null +++ b/assets/icons/a_2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/b1.svg b/assets/icons/b1.svg deleted file mode 100644 index 4a75083..0000000 --- a/assets/icons/b1.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/b_1.svg b/assets/icons/b_1.svg new file mode 100644 index 0000000..85f988e --- /dev/null +++ b/assets/icons/b_1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/b_2.svg b/assets/icons/b_2.svg new file mode 100644 index 0000000..40dfb5d --- /dev/null +++ b/assets/icons/b_2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..4d7ff66 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"yimaru-lms-e834e","appId":"1:574860813475:android:cd7fa6cf3a0527d97acb16","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"yimaru-lms-e834e","configurations":{"android":"1:574860813475:android:cd7fa6cf3a0527d97acb16","ios":"1:574860813475:ios:3ac9f7c4ae1771287acb16"}}}}}} \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 79e3510..f72da0a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -368,7 +368,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -384,7 +384,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -401,7 +401,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -416,7 +416,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -547,7 +547,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -569,7 +569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/lib/app/app.dart b/lib/app/app.dart index 2b42eff..0130a88 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -33,6 +33,8 @@ import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'; import 'package:yimaru_app/ui/views/failure/failure_view.dart'; import 'package:yimaru_app/services/permission_handler_service.dart'; import 'package:yimaru_app/services/image_picker_service.dart'; +import 'package:yimaru_app/services/google_auth_service.dart'; +import 'package:yimaru_app/services/image_downloader_service.dart'; // @stacked-import @StackedApp( @@ -74,6 +76,8 @@ import 'package:yimaru_app/services/image_picker_service.dart'; LazySingleton(classType: StatusCheckerService), LazySingleton(classType: PermissionHandlerService), LazySingleton(classType: ImagePickerService), + LazySingleton(classType: GoogleAuthService), + LazySingleton(classType: ImageDownloaderService), // @stacked-service ], bottomsheets: [ diff --git a/lib/app/app.locator.dart b/lib/app/app.locator.dart index ccdd10f..77ac6ff 100644 --- a/lib/app/app.locator.dart +++ b/lib/app/app.locator.dart @@ -14,6 +14,8 @@ import 'package:stacked_shared/stacked_shared.dart'; import '../services/api_service.dart'; import '../services/authentication_service.dart'; import '../services/dio_service.dart'; +import '../services/google_auth_service.dart'; +import '../services/image_downloader_service.dart'; import '../services/image_picker_service.dart'; import '../services/permission_handler_service.dart'; import '../services/secure_storage_service.dart'; @@ -40,4 +42,6 @@ Future setupLocator({ locator.registerLazySingleton(() => StatusCheckerService()); locator.registerLazySingleton(() => PermissionHandlerService()); locator.registerLazySingleton(() => ImagePickerService()); + locator.registerLazySingleton(() => GoogleAuthService()); + locator.registerLazySingleton(() => ImageDownloaderService()); } diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..f2470b8 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,70 @@ +// 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: 'AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk', + appId: '1:574860813475:android:cd7fa6cf3a0527d97acb16', + messagingSenderId: '574860813475', + projectId: 'yimaru-lms-e834e', + storageBucket: 'yimaru-lms-e834e.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyBBcQ17JB6RBTjD7G7mh6Xf_FMUGxP5cC8', + appId: '1:574860813475:ios:3ac9f7c4ae1771287acb16', + messagingSenderId: '574860813475', + projectId: 'yimaru-lms-e834e', + storageBucket: 'yimaru-lms-e834e.firebasestorage.app', + androidClientId: + '574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com', + iosBundleId: 'com.yimaru.lms.app', + ); +} diff --git a/lib/models/assessment.dart b/lib/models/assessment.dart index e1cc5e5..9a6da65 100644 --- a/lib/models/assessment.dart +++ b/lib/models/assessment.dart @@ -1,17 +1,35 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:yimaru_app/models/option.dart'; -import 'package:yimaru_app/models/question.dart'; part 'assessment.g.dart'; @JsonSerializable() class Assessment { - @JsonKey(name: 'Question') - final Question? question; + final int? id; + + final int? points; + + final String? status; + + @JsonKey(name: 'question_type') + final String? questionType; + + @JsonKey(name: 'question_text') + final String? questionText; + + @JsonKey(name: 'difficulty_level') + final String? difficultyLevel; - @JsonKey(name: 'Options') final List