From 4409aef9e286a3249ee29133fbb1277174fd8ed4 Mon Sep 17 00:00:00 2001 From: Kerod-Fresenbet-Gebremedhin2660 Date: Tue, 2 Jun 2026 12:40:44 +0300 Subject: [PATCH] feat: integration of apple signin --- assets/translations/am.json | 2 + assets/translations/en.json | 4 +- ios/Runner.xcodeproj/project.pbxproj | 24 +- ios/Runner/Runner.entitlements | 10 + lib/app/app.dart | 2 + lib/app/app.locator.dart | 2 + lib/services/api_service.dart | 28 ++ lib/services/apple_auth_service.dart | 36 ++ lib/ui/common/app_constants.dart | 5 +- lib/ui/common/enmus.dart | 6 +- .../common/translations/codegen_loader.g.dart | 402 +++++++++--------- lib/ui/common/translations/locale_keys.g.dart | 4 +- lib/ui/views/login/login_viewmodel.dart | 51 ++- .../screens/login_with_email_screen.dart | 22 +- lib/ui/views/register/register_viewmodel.dart | 51 ++- .../screens/register_with_email_screen.dart | 28 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 28 +- pubspec.yaml | 1 + 19 files changed, 486 insertions(+), 222 deletions(-) create mode 100644 ios/Runner/Runner.entitlements create mode 100644 lib/services/apple_auth_service.dart diff --git a/assets/translations/am.json b/assets/translations/am.json index 2a704b6..5aff1d1 100644 --- a/assets/translations/am.json +++ b/assets/translations/am.json @@ -9,12 +9,14 @@ "cont": "ቀጥል", "register": "ይመዝገቡ", "login_with_google": "በጉግል ይግቡ", + "login_with_apple": "በአፕል ይግቡ", "or": "ወይም", "login_with_phone": "በስልክ ቁጥር ይግቡ", "create_account": "አዲስ መለያ ይፍጠሩ", "already_have_account": "መለያ አለዎት?", "login": " ይግቡ ", "register_with_google": "በጉግል ይመዝገቡ", + "register_with_apple": "በአፕል ይመዝገቡ", "register_with_phone": "በስልክ ቁጥር ይመዝገቡ", "enter_phone_number": "የስልክ ቁጥርዎን ያስገቡ። የማረጋገጫ ኮድ እንልክልዎታለን።", "login_with_email": "በኢሜይል ይግቡ", diff --git a/assets/translations/en.json b/assets/translations/en.json index 52b7365..cb37ef8 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -9,12 +9,14 @@ "cont": "Continue", "register": "Register", "login_with_google": "Login with Google", + "login_with_apple": "Login with Apple", "or": "Or", "login_with_phone": "Login with phone number", "create_account": "Create an account", "already_have_account": "Already have an account?", "login": "Login", "register_with_google": "Register with Google", + "register_with_apple": "Register with Apple", "register_with_phone": "Register with phone number", "enter_phone_number": "Enter your phone number. We will send you a confirmation code there.", "login_with_email": "Login with email", @@ -190,7 +192,7 @@ "finish_all_practice_previouse_course": "Finish the previous course practice to take this", "track_journey": "Track your learning journey and see your growth over time.", "learn_english": "Learn English", - "keep_momentum":"Great job! Keep the momentum.", + "keep_momentum": "Great job! Keep the momentum.", "completed_practices": "Completed Practices", "total_practices": "Total Practices", "progress_percentage": "Progress Percentage" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 3bed9d1..4d72727 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -66,6 +66,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 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 = ""; }; + ACEEA7C32CFC6E9900D60211 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; A8C2A6C7D1D99F7BA12EAF94 /* 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 = ""; }; F1F6AAAC52D909E27AEDEFC0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; F9793345F00B89E38C23EBB8 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -163,6 +164,7 @@ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 8E1B3E492C540A0B00F51C11 /* GoogleService-Info.plist */, + ACEEA7C32CFC6E9900D60211 /* Runner.entitlements */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, @@ -236,6 +238,11 @@ 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; + SystemCapabilities = { + com.apple.SignInWithApple = { + enabled = 1; + }; + }; }; }; }; @@ -249,7 +256,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -345,14 +352,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -366,14 +369,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -509,6 +508,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; @@ -698,6 +698,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; @@ -727,6 +728,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; @@ -785,7 +787,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..a812db5 --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.applesignin + + Default + + + diff --git a/lib/app/app.dart b/lib/app/app.dart index b50252d..cfcc5e7 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -56,6 +56,7 @@ import 'package:yimaru_app/services/localization_service.dart'; import 'package:yimaru_app/ui/views/landing/landing_view.dart'; import 'package:yimaru_app/ui/views/course_module/course_module_view.dart'; import 'package:yimaru_app/services/onboarding_service.dart'; +import 'package:yimaru_app/services/apple_auth_service.dart'; import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart'; import 'package:yimaru_app/ui/views/payment/payment_view.dart'; // @stacked-import @@ -124,6 +125,7 @@ import 'package:yimaru_app/ui/views/payment/payment_view.dart'; LazySingleton(classType: LearnService), LazySingleton(classType: LocalizationService), LazySingleton(classType: OnboardingService), + LazySingleton(classType: AppleAuthService), // @stacked-service ], bottomsheets: [ diff --git a/lib/app/app.locator.dart b/lib/app/app.locator.dart index 85eb85a..e5c7862 100644 --- a/lib/app/app.locator.dart +++ b/lib/app/app.locator.dart @@ -13,6 +13,7 @@ import 'package:stacked_services/src/navigation/navigation_service.dart'; import 'package:stacked_shared/stacked_shared.dart'; import '../services/api_service.dart'; +import '../services/apple_auth_service.dart'; import '../services/audio_player_service.dart'; import '../services/authentication_service.dart'; import '../services/course_service.dart'; @@ -67,4 +68,5 @@ Future setupLocator( locator.registerLazySingleton(() => LearnService()); locator.registerLazySingleton(() => LocalizationService()); locator.registerLazySingleton(() => OnboardingService()); + locator.registerLazySingleton(() => AppleAuthService()); } diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 8107694..005b4cd 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -112,6 +112,34 @@ class ApiService { } } + // Apple auth + Future> appleAuth(Map data) async { + try { + Response response = await _service.dio.post( + '$kBaseUrl/$kAppleAuthUrl', + data: data, + ); + + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Logged in successfully', + 'data': User.fromJson(response.data['data']), + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': '${response.data['message']}, ${response.data['error']}' + }; + } + } on DioException catch (e) { + return { + 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), + }; + } + } + // Verify otp Future> verifyOtp(Map data) async { try { diff --git a/lib/services/apple_auth_service.dart b/lib/services/apple_auth_service.dart new file mode 100644 index 0000000..f171ef5 --- /dev/null +++ b/lib/services/apple_auth_service.dart @@ -0,0 +1,36 @@ +import 'dart:io'; + +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; +import 'package:stacked/stacked.dart'; + +class AppleAuthService with ListenableServiceMixin { + AuthorizationCredentialAppleID? _appleCredential; + + AuthorizationCredentialAppleID? get appleCredential => _appleCredential; + + AppleAuthService() { + listenToReactiveValues([_appleCredential]); + } + + bool get isSupported => Platform.isIOS; + + Future appleAuth() async { + if (!isSupported) { + throw UnsupportedError('Apple Sign-In is only available on iOS.'); + } + + _appleCredential = await SignInWithApple.getAppleIDCredential( + scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ], + ); + + notifyListeners(); + } + + void logout() { + _appleCredential = null; + notifyListeners(); + } +} diff --git a/lib/ui/common/app_constants.dart b/lib/ui/common/app_constants.dart index 50bb114..de0843e 100644 --- a/lib/ui/common/app_constants.dart +++ b/lib/ui/common/app_constants.dart @@ -101,6 +101,8 @@ String kLessonProgressUrl = 'api/v1/progress/videos'; String kGoogleAuthUrl = 'api/v1/auth/google/android'; +String kAppleAuthUrl = 'api/v1/auth/apple'; + String kCourseProgressUrl = 'api/v1/progress/courses'; String kAssessmentsUrl = 'api/v1/assessment/questions'; @@ -124,5 +126,4 @@ String kTelegramSupportLink = 'https://t.me/yimaruacademy2026'; String kErrorUrl = 'https://api.yimaruacademy.com/payment/error'; -String kSuccessUrl = - 'https://api.yimaruacademy.com/payment/success'; +String kSuccessUrl = 'https://api.yimaruacademy.com/payment/success'; diff --git a/lib/ui/common/enmus.dart b/lib/ui/common/enmus.dart index 04bda1b..77f86f8 100644 --- a/lib/ui/common/enmus.dart +++ b/lib/ui/common/enmus.dart @@ -5,10 +5,10 @@ enum Voice { sample, recorded } enum ResponseStatus { success, failure } // Login method -enum LoginMethod { phone, email, google } +enum LoginMethod { phone, email, google, apple } // Sign-up method -enum SignUpMethod { phone, email, google } +enum SignUpMethod { phone, email, google, apple } // Learn practice enum LearnPractices { course, module, lesson } @@ -61,7 +61,9 @@ enum StateObjects { profileCompletion, learnSubscription, learnSubscriptions, + loginWithApple, registerWithGoogle, + registerWithApple, learnPracticeSample, learnPracticeAnswer, loginWithPhoneNumber, diff --git a/lib/ui/common/translations/codegen_loader.g.dart b/lib/ui/common/translations/codegen_loader.g.dart index 83d4d43..4b2e4e7 100644 --- a/lib/ui/common/translations/codegen_loader.g.dart +++ b/lib/ui/common/translations/codegen_loader.g.dart @@ -14,204 +14,7 @@ class CodegenLoader extends AssetLoader{ return Future.value(mapLocales[locale.toString()]); } - static const Map _am = { - "loading": "በመጫን ላይ", - "welcome_back": "እንኳን በደህና ተመለሱ", - "checking_user_info": "የተጠቃሚ መረጃን በማረጋገጥ ላይ", - "dont_have_account": "መለያ የለዎትም?", - "email": "ኢሜይል", - "password": "የይለፍ ቃል", - "forgot_password": "የይለፍ ቃል ረሱ?", - "cont": "ቀጥል", - "register": "ይመዝገቡ", - "login_with_google": "በጉግል ይግቡ", - "or": "ወይም", - "login_with_phone": "በስልክ ቁጥር ይግቡ", - "create_account": "አዲስ መለያ ይፍጠሩ", - "already_have_account": "መለያ አለዎት?", - "login": " ይግቡ ", - "register_with_google": "በጉግል ይመዝገቡ", - "register_with_phone": "በስልክ ቁጥር ይመዝገቡ", - "enter_phone_number": "የስልክ ቁጥርዎን ያስገቡ። የማረጋገጫ ኮድ እንልክልዎታለን።", - "login_with_email": "በኢሜይል ይግቡ", - "create_password": "የይለፍ ቃል ይፍጠሩ", - "confirm_password": "የይለፍ ቃል ያረጋግጡ", - "eight_character_minimum": "ቢያንስ 8 ፊደላት", - "password_match": "የይለፍ ቃሉ ተመሳስሏል", - "sign_up_agreement": "‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።", - "terms_of_services": "የአገልግሎት ውሎች", - "and": "እና", - "privacy_policy": "የግላዊነት ፖሊሲ", - "register_with_email": "በኢሜል ይመዝገቡ", - "verification_code": "የማረጋገጫ ኮድ", - "resend_code": "ኮዱን እንደገና ላክ", - "code_sent_to_phone": "ኮዱ ወደ ስልክ ቁጥርዎ ተልኳል", - "code_sent_to_email": "ኮዱ ወደ ኢሜል ተልኳል", - "resend_code_in": "ኮዱን እንደገና ለመላክ የቀረው ጊዜ", - "reset_password": " የይለፍ ቃልን ይቀይሩ", - "enter_email_reset_code": "ኢሜይልዎን ያስገቡ። የይለፍ ቃል መለወጫ ኮድ እንልክልዎታለን።", - "please_wait": "እባክዎ ይጠብቁ", - "reset_code_sent": "የመቀየሪያ ኮድ በተሳካ ሁኔታ ተልኳል", - "reset_code": " የመቀየሪያ ኮድ ", - "new_password": "አዲስ የይለፍ ቃል", - "logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል", - "view_course": " ኮርሱን ይመልከቱ", - "continue_learning": "መማርን ይቀጥሉ", - "start_learning": "ትምህርትን ይጀምሩ", - "completed": "ተጠናቋል", - "take_practice": "ልምምድ ያድርጉ", - "your_current_level": "የአሁኑ ደረጃዎ", - "overall_progress": "አጠቃላይ እድገት", - "great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው", - "view_module": "ሞጁሉን ይመልከቱ", - "progress": "እድገት", - "keep_going": "ይቀጥሉ - ከግማሽ በላይ ጨርሰዋል ", - "lessons_in_module": "በዚህ ሞጁል ውስጥ ያሉ ትምህርቶች ", - "practice": "ልምምድ", - "start": "ጀምር", - "in_progress": "በሂደት ላይ", - "hello": "ሰላም", - "ready_to_learn": " ዛሬ እንግሊዝኛ ለመማር ተዘጋጅተዋል? ", - "learn": "ይማሩ ", - "course": "ኮርስ", - "profile": " ፕሮፋይል ", - "speaking_partner": "የንግግር ጓደኛ", - "practice_what_you_learned": "አሁን የተማሩትን እንለማመድ", - "practice_questions": "ጥቂት ጥያቄዎችን እጠይቃለሁ እና መልስ መስጠት ይችላሉ", - "start_practice": "ልምምድ ጀምር", - "almost_there": "ሊጨርሱ ተቃርበዋል", - "finish_session": "እድገትዎን ለማየት ክፍለ ጊዜውን ያጠናቅቁ", - "continue_practice": "ልምምዱን ይቀጥሉ", - "end_session": "ክፍለ ጊዜውን ያብቁ ", - "tap_start_to_listen": "ለማዳመጥ የጀምር ቁልፉን ይጫኑ", - "practice_speaking": "ንግግርን ይለማመዱ", - "tap_microphone": "ለመናገር ማይክሮፎኑን ይጫኑ", - "reply": "እንደገና አዳምጥ", - "cancel": "ይቅር", - "you_are_speaking": "እየተናገሩ ነው", - "practice_completed": "ልምምዱ ተጠናቅቋል", - "great_improvement": "በዚህኛው በራስ መተማመንዎ ጨምሯል፤ ትልቅ መሻሻል ነው", - "practice_again": "እንደገና ይለማመዱ", - "conversation_review": "የንግግር ግምገማ", - "result": "ውጤት", - "quick_tip": "ጠቃሚ ምክር", - "retry": "እንደገና ይሞክሩ", - "completed_a1": "እንኳን ደስ አለዎት! A1 ደረጃን አጠናቅቀዋል", - "analyzing_speaking": "የንግግር ችሎታዎን እየገመገምን ነው", - "view_profile": "ፕሮፋይሎን ይመልከቱ ", - "hi": "ሰላም", - "edit_profile": "መገለጫ ያስተካክሉ", - "first_name": "የመጀመሪያ ስም", - "last_name": "የአባት ስም", - "gender": "ፆታ", - "male": "ወንድ", - "female": "ሴት", - "phone_number": "የስልክ ቁጥር", - "country": "ሀገር", - "region": "ክልል", - "select_region": "ክልል ይምረጡ", - "enter_your_city": "ከተማዎን ያስገቡ", - "occupation": "የስራ መስክ", - "select_occupation": "ሙያዎን ይምረጡ", - "save_changes": "ለውጦችን ያስቀምጡ", - "my_progress": "የእኔ እድገት", - "track_your_achievement": "ስኬቶችዎን እና ተከታታይ የትምህርት ጉዞዎን ይከታተሉ", - "account_and_privacy": "መለያ እና ግላዊነት", - "manage_settings": "ቅንብሮችን እና የመተግበሪያ ምርጫዎችን ያስተዳድሩ", - "support": "ድጋፍ", - "get_help": "በስልክ ወይም በቴሌግራም እገዛ ያግኙ", - "logout": "ውጣ", - "app_settings": "የመተግበሪያ ቅንብሮች", - "legal_and_information": "ሕጋዊ እና መረጃ", - "change_language": "ቋንቋ ቀይር", - "terms_and_conditions": "ውሎች እና ሁኔታዎች", - "delete_account": "መለያ ሰርዝ", - "language_preference": "የቋንቋ ምርጫ", - "choose_your_language": "ለውጦችን አስቀምጥ", - "switch_language_anytime": "ቋንቋዎችን በማንኛውም ጊዜ መቀየር ይችላሉ", - "need_help": "እገዛ ይፈልጋሉ?", - "call_support": "የስልክ ድጋፍ", - "talk_with_support": "በቀጥታ ከድጋፍ ቡድናችን ጋር ይነጋገሩ", - "telegram_support": "የቴሌግራም ድጋፍ", - "chat_via_telegram": "በቴሌግራም በፍጥነት ይወያዩ", - "call_our_support": "ከ3 ጠዋት እስከ 12 ማታ ድረስ የድጋፍ ቡድናችንን ይደውሉ", - "tap_to_call": "ለመደወል ይንኩ", - "join_telegram": "በቴሌግራም የይማሩ አካዳሚን ይቀላቀሉ", - "connect_with_support_team": "ለፈጣን እርዳታ እና የማህበረሰብ ዝማኔዎች፣ በቴሌግራም ከድጋፍ ቡድናችን ጋር ወዲያውኑ ይገናኙ።", - "open_in_telegram": "በቴሌግራም ይክፈቱ", - "search_for": "ፈልጉት", - "current_level": "የአሁኑ ደረጃ", - "keep_up_the_great_work": "በጣም ጥሩ እየሰራህ ነው! ቀጥልበት፣ አስደናቂ ነህ።", - "no_practice_available": "ምንም ልምምድ አልተገኘም!", - "begin_module_practice": "የሞጁሉን ልምምድ ጀምር", - "lets_practice_lesson": "እንለማመድ", - "lets_quickly_review": "በዚህ ሞጁል ውስጥ የተማርከውን በፍጥነት እንከልስ!", - "lets_practice_module": "አሁን የተማርከውን እንለማመድ!", - "ask_you_few_actions": "ጥቂት ጥያቄዎችን እጠይቅሃለሁ፣ አንተም በተፈጥሮ መልስ ልትሰጥ ትችላለህ።", - "begin_level_practice": "የደረጃ ልምምድን ጀምር", - "lets_practice_course": "የኮርሱን ልምምድ እንለማመድ", - "lets_quick_review": "በዚህ ደረጃ የተማርከውን በፍጥነት እንከልስ!", - "speaking": "እየተናገረ ነው", - "you_have_finished_practice": "ልምምድህን አጠናቀቅህ", - "view_results": "ውጤቶቼን እይ", - "sample_answer": "ናሙና መልስ", - "your_answer": "መልስህ", - "sound_confident": "በዚህ ጊዜ የበለጠ እምነት ያለህ ይመስላል — በጣም ጥሩ መሻሻል ነው!", - "you_have_completed": "አያይ! አጠናቀቅህ", - "yes": "አዎ", - "no": "አይ", - "want_to_quit": "ለመውጣት እርግጠኛ ነህ?", - "required_field": "ይህ መስክ ያስፈልጋል", - "enter_full_name": "ሙሉ ስምህን አስገባ", - "invalid_email": "የማይሰራ የኢሜይል ቅርጸት", - "phone_must_start_with": "የስልክ ቁጥር በ251 መጀመር አለበት", - "phone_must_be": "የስልክ ቁጥር 12 አሃዞች መሆን አለበት", - "what_should_we_call_you": "ምን ብለን እንጠራህ?", - "name_for_personalization": "በመማር ጉዞህ ውስጥ ለግል ለማድረግ ስምህን እንጠቀማለን።", - "choose_your_gender": "ጾታህን ምረጥ", - "gender_for_personalization": "በጾታህ መሰረት የመማር ተሞክሮህን እናበጅለታለን።", - "age_range": "በየትኛው የእድሜ ክልል ውስጥ ነህ?", - "age_for_personalization": "በእድሜህ መሰረት የመማር ተሞክሮህን እናበጅለታለን።", - "educational_background": "አሁን ያለህ የትምህርት ደረጃ ምንድን ነው?", - "education_for_personalization": "ይህ ትምህርቶችን ከልምድህ ጋር እንዲስማሙ ለማድረግ ይረዳናል።", - "your_occupation": "ስራህ ምንድን ነው?", - "occupation_for_personalization": "በስራህ መሰረት የመማር ተሞክሮህን እናበጅለታለን።", - "location": "ከየት ነህ?", - "select_country_region": "አገርህን እና ክልልህን ከተቆልቋይ ዝርዝሩ ምረጥ", - "select_country": "አገር ምረጥ", - "learning_goal": "የመማር ዓላማህን ምረጥ", - "language_goal": "እንግሊዝኛህን ለማሻሻል ዋና ዓላማህ ምንድን ነው?", - "your_goal": "ዓላማህ የመማር ጉዞህን እንዲስማማ ለማድረግ ይረዳናል።", - "write_your_goal": "ዓላማህን ጻፍ…", - "challenge_you_face": "What challenge do you face most with English?", - "evey_one_has_strugle": "ሁሉም ሰው ችግሮች አሉት፣ የአንተን እንጀምር እንፍታ", - "write_your_challenge": "ችግርህን ጻፍ…", - "topic_interest": "በጣም የሚስቡህ ርዕሶች የትኞቹ ናቸው?", - "favourite_topic": "የምትወዳቸው ርዕሶች አስደሳች እና ከሕይወትህ ጋር የተዛመዱ ትምህርቶችን ለመፍጠር ይረዱናል።", - "your_interest": "ፍላጎትህን ጻፍ…", - "want_quick_assessment": "የእንግሊዝኛ ደረጃህን ለማወቅ ፈጣን ግምገማ ትፈልጋለህ?", - "answer_quick_questions": "የእንግሊዝኛ ችሎታህን ለመረዳት ጥቂት ፈጣን ጥያቄዎችን መልስ።", - "skip": "ዝለል", - "finish_level": "ደረጃውን አጠናቅቅ", - "likely_speaker": "አንተ ምናልባት ተናጋሪ ነህ", - "great_job": "በጣም ጥሩ ስራ! ለመሻሻል ቀጣዩ ደረጃህ ይኸው ነው።", - "lets_start_practice": "ልምምድህን እንጀምር", - "welcome_abroad": "እንኳን ደህና መጣህ", - "ready_to_explore": "የግል ትምህርቶችህን ለማሰስ ዝግጁ ነህ።", - "finish": "አጠናቅቅ", - "finish_all_practice_lesson": "ይህን ልምምድ ለመውሰድ የቀድሞውን የትምህርት ልምምድ ያጠናቅቁ", - "finish_all_practice_module": "የሞጁሉን ልምምድ ለመውሰድ የትምህርት ልምምዶችን ያጠናቅቁ", - "finish_all_practice_course": "የኮርሱን ልምምድ ለመውሰድ የሞጁል ልምምዶችን ያጠናቅቁ", - "finish_all_practice_previouse_module": "ይህን ልምምድ ለመውሰድ የቀድሞውን የሞጁል ልምምድ ያጠናቅቁ", - "finish_all_practice_previouse_course": "ይህን ለመውሰድ የቀድሞውን የኮርስ ልምምድ ያጠናቅቁ", - "track_journey": "የትምህርት ጉዞዎን ይከታተሉ እና በጊዜ ሂደት ያሳዩትን እድገት ይመልከቱ።", - "learn_english": "እንግሊዝኛ ይማሩ", - "keep_momentum": "በጣም ጥሩ ስራ! በዚሁ ብርታት ይቀጥሉ።", - "completed_practices": "የተጠናቀቁ ልምምዶች", - "total_practices": "ጠቅላላ ልምምዶች", - "progress_percentage": "የእድገት መቶኛ" -}; -static const Map _en = { + static const Map _en = { "loading": "Loading", "welcome_back": "Welcome back", "checking_user_info": "Checking user info", @@ -222,12 +25,14 @@ static const Map _en = { "cont": "Continue", "register": "Register", "login_with_google": "Login with Google", + "login_with_apple": "Login with Apple", "or": "Or", "login_with_phone": "Login with phone number", "create_account": "Create an account", "already_have_account": "Already have an account?", "login": "Login", "register_with_google": "Register with Google", + "register_with_apple": "Register with Apple", "register_with_phone": "Register with phone number", "enter_phone_number": "Enter your phone number. We will send you a confirmation code there.", "login_with_email": "Login with email", @@ -408,5 +213,204 @@ static const Map _en = { "total_practices": "Total Practices", "progress_percentage": "Progress Percentage" }; -static const Map> mapLocales = {"am": _am, "en": _en}; +static const Map _am = { + "loading": "በመጫን ላይ", + "welcome_back": "እንኳን በደህና ተመለሱ", + "checking_user_info": "የተጠቃሚ መረጃን በማረጋገጥ ላይ", + "dont_have_account": "መለያ የለዎትም?", + "email": "ኢሜይል", + "password": "የይለፍ ቃል", + "forgot_password": "የይለፍ ቃል ረሱ?", + "cont": "ቀጥል", + "register": "ይመዝገቡ", + "login_with_google": "በጉግል ይግቡ", + "login_with_apple": "በአፕል ይግቡ", + "or": "ወይም", + "login_with_phone": "በስልክ ቁጥር ይግቡ", + "create_account": "አዲስ መለያ ይፍጠሩ", + "already_have_account": "መለያ አለዎት?", + "login": " ይግቡ ", + "register_with_google": "በጉግል ይመዝገቡ", + "register_with_apple": "በአፕል ይመዝገቡ", + "register_with_phone": "በስልክ ቁጥር ይመዝገቡ", + "enter_phone_number": "የስልክ ቁጥርዎን ያስገቡ። የማረጋገጫ ኮድ እንልክልዎታለን።", + "login_with_email": "በኢሜይል ይግቡ", + "create_password": "የይለፍ ቃል ይፍጠሩ", + "confirm_password": "የይለፍ ቃል ያረጋግጡ", + "eight_character_minimum": "ቢያንስ 8 ፊደላት", + "password_match": "የይለፍ ቃሉ ተመሳስሏል", + "sign_up_agreement": "‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።", + "terms_of_services": "የአገልግሎት ውሎች", + "and": "እና", + "privacy_policy": "የግላዊነት ፖሊሲ", + "register_with_email": "በኢሜል ይመዝገቡ", + "verification_code": "የማረጋገጫ ኮድ", + "resend_code": "ኮዱን እንደገና ላክ", + "code_sent_to_phone": "ኮዱ ወደ ስልክ ቁጥርዎ ተልኳል", + "code_sent_to_email": "ኮዱ ወደ ኢሜል ተልኳል", + "resend_code_in": "ኮዱን እንደገና ለመላክ የቀረው ጊዜ", + "reset_password": " የይለፍ ቃልን ይቀይሩ", + "enter_email_reset_code": "ኢሜይልዎን ያስገቡ። የይለፍ ቃል መለወጫ ኮድ እንልክልዎታለን።", + "please_wait": "እባክዎ ይጠብቁ", + "reset_code_sent": "የመቀየሪያ ኮድ በተሳካ ሁኔታ ተልኳል", + "reset_code": " የመቀየሪያ ኮድ ", + "new_password": "አዲስ የይለፍ ቃል", + "logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል", + "view_course": " ኮርሱን ይመልከቱ", + "continue_learning": "መማርን ይቀጥሉ", + "start_learning": "ትምህርትን ይጀምሩ", + "completed": "ተጠናቋል", + "take_practice": "ልምምድ ያድርጉ", + "your_current_level": "የአሁኑ ደረጃዎ", + "overall_progress": "አጠቃላይ እድገት", + "great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው", + "view_module": "ሞጁሉን ይመልከቱ", + "progress": "እድገት", + "keep_going": "ይቀጥሉ - ከግማሽ በላይ ጨርሰዋል ", + "lessons_in_module": "በዚህ ሞጁል ውስጥ ያሉ ትምህርቶች ", + "practice": "ልምምድ", + "start": "ጀምር", + "in_progress": "በሂደት ላይ", + "hello": "ሰላም", + "ready_to_learn": " ዛሬ እንግሊዝኛ ለመማር ተዘጋጅተዋል? ", + "learn": "ይማሩ ", + "course": "ኮርስ", + "profile": " ፕሮፋይል ", + "speaking_partner": "የንግግር ጓደኛ", + "practice_what_you_learned": "አሁን የተማሩትን እንለማመድ", + "practice_questions": "ጥቂት ጥያቄዎችን እጠይቃለሁ እና መልስ መስጠት ይችላሉ", + "start_practice": "ልምምድ ጀምር", + "almost_there": "ሊጨርሱ ተቃርበዋል", + "finish_session": "እድገትዎን ለማየት ክፍለ ጊዜውን ያጠናቅቁ", + "continue_practice": "ልምምዱን ይቀጥሉ", + "end_session": "ክፍለ ጊዜውን ያብቁ ", + "tap_start_to_listen": "ለማዳመጥ የጀምር ቁልፉን ይጫኑ", + "practice_speaking": "ንግግርን ይለማመዱ", + "tap_microphone": "ለመናገር ማይክሮፎኑን ይጫኑ", + "reply": "እንደገና አዳምጥ", + "cancel": "ይቅር", + "you_are_speaking": "እየተናገሩ ነው", + "practice_completed": "ልምምዱ ተጠናቅቋል", + "great_improvement": "በዚህኛው በራስ መተማመንዎ ጨምሯል፤ ትልቅ መሻሻል ነው", + "practice_again": "እንደገና ይለማመዱ", + "conversation_review": "የንግግር ግምገማ", + "result": "ውጤት", + "quick_tip": "ጠቃሚ ምክር", + "retry": "እንደገና ይሞክሩ", + "completed_a1": "እንኳን ደስ አለዎት! A1 ደረጃን አጠናቅቀዋል", + "analyzing_speaking": "የንግግር ችሎታዎን እየገመገምን ነው", + "view_profile": "ፕሮፋይሎን ይመልከቱ ", + "hi": "ሰላም", + "edit_profile": "መገለጫ ያስተካክሉ", + "first_name": "የመጀመሪያ ስም", + "last_name": "የአባት ስም", + "gender": "ፆታ", + "male": "ወንድ", + "female": "ሴት", + "phone_number": "የስልክ ቁጥር", + "country": "ሀገር", + "region": "ክልል", + "select_region": "ክልል ይምረጡ", + "enter_your_city": "ከተማዎን ያስገቡ", + "occupation": "የስራ መስክ", + "select_occupation": "ሙያዎን ይምረጡ", + "save_changes": "ለውጦችን ያስቀምጡ", + "my_progress": "የእኔ እድገት", + "track_your_achievement": "ስኬቶችዎን እና ተከታታይ የትምህርት ጉዞዎን ይከታተሉ", + "account_and_privacy": "መለያ እና ግላዊነት", + "manage_settings": "ቅንብሮችን እና የመተግበሪያ ምርጫዎችን ያስተዳድሩ", + "support": "ድጋፍ", + "get_help": "በስልክ ወይም በቴሌግራም እገዛ ያግኙ", + "logout": "ውጣ", + "app_settings": "የመተግበሪያ ቅንብሮች", + "legal_and_information": "ሕጋዊ እና መረጃ", + "change_language": "ቋንቋ ቀይር", + "terms_and_conditions": "ውሎች እና ሁኔታዎች", + "delete_account": "መለያ ሰርዝ", + "language_preference": "የቋንቋ ምርጫ", + "choose_your_language": "ለውጦችን አስቀምጥ", + "switch_language_anytime": "ቋንቋዎችን በማንኛውም ጊዜ መቀየር ይችላሉ", + "need_help": "እገዛ ይፈልጋሉ?", + "call_support": "የስልክ ድጋፍ", + "talk_with_support": "በቀጥታ ከድጋፍ ቡድናችን ጋር ይነጋገሩ", + "telegram_support": "የቴሌግራም ድጋፍ", + "chat_via_telegram": "በቴሌግራም በፍጥነት ይወያዩ", + "call_our_support": "ከ3 ጠዋት እስከ 12 ማታ ድረስ የድጋፍ ቡድናችንን ይደውሉ", + "tap_to_call": "ለመደወል ይንኩ", + "join_telegram": "በቴሌግራም የይማሩ አካዳሚን ይቀላቀሉ", + "connect_with_support_team": "ለፈጣን እርዳታ እና የማህበረሰብ ዝማኔዎች፣ በቴሌግራም ከድጋፍ ቡድናችን ጋር ወዲያውኑ ይገናኙ።", + "open_in_telegram": "በቴሌግራም ይክፈቱ", + "search_for": "ፈልጉት", + "current_level": "የአሁኑ ደረጃ", + "keep_up_the_great_work": "በጣም ጥሩ እየሰራህ ነው! ቀጥልበት፣ አስደናቂ ነህ።", + "no_practice_available": "ምንም ልምምድ አልተገኘም!", + "begin_module_practice": "የሞጁሉን ልምምድ ጀምር", + "lets_practice_lesson": "እንለማመድ", + "lets_quickly_review": "በዚህ ሞጁል ውስጥ የተማርከውን በፍጥነት እንከልስ!", + "lets_practice_module": "አሁን የተማርከውን እንለማመድ!", + "ask_you_few_actions": "ጥቂት ጥያቄዎችን እጠይቅሃለሁ፣ አንተም በተፈጥሮ መልስ ልትሰጥ ትችላለህ።", + "begin_level_practice": "የደረጃ ልምምድን ጀምር", + "lets_practice_course": "የኮርሱን ልምምድ እንለማመድ", + "lets_quick_review": "በዚህ ደረጃ የተማርከውን በፍጥነት እንከልስ!", + "speaking": "እየተናገረ ነው", + "you_have_finished_practice": "ልምምድህን አጠናቀቅህ", + "view_results": "ውጤቶቼን እይ", + "sample_answer": "ናሙና መልስ", + "your_answer": "መልስህ", + "sound_confident": "በዚህ ጊዜ የበለጠ እምነት ያለህ ይመስላል — በጣም ጥሩ መሻሻል ነው!", + "you_have_completed": "አያይ! አጠናቀቅህ", + "yes": "አዎ", + "no": "አይ", + "want_to_quit": "ለመውጣት እርግጠኛ ነህ?", + "required_field": "ይህ መስክ ያስፈልጋል", + "enter_full_name": "ሙሉ ስምህን አስገባ", + "invalid_email": "የማይሰራ የኢሜይል ቅርጸት", + "phone_must_start_with": "የስልክ ቁጥር በ251 መጀመር አለበት", + "phone_must_be": "የስልክ ቁጥር 12 አሃዞች መሆን አለበት", + "what_should_we_call_you": "ምን ብለን እንጠራህ?", + "name_for_personalization": "በመማር ጉዞህ ውስጥ ለግል ለማድረግ ስምህን እንጠቀማለን።", + "choose_your_gender": "ጾታህን ምረጥ", + "gender_for_personalization": "በጾታህ መሰረት የመማር ተሞክሮህን እናበጅለታለን።", + "age_range": "በየትኛው የእድሜ ክልል ውስጥ ነህ?", + "age_for_personalization": "በእድሜህ መሰረት የመማር ተሞክሮህን እናበጅለታለን።", + "educational_background": "አሁን ያለህ የትምህርት ደረጃ ምንድን ነው?", + "education_for_personalization": "ይህ ትምህርቶችን ከልምድህ ጋር እንዲስማሙ ለማድረግ ይረዳናል።", + "your_occupation": "ስራህ ምንድን ነው?", + "occupation_for_personalization": "በስራህ መሰረት የመማር ተሞክሮህን እናበጅለታለን።", + "location": "ከየት ነህ?", + "select_country_region": "አገርህን እና ክልልህን ከተቆልቋይ ዝርዝሩ ምረጥ", + "select_country": "አገር ምረጥ", + "learning_goal": "የመማር ዓላማህን ምረጥ", + "language_goal": "እንግሊዝኛህን ለማሻሻል ዋና ዓላማህ ምንድን ነው?", + "your_goal": "ዓላማህ የመማር ጉዞህን እንዲስማማ ለማድረግ ይረዳናል።", + "write_your_goal": "ዓላማህን ጻፍ…", + "challenge_you_face": "What challenge do you face most with English?", + "evey_one_has_strugle": "ሁሉም ሰው ችግሮች አሉት፣ የአንተን እንጀምር እንፍታ", + "write_your_challenge": "ችግርህን ጻፍ…", + "topic_interest": "በጣም የሚስቡህ ርዕሶች የትኞቹ ናቸው?", + "favourite_topic": "የምትወዳቸው ርዕሶች አስደሳች እና ከሕይወትህ ጋር የተዛመዱ ትምህርቶችን ለመፍጠር ይረዱናል።", + "your_interest": "ፍላጎትህን ጻፍ…", + "want_quick_assessment": "የእንግሊዝኛ ደረጃህን ለማወቅ ፈጣን ግምገማ ትፈልጋለህ?", + "answer_quick_questions": "የእንግሊዝኛ ችሎታህን ለመረዳት ጥቂት ፈጣን ጥያቄዎችን መልስ።", + "skip": "ዝለል", + "finish_level": "ደረጃውን አጠናቅቅ", + "likely_speaker": "አንተ ምናልባት ተናጋሪ ነህ", + "great_job": "በጣም ጥሩ ስራ! ለመሻሻል ቀጣዩ ደረጃህ ይኸው ነው።", + "lets_start_practice": "ልምምድህን እንጀምር", + "welcome_abroad": "እንኳን ደህና መጣህ", + "ready_to_explore": "የግል ትምህርቶችህን ለማሰስ ዝግጁ ነህ።", + "finish": "አጠናቅቅ", + "finish_all_practice_lesson": "ይህን ልምምድ ለመውሰድ የቀድሞውን የትምህርት ልምምድ ያጠናቅቁ", + "finish_all_practice_module": "የሞጁሉን ልምምድ ለመውሰድ የትምህርት ልምምዶችን ያጠናቅቁ", + "finish_all_practice_course": "የኮርሱን ልምምድ ለመውሰድ የሞጁል ልምምዶችን ያጠናቅቁ", + "finish_all_practice_previouse_module": "ይህን ልምምድ ለመውሰድ የቀድሞውን የሞጁል ልምምድ ያጠናቅቁ", + "finish_all_practice_previouse_course": "ይህን ለመውሰድ የቀድሞውን የኮርስ ልምምድ ያጠናቅቁ", + "track_journey": "የትምህርት ጉዞዎን ይከታተሉ እና በጊዜ ሂደት ያሳዩትን እድገት ይመልከቱ።", + "learn_english": "እንግሊዝኛ ይማሩ", + "keep_momentum": "በጣም ጥሩ ስራ! በዚሁ ብርታት ይቀጥሉ።", + "completed_practices": "የተጠናቀቁ ልምምዶች", + "total_practices": "ጠቅላላ ልምምዶች", + "progress_percentage": "የእድገት መቶኛ" +}; +static const Map> mapLocales = {"en": _en, "am": _am}; } diff --git a/lib/ui/common/translations/locale_keys.g.dart b/lib/ui/common/translations/locale_keys.g.dart index c6d4cbc..1072f9d 100644 --- a/lib/ui/common/translations/locale_keys.g.dart +++ b/lib/ui/common/translations/locale_keys.g.dart @@ -13,12 +13,14 @@ abstract class LocaleKeys { static const cont = 'cont'; static const register = 'register'; static const login_with_google = 'login_with_google'; + static const login_with_apple = 'login_with_apple'; static const or = 'or'; static const login_with_phone = 'login_with_phone'; static const create_account = 'create_account'; static const already_have_account = 'already_have_account'; static const login = 'login'; static const register_with_google = 'register_with_google'; + static const register_with_apple = 'register_with_apple'; static const register_with_phone = 'register_with_phone'; static const enter_phone_number = 'enter_phone_number'; static const login_with_email = 'login_with_email'; @@ -43,10 +45,10 @@ abstract class LocaleKeys { static const reset_code = 'reset_code'; static const new_password = 'new_password'; static const logged_in_successfully = 'logged_in_successfully'; - static const view_course = 'view_course'; static const continue_learning = 'continue_learning'; static const start_learning = 'start_learning'; static const completed = 'completed'; + static const view_course = 'view_course'; static const take_practice = 'take_practice'; static const your_current_level = 'your_current_level'; static const overall_progress = 'overall_progress'; diff --git a/lib/ui/views/login/login_viewmodel.dart b/lib/ui/views/login/login_viewmodel.dart index ca70a80..6dc8b83 100644 --- a/lib/ui/views/login/login_viewmodel.dart +++ b/lib/ui/views/login/login_viewmodel.dart @@ -7,6 +7,7 @@ import 'package:yimaru_app/app/app.router.dart'; import 'package:yimaru_app/models/user.dart'; import '../../../services/api_service.dart'; +import '../../../services/apple_auth_service.dart'; import '../../../services/authentication_service.dart'; import '../../../services/google_auth_service.dart'; import '../../../services/localization_service.dart'; @@ -25,19 +26,23 @@ class LoginViewModel extends ReactiveViewModel final _googleAuthService = locator(); + final _appleAuthService = locator(); + final _localizationService = locator(); final _authenticationService = locator(); @override List get listenableServices => - [_googleAuthService, _localizationService]; + [_googleAuthService, _appleAuthService, _localizationService]; // Google user GoogleSignInAccount? get _googleUser => _googleAuthService.googleUser; GoogleSignInAccount? get googleUser => _googleUser; + bool get isAppleSignInAvailable => _appleAuthService.isSupported; + // Languages Map get _selectedLanguage => _localizationService.selectedLanguage; @@ -235,6 +240,50 @@ class LoginViewModel extends ReactiveViewModel Future signInWithGoogle() async => await runBusyFuture(_googleAuth(), busyObject: StateObjects.loginWithGoogle); + // Sign-in with Apple + Future _appleAuth() async { + if (await _statusChecker.checkConnection()) { + await _appleAuthService.appleAuth(); + + final credential = _appleAuthService.appleCredential; + final identityToken = credential?.identityToken; + + if (identityToken == null || identityToken.isEmpty) { + showErrorToast('Apple login failed. Please try again.'); + return; + } + + Map data = { + 'id_token': identityToken, + 'email': credential?.email, + 'first_name': credential?.givenName, + 'last_name': credential?.familyName, + }; + + data.removeWhere((_, value) => value == null || value == ''); + + Map response = await _apiService.appleAuth(data); + + if (response['status'] == ResponseStatus.success) { + User user = response['data'] as User; + Map data = { + 'userId': user.userId, + 'accessToken': user.accessToken, + 'refreshToken': user.refreshToken + }; + await _authenticationService.saveUserCredential(data); + clearUserData(); + await replaceWithStartUp(); + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + } + } + + Future signInWithApple() async => await runBusyFuture(_appleAuth(), + busyObject: StateObjects.loginWithApple); + // Login with phone Future loginWithPhoneNumber() async => await runBusyFuture(_loginWithPhoneNumber(), diff --git a/lib/ui/views/login/screens/login_with_email_screen.dart b/lib/ui/views/login/screens/login_with_email_screen.dart index 6ae08bd..23579f4 100644 --- a/lib/ui/views/login/screens/login_with_email_screen.dart +++ b/lib/ui/views/login/screens/login_with_email_screen.dart @@ -66,7 +66,8 @@ class LoginWithEmailScreen extends ViewModelWidget { Stack(children: [ _buildScaffold(context: context, viewModel: viewModel), _buildLoginWithEmailState(viewModel), - _buildLoginWithGoogleState(viewModel) + _buildLoginWithGoogleState(viewModel), + _buildLoginWithAppleState(viewModel) ]); Widget _buildScaffold( @@ -232,6 +233,8 @@ class LoginWithEmailScreen extends ViewModelWidget { List _buildLowerColumnChildren(LoginViewModel viewModel) => [ _buildContinueButton(viewModel), _buildLoginWithGoogleButton(viewModel), + if (viewModel.isAppleSignInAvailable) + _buildLoginWithAppleButton(viewModel), _buildOptionTextDivider(), _buildLoginWithPhoneButton(viewModel), verticalSpaceMedium @@ -265,6 +268,18 @@ class LoginWithEmailScreen extends ViewModelWidget { onTap: () async => await viewModel.signInWithGoogle(), ); + Widget _buildLoginWithAppleButton(LoginViewModel viewModel) => + CustomElevatedButton( + height: 55, + borderRadius: 12, + backgroundColor: kcWhite, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + text: LocaleKeys.login_with_apple.tr(), + leadingIcon: Icons.apple, + onTap: () async => await viewModel.signInWithApple(), + ); + Widget _buildOptionTextDivider() => const OptionTextDivider(); Widget _buildLoginWithPhoneButton(LoginViewModel viewModel) => @@ -287,4 +302,9 @@ class LoginWithEmailScreen extends ViewModelWidget { viewModel.busy(StateObjects.loginWithGoogle) ? const PageLoadingIndicator() : Container(); + + Widget _buildLoginWithAppleState(LoginViewModel viewModel) => + viewModel.busy(StateObjects.loginWithApple) + ? const PageLoadingIndicator() + : Container(); } diff --git a/lib/ui/views/register/register_viewmodel.dart b/lib/ui/views/register/register_viewmodel.dart index b1fc0ff..df9386b 100644 --- a/lib/ui/views/register/register_viewmodel.dart +++ b/lib/ui/views/register/register_viewmodel.dart @@ -10,6 +10,7 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import '../../../app/app.locator.dart'; import '../../../models/user.dart'; +import '../../../services/apple_auth_service.dart'; import '../../../services/google_auth_service.dart'; import '../../../services/localization_service.dart'; import '../../../services/status_checker_service.dart'; @@ -25,19 +26,23 @@ class RegisterViewModel extends ReactiveViewModel final _googleAuthService = locator(); + final _appleAuthService = locator(); + final _localizationService = locator(); final _authenticationService = locator(); @override List get listenableServices => - [_googleAuthService, _localizationService]; + [_googleAuthService, _appleAuthService, _localizationService]; // Google user GoogleSignInAccount? get _googleUser => _googleAuthService.googleUser; GoogleSignInAccount? get googleUser => _googleUser; + bool get isAppleSignInAvailable => _appleAuthService.isSupported; + // Languages Map get _selectedLanguage => _localizationService.selectedLanguage; @@ -337,6 +342,50 @@ class RegisterViewModel extends ReactiveViewModel } } + // Register with Apple + Future registerWithApple() async => await runBusyFuture(_appleLogin(), + busyObject: StateObjects.registerWithApple); + + Future _appleLogin() async { + if (await _statusChecker.checkConnection()) { + await _appleAuthService.appleAuth(); + + final credential = _appleAuthService.appleCredential; + final identityToken = credential?.identityToken; + + if (identityToken == null || identityToken.isEmpty) { + showErrorToast('Apple login failed. Please try again.'); + return; + } + + Map data = { + 'id_token': identityToken, + 'email': credential?.email, + 'first_name': credential?.givenName, + 'last_name': credential?.familyName, + }; + + data.removeWhere((_, value) => value == null || value == ''); + + Map response = await _apiService.appleAuth(data); + + if (response['status'] == ResponseStatus.success) { + User user = response['data'] as User; + Map data = { + 'userId': user.userId, + 'accessToken': user.accessToken, + 'refreshToken': user.refreshToken + }; + await _authenticationService.saveUserCredential(data); + clearUserData(); + await replaceWithStartUp(); + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + } + } + Future verifyOtp() async => await runBusyFuture(_verifyOtp(), busyObject: StateObjects.verifyOtp); diff --git a/lib/ui/views/register/screens/register_with_email_screen.dart b/lib/ui/views/register/screens/register_with_email_screen.dart index 32b2285..2e9fcc9 100644 --- a/lib/ui/views/register/screens/register_with_email_screen.dart +++ b/lib/ui/views/register/screens/register_with_email_screen.dart @@ -65,7 +65,9 @@ class RegisterWithEmailScreen extends ViewModelWidget { Stack( children: [ _buildScaffold(context: context, viewModel: viewModel), - _buildRegisterWithEmailState(viewModel) + _buildRegisterWithEmailState(viewModel), + _buildRegisterWithGoogleState(viewModel), + _buildRegisterWithAppleState(viewModel) ], ); @@ -191,6 +193,8 @@ class RegisterWithEmailScreen extends ViewModelWidget { List _buildLowerColumnChildren(RegisterViewModel viewModel) => [ _buildContinueButton(viewModel), _buildRegisterWithGoogleButton(viewModel), + if (viewModel.isAppleSignInAvailable) + _buildRegisterWithAppleButton(viewModel), _buildOptionTextDivider(), _buildRegisterWithEmailButton(viewModel), verticalSpaceMedium @@ -225,6 +229,18 @@ class RegisterWithEmailScreen extends ViewModelWidget { onTap: () async => await viewModel.registerWithGoogle(), ); + Widget _buildRegisterWithAppleButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + borderRadius: 12, + backgroundColor: kcWhite, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + text: LocaleKeys.register_with_apple.tr(), + leadingIcon: Icons.apple, + onTap: () async => await viewModel.registerWithApple(), + ); + Widget _buildOptionTextDivider() => const OptionTextDivider(); Widget _buildRegisterWithEmailButton(RegisterViewModel viewModel) => @@ -243,4 +259,14 @@ class RegisterWithEmailScreen extends ViewModelWidget { viewModel.busy(StateObjects.register) ? const PageLoadingIndicator() : Container(); + + Widget _buildRegisterWithGoogleState(RegisterViewModel viewModel) => + viewModel.busy(StateObjects.registerWithGoogle) + ? const PageLoadingIndicator() + : Container(); + + Widget _buildRegisterWithAppleState(RegisterViewModel viewModel) => + viewModel.busy(StateObjects.registerWithApple) + ? const PageLoadingIndicator() + : Container(); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index eb9631b..1ccd1dd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -17,6 +17,7 @@ import google_sign_in_ios import package_info_plus import record_macos import shared_preferences_foundation +import sign_in_with_apple import sqflite_darwin import url_launcher_macos import video_player_avfoundation @@ -35,6 +36,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index d4216d7..57b8d36 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1557,6 +1557,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + sign_in_with_apple: + dependency: "direct main" + description: + name: sign_in_with_apple + sha256: "5568378c3cc5993931955357d85e4c3344fa4365006915bdef965fa3de1dc0a5" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + sign_in_with_apple_platform_interface: + dependency: transitive + description: + name: sign_in_with_apple_platform_interface + sha256: "981bca52cf3bb9c3ad7ef44aace2d543e5c468bb713fd8dda4275ff76dfa6659" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + sign_in_with_apple_web: + dependency: transitive + description: + name: sign_in_with_apple_web + sha256: f316400827f52cafcf50d00e1a2e8a0abc534ca1264e856a81c5f06bd5b10fed + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -2011,5 +2035,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" + dart: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" diff --git a/pubspec.yaml b/pubspec.yaml index dde6e73..d971fa4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: flutter_phone_direct_caller: ^2.2.1 flutter_local_notifications: ^20.1.0 internet_connection_checker_plus: ^2.9.1+2 + sign_in_with_apple: ^8.0.0 dev_dependencies: flutter_test: