diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b50b711..9752eb6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,11 @@ on: - main pull_request: +env: + FLUTTER_CHANNEL: "stable" + FLUTTER_VERSION: "3.32.8" + RUBY_VERSION: "3.2.2" + jobs: analyze: name: Analyze code (SAST) @@ -17,8 +22,8 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.32.8" - channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: ${{ env.FLUTTER_CHANNEL }} cache: true - name: Generate code run: flutter pub run build_runner build --delete-conflicting-outputs @@ -36,8 +41,8 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.32.8" - channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: ${{ env.FLUTTER_CHANNEL }} cache: true - name: Run tests run: flutter test @@ -53,8 +58,8 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.32.8" - channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: ${{ env.FLUTTER_CHANNEL }} cache: true - name: Create key.jks @@ -81,58 +86,92 @@ jobs: name: apk path: build/app/outputs/flutter-apk/ - deploy-firebase: - name: Deploy to Firebase + distribute: + name: Upload artifact to Firebase App Distribution runs-on: ubuntu-latest needs: build-android - if: github.ref == 'refs/heads/main' + if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - - name: Checkout + - name: Checkout repository uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: "3.4.5" - bundler-cache: true - working-directory: "android" - - - name: Set up Flutter - uses: subosito/flutter-action@v2 + - name: Download app APK + uses: actions/download-artifact@v4 with: - flutter-version: "3.32.8" - channel: "stable" - cache: true - - - name: Create google_service_account.json - run: | - echo "${{ secrets.GOOGLE_SERVICES_ACCOUNT_BASE64 }}" | base64 --decode > google_service_account.json - - - name: Create firebase google_service_account.json + name: apk + - name: Upload artifact to Firebase App Distribution + id: uploadArtifact env: - CREDENTIAL_FILE_CONTENT: ${{ secrets.CREDENTIAL_FILE_CONTENT }} - run: | - cat <<< "$CREDENTIAL_FILE_CONTENT" > service_credentials_content.json - - - name: Create key.jks + INPUT_APPID: ${{secrets.FIREBASE_APP_ID}} + INPUT_SERVICECREDENTIALSFILECONTENT: ${{ secrets.CREDENTIAL_FILE_CONTENT }} + GOOGLE_APPLICATION_CREDENTIALS: service_credentials_content.json + INPUT_GROUPS: testers + INPUT_FILE: app-release.apk run: | - echo "${{ secrets.ANDROID_KEYSTORE_FILE_BASE64 }}" | base64 --decode > android/key.jks - - - name: Create key.properties - run: | - cat < android/key.properties - storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - keyPassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - keyAlias=release - storeFile=../key.jks - EOF - env: - ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - - - name: Deploy to Firebase - uses: maierj/fastlane-action@v3.1.0 - with: - lane: "firebase" - subdirectory: android - env: - APP_PACKAGE_NAME: ${{ vars.APP_PACKAGE_NAME }} + cat <<< "${INPUT_SERVICECREDENTIALSFILECONTENT}" > service_credentials_content.json + sudo npm install -g firebase-tools + firebase appdistribution:distribute "$INPUT_FILE" --app "$INPUT_APPID" --groups "$INPUT_GROUPS" --testers "$INPUT_TESTERS" --release-notes "$(git log -1 --pretty=short)" + # deploy-firebase: + # name: Distribute existing APK to Firebase App Distribution + # runs-on: ubuntu-latest + # needs: build-android + # # if: github.ref == 'refs/heads/main' + # steps: + # - name: Checkout + # uses: actions/checkout@v4 + + # - name: Set up Ruby + # uses: ruby/setup-ruby@v1 + # with: + # ruby-version: ${{ env.RUBY_VERSION }} + # bundler-cache: true + # working-directory: "android" + + # - name: Set up Flutter + # uses: subosito/flutter-action@v2 + # with: + # flutter-version: ${{ env.FLUTTER_VERSION }} + # channel: ${{ env.FLUTTER_CHANNEL }} + # cache: true + + # - name: Create google_service_account.json + # run: | + # echo "${{ secrets.GOOGLE_SERVICES_ACCOUNT_BASE64 }}" | base64 --decode > google_service_account.json + + # - name: Create firebase google_service_account.json + # env: + # CREDENTIAL_FILE_CONTENT: ${{ secrets.CREDENTIAL_FILE_CONTENT }} + # run: | + # cat <<< "$CREDENTIAL_FILE_CONTENT" > service_credentials_content.json + + # - name: Create key.jks + # run: | + # echo "${{ secrets.ANDROID_KEYSTORE_FILE_BASE64 }}" | base64 --decode > android/key.jks + + # - name: Create key.properties + # run: | + # cat < android/key.properties + # storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + # keyPassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + # keyAlias=release + # storeFile=../key.jks + # EOF + # env: + # ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + + # - name: Deploy existing APK to Firebase + # uses: maierj/fastlane-action@v3.1.0 + # with: + # lane: "distribute_existing_apk" + # env: + # APP_PACKAGE_NAME: ${{ vars.APP_PACKAGE_NAME }} + # FIREBASE_APP_ID: ${{ vars.FIREBASE_APP_ID }} + # APK_PATH: ${{ vars.APK_PATH }} + + # - name: Deploy to Firebase + # uses: maierj/fastlane-action@v3.1.0 + # with: + # lane: "release_play_store_using_firebase" + # subdirectory: android + # env: + # APP_PACKAGE_NAME: ${{ vars.APP_PACKAGE_NAME }} + # FIREBASE_APP_ID: ${{ vars.FIREBASE_APP_ID }} diff --git a/.github/workflows/deploy-with-fastlane.yaml b/.github/workflows/deploy-with-fastlane.yaml index 9d308197..f0e2c1fc 100644 --- a/.github/workflows/deploy-with-fastlane.yaml +++ b/.github/workflows/deploy-with-fastlane.yaml @@ -6,13 +6,12 @@ on: push: tags: - "v*" - - "v*-rc.*" - "*.*.*" env: FLUTTER_CHANNEL: "stable" FLUTTER_VERSION: "3.32.8" - RUBY_VERSION: "3.4.5" #"3.2.2" + RUBY_VERSION: "3.2.2" # 3.2.2 jobs: # build_ios: diff --git a/assets/translations/de.json b/assets/translations/de.json index 838e4d58..b1213414 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -322,5 +322,9 @@ "or": "ODER", "proceedWithGoogle": "Mit Google fortfahren", "proceedWithApple": "Mit Apple fortfahren", - "emailPhoneValidateDesc": "Entweder eine E-Mail oder eine Telefonnummer muss angegeben werden." + "emailPhoneValidateDesc": "Entweder eine E-Mail oder eine Telefonnummer muss angegeben werden.", + "logoutWarningTitle": "Nicht synchronisierte Daten", + "logoutWarningMessage": "Sie haben nicht synchronisierte Daten, die verloren gehen, wenn Sie sich jetzt abmelden. Möchten Sie fortfahren?", + "syncNow": "Jetzt synchronisieren", + "logoutAnyway": "Trotzdem abmelden" } diff --git a/assets/translations/en.json b/assets/translations/en.json index 891483e2..9cc744c7 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -322,5 +322,9 @@ "or": "OR", "proceedWithGoogle": "Proceed with Google", "proceedWithApple": "Proceed with Apple", - "emailPhoneValidateDesc": "Either email or phone must be provided." + "emailPhoneValidateDesc": "Either email or phone must be provided.", + "logoutWarningTitle": "Unsynchronized Data", + "logoutWarningMessage": "You have unsynchronized data that will be lost if you logout now. Do you want to continue?", + "syncNow": "Sync Now", + "logoutAnyway": "Logout Anyway" } diff --git a/assets/translations/es.json b/assets/translations/es.json index c3810f03..da802be8 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -321,5 +321,9 @@ "or": "O", "proceedWithGoogle": "Continuar con Google", "proceedWithApple": "Continuar con Apple", - "emailPhoneValidateDesc": "Se debe proporcionar un correo electrónico o un número de teléfono." + "emailPhoneValidateDesc": "Se debe proporcionar un correo electrónico o un número de teléfono.", + "logoutWarningTitle": "Datos no sincronizados", + "logoutWarningMessage": "Tienes datos no sincronizados que se perderán si cierras sesión ahora. ¿Quieres continuar?", + "syncNow": "Sincronizar ahora", + "logoutAnyway": "Cerrar sesión de todos modos" } diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 283deaa4..d385540c 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -320,5 +320,9 @@ "or": "OU", "proceedWithGoogle": "Continuer avec Google", "proceedWithApple": "Continuer avec Apple", - "emailPhoneValidateDesc": "Un e-mail ou un numéro de téléphone doit être fourni." + "emailPhoneValidateDesc": "Un e-mail ou un numéro de téléphone doit être fourni.", + "logoutWarningTitle": "Données non synchronisées", + "logoutWarningMessage": "Vous avez des données non synchronisées qui seront perdues si vous vous déconnectez maintenant. Voulez-vous continuer ?", + "syncNow": "Synchroniser maintenant", + "logoutAnyway": "Se déconnecter quand même" } diff --git a/assets/translations/it.json b/assets/translations/it.json index 3345925e..6961f1f8 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -322,5 +322,9 @@ "or": "O", "proceedWithGoogle": "Continua con Google", "proceedWithApple": "Continua con Apple", - "emailPhoneValidateDesc": "È necessario fornire un'email o un numero di telefono." + "emailPhoneValidateDesc": "È necessario fornire un'email o un numero di telefono.", + "logoutWarningTitle": "Dati non sincronizzati", + "logoutWarningMessage": "Hai dati non sincronizzati che andranno persi se esci ora. Vuoi continuare?", + "syncNow": "Sincronizza ora", + "logoutAnyway": "Esci comunque" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 53e744a6..aa45171f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,14 @@ PODS: + - AppAuth (2.0.0): + - AppAuth/Core (= 2.0.0) + - AppAuth/ExternalUserAgent (= 2.0.0) + - AppAuth/Core (2.0.0) + - AppAuth/ExternalUserAgent (2.0.0): + - AppAuth/Core + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) - connectivity_plus (0.0.1): - Flutter - DKImagePickerController/Core (4.3.9): @@ -116,6 +126,11 @@ PODS: - Flutter - flutter_secure_storage (6.0.0): - Flutter + - google_sign_in_ios (0.0.1): + - Flutter + - FlutterMacOS + - GoogleSignIn (~> 9.0) + - GTMSessionFetcher (>= 3.4.0) - GoogleAdsOnDeviceConversion (2.1.0): - GoogleUtilities/Logger (~> 8.1) - GoogleUtilities/Network (~> 8.1) @@ -145,6 +160,11 @@ PODS: - GoogleDataTransport (10.1.0): - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) + - GoogleSignIn (9.0.0): + - AppAuth (~> 2.0) + - AppCheckCore (~> 11.0) + - GTMAppAuth (~> 5.0) + - GTMSessionFetcher/Core (~> 3.3) - GoogleUtilities/AppDelegateSwizzler (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Logger @@ -172,6 +192,14 @@ PODS: - GoogleUtilities/UserDefaults (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy + - GTMAppAuth (5.0.0): + - AppAuth/Core (~> 2.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher (3.5.0): + - GTMSessionFetcher/Full (= 3.5.0) + - GTMSessionFetcher/Core (3.5.0) + - GTMSessionFetcher/Full (3.5.0): + - GTMSessionFetcher/Core - image_cropper (0.0.4): - Flutter - TOCropViewController (~> 2.7.4) @@ -240,6 +268,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_device_uuid (from `.symlinks/plugins/flutter_device_uuid/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) - image_cropper (from `.symlinks/plugins/image_cropper/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -251,6 +280,8 @@ DEPENDENCIES: SPEC REPOS: trunk: + - AppAuth + - AppCheckCore - DKImagePickerController - DKPhotoGallery - FCUUID @@ -266,7 +297,10 @@ SPEC REPOS: - GoogleAdsOnDeviceConversion - GoogleAppMeasurement - GoogleDataTransport + - GoogleSignIn - GoogleUtilities + - GTMAppAuth + - GTMSessionFetcher - nanopb - PromisesObjC - PromisesSwift @@ -295,6 +329,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_device_uuid/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + google_sign_in_ios: + :path: ".symlinks/plugins/google_sign_in_ios/darwin" image_cropper: :path: ".symlinks/plugins/image_cropper/ios" image_picker_ios: @@ -313,6 +349,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: + AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063 + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 @@ -334,10 +372,14 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_device_uuid: 8df3bc29405e4f2f999c206cce4ecf33cc1f6a21 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + google_sign_in_ios: 205742c688aea0e64db9da03c33121694a365109 GoogleAdsOnDeviceConversion: 2be6297a4f048459e0ae17fad9bfd2844e10cf64 GoogleAppMeasurement: 700dce7541804bec33db590a5c496b663fbe2539 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleSignIn: c7f09cfbc85a1abf69187be091997c317cc33b77 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238 + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 image_cropper: c4326ea50132b1e1564499e5d32a84f01fb03537 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index dffa7f8d..632db1f3 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -202,6 +202,7 @@ 3B06AD1E1E4923F5004D2608 /* Thin Binary */, F6E451D6859606ACB7038B99 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, 815E79B7E22BFBC575E55C45 /* [CP] Embed Pods Frameworks */, + 3FE48A3652FBD4FE3D47E82F /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -312,6 +313,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 3FE48A3652FBD4FE3D47E82F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 815E79B7E22BFBC575E55C45 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/lib/di/injection.config.dart b/lib/di/injection.config.dart index 461eb2bd..84243894 100644 --- a/lib/di/injection.config.dart +++ b/lib/di/injection.config.dart @@ -119,6 +119,7 @@ import '../domain/usecases/party/listen_to_parties_usecase.dart' as _i714; import '../domain/usecases/party/update_party_usecase.dart' as _i911; import '../domain/usecases/subscription/fetch_subscription_usecase.dart' as _i314; +import '../domain/usecases/sync/check_pending_changes_usecase.dart' as _i662; import '../domain/usecases/transaction/create_transaction_usecase.dart' as _i669; import '../domain/usecases/transaction/delete_transaction_usecase.dart' @@ -181,6 +182,8 @@ _i174.GetIt $initGetIt( () => _i529.FirebaseCrashlyticsService()); gh.singleton<_i683.PreferenceManager>( () => _i683.PreferenceManagerImpl()..init()); + gh.factory<_i662.CheckPendingChangesUsecase>( + () => _i662.CheckPendingChangesUsecase(gh<_i704.AppDatabase>())); gh.factory<_i655.PartyLocalDataSource>( () => _i655.PartyLocalDataSourceImpl(gh<_i704.AppDatabase>())); gh.factory<_i276.AuthLocalDataSource>( @@ -243,14 +246,14 @@ _i174.GetIt $initGetIt( localDataSource: gh<_i900.ExchangeRateLocalDataSource>(), onboardingLocalDataSource: gh<_i480.OnboardingLocalDataSource>(), )); - gh.factory<_i445.AddCategoryUseCase>( - () => _i445.AddCategoryUseCase(gh<_i410.CategoryRepository>())); gh.factory<_i986.UpdateCategoryUseCase>( () => _i986.UpdateCategoryUseCase(gh<_i410.CategoryRepository>())); - gh.factory<_i961.GetCategoriesUseCase>( - () => _i961.GetCategoriesUseCase(gh<_i410.CategoryRepository>())); gh.factory<_i292.DeleteCategoryUseCase>( () => _i292.DeleteCategoryUseCase(gh<_i410.CategoryRepository>())); + gh.factory<_i445.AddCategoryUseCase>( + () => _i445.AddCategoryUseCase(gh<_i410.CategoryRepository>())); + gh.factory<_i961.GetCategoriesUseCase>( + () => _i961.GetCategoriesUseCase(gh<_i410.CategoryRepository>())); gh.singleton<_i11.CloudBenefitRepository>(() => _i415.CloudBenefitRepositoryImpl( gh<_i61.CloudBenefitRemoteDataSource>())); @@ -291,20 +294,20 @@ _i174.GetIt $initGetIt( gh<_i704.AppDatabase>(), gh<_i478.GroupRemoteDataSource>(), )); - gh.factory<_i100.VerifyEmailUseCase>( - () => _i100.VerifyEmailUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i400.LoginWithPhoneUseCase>( - () => _i400.LoginWithPhoneUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i494.PasswordResetUseCase>( - () => _i494.PasswordResetUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i402.GetOtpCodeUseCase>( - () => _i402.GetOtpCodeUseCase(gh<_i800.AuthRepository>())); gh.factory<_i705.RegisterUseCase>( () => _i705.RegisterUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i542.PasswordResetCodeUseCase>( - () => _i542.PasswordResetCodeUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i402.GetOtpCodeUseCase>( + () => _i402.GetOtpCodeUseCase(gh<_i800.AuthRepository>())); gh.factory<_i524.LoginWithEmailUseCase>( () => _i524.LoginWithEmailUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i542.PasswordResetCodeUseCase>( + () => _i542.PasswordResetCodeUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i400.LoginWithPhoneUseCase>( + () => _i400.LoginWithPhoneUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i494.PasswordResetUseCase>( + () => _i494.PasswordResetUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i100.VerifyEmailUseCase>( + () => _i100.VerifyEmailUseCase(gh<_i800.AuthRepository>())); gh.factory<_i455.CategoryCubit>(() => _i455.CategoryCubit( gh<_i445.AddCategoryUseCase>(), gh<_i986.UpdateCategoryUseCase>(), @@ -312,43 +315,43 @@ _i174.GetIt $initGetIt( gh<_i961.GetCategoriesUseCase>(), gh<_i500.ListenToCategoriesUseCase>(), )); - gh.factory<_i768.LoginWithEmailPassword>( - () => _i768.LoginWithEmailPassword(gh<_i800.AuthRepository>())); - gh.factory<_i723.LoginWithPhonePassword>( - () => _i723.LoginWithPhonePassword(gh<_i800.AuthRepository>())); - gh.factory<_i880.GetLoggedInUser>( - () => _i880.GetLoggedInUser(gh<_i800.AuthRepository>())); - gh.factory<_i2.OnboardingCompleted>( - () => _i2.OnboardingCompleted(gh<_i800.AuthRepository>())); - gh.factory<_i828.IsOnboardingCompleted>( - () => _i828.IsOnboardingCompleted(gh<_i800.AuthRepository>())); - gh.factory<_i498.LoginByPhoneUsecase>( - () => _i498.LoginByPhoneUsecase(gh<_i800.AuthRepository>())); gh.factory<_i42.LoginByEmailUsecase>( () => _i42.LoginByEmailUsecase(gh<_i800.AuthRepository>())); + gh.factory<_i498.LoginByPhoneUsecase>( + () => _i498.LoginByPhoneUsecase(gh<_i800.AuthRepository>())); + gh.factory<_i828.IsOnboardingCompleted>( + () => _i828.IsOnboardingCompleted(gh<_i800.AuthRepository>())); + gh.factory<_i723.LoginWithPhonePassword>( + () => _i723.LoginWithPhonePassword(gh<_i800.AuthRepository>())); gh.factory<_i444.StreamAuthStatus>( () => _i444.StreamAuthStatus(gh<_i800.AuthRepository>())); + gh.factory<_i768.LoginWithEmailPassword>( + () => _i768.LoginWithEmailPassword(gh<_i800.AuthRepository>())); + gh.factory<_i2.OnboardingCompleted>( + () => _i2.OnboardingCompleted(gh<_i800.AuthRepository>())); + gh.factory<_i880.GetLoggedInUser>( + () => _i880.GetLoggedInUser(gh<_i800.AuthRepository>())); gh.factory<_i831.RegisterCubit>(() => _i831.RegisterCubit( gh<_i705.RegisterUseCase>(), gh<_i402.GetOtpCodeUseCase>(), gh<_i100.VerifyEmailUseCase>(), )); + gh.factory<_i243.SaveOnboardingState>( + () => _i243.SaveOnboardingState(gh<_i867.OnboardingRepository>())); gh.factory<_i575.GetOnboardingState>( () => _i575.GetOnboardingState(gh<_i867.OnboardingRepository>())); gh.factory<_i332.GetOnboardingStateStream>( () => _i332.GetOnboardingStateStream(gh<_i867.OnboardingRepository>())); - gh.factory<_i243.SaveOnboardingState>( - () => _i243.SaveOnboardingState(gh<_i867.OnboardingRepository>())); gh.factory<_i56.DeletePartyUseCase>( () => _i56.DeletePartyUseCase(gh<_i661.PartyRepository>())); - gh.factory<_i12.GetPartiesUseCase>( - () => _i12.GetPartiesUseCase(gh<_i661.PartyRepository>())); + gh.factory<_i714.ListenToPartiesUseCase>( + () => _i714.ListenToPartiesUseCase(gh<_i661.PartyRepository>())); gh.factory<_i911.UpdatePartyUseCase>( () => _i911.UpdatePartyUseCase(gh<_i661.PartyRepository>())); gh.factory<_i84.AddPartyUseCase>( () => _i84.AddPartyUseCase(gh<_i661.PartyRepository>())); - gh.factory<_i714.ListenToPartiesUseCase>( - () => _i714.ListenToPartiesUseCase(gh<_i661.PartyRepository>())); + gh.factory<_i12.GetPartiesUseCase>( + () => _i12.GetPartiesUseCase(gh<_i661.PartyRepository>())); gh.factory<_i397.ListenExchangeRate>( () => _i397.ListenExchangeRate(gh<_i1057.ExchangeRateRepository>())); gh.factory<_i88.BenefitsCubit>( @@ -379,18 +382,18 @@ _i174.GetIt $initGetIt( localDataSource: gh<_i849.WalletLocalDataSource>(), db: gh<_i704.AppDatabase>(), )); - gh.factory<_i163.DeleteTransactionUseCase>( - () => _i163.DeleteTransactionUseCase(gh<_i118.TransactionRepository>())); + gh.factory<_i947.GetAllTransactionsUseCase>( + () => _i947.GetAllTransactionsUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i973.ListenToTransactionsUseCase>(() => _i973.ListenToTransactionsUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i241.UpdateTransactionUseCase>( () => _i241.UpdateTransactionUseCase(gh<_i118.TransactionRepository>())); - gh.factory<_i947.GetAllTransactionsUseCase>( - () => _i947.GetAllTransactionsUseCase(gh<_i118.TransactionRepository>())); - gh.factory<_i418.UpdateWalletUseCase>( - () => _i418.UpdateWalletUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i163.DeleteTransactionUseCase>( + () => _i163.DeleteTransactionUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i62.DeleteWalletUseCase>( () => _i62.DeleteWalletUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i418.UpdateWalletUseCase>( + () => _i418.UpdateWalletUseCase(gh<_i368.WalletRepository>())); gh.factory<_i80.AddWalletUseCase>( () => _i80.AddWalletUseCase(gh<_i368.WalletRepository>())); gh.factory<_i713.GetWalletsUseCase>( @@ -410,10 +413,10 @@ _i174.GetIt $initGetIt( gh<_i118.TransactionRepository>(), gh<_i1057.ExchangeRateRepository>(), )); - gh.factory<_i225.EnsureDefaultWalletExistsUseCase>(() => - _i225.EnsureDefaultWalletExistsUseCase(gh<_i368.WalletRepository>())); gh.factory<_i82.ListenToWalletsUseCase>( () => _i82.ListenToWalletsUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i225.EnsureDefaultWalletExistsUseCase>(() => + _i225.EnsureDefaultWalletExistsUseCase(gh<_i368.WalletRepository>())); gh.factory<_i841.PartyCubit>(() => _i841.PartyCubit( getPartiesUseCase: gh<_i12.GetPartiesUseCase>(), addPartyUseCase: gh<_i84.AddPartyUseCase>(), @@ -445,16 +448,16 @@ _i174.GetIt $initGetIt( gh<_i640.LogoutUsecase>(), gh<_i481.UserContextService>(), )); - gh.factory<_i759.DeleteGroupUseCase>( - () => _i759.DeleteGroupUseCase(gh<_i957.GroupRepository>())); - gh.factory<_i820.UpdateGroupUseCase>( - () => _i820.UpdateGroupUseCase(gh<_i957.GroupRepository>())); - gh.factory<_i353.AddGroupUseCase>( - () => _i353.AddGroupUseCase(gh<_i957.GroupRepository>())); gh.factory<_i982.GetGroupsUseCase>( () => _i982.GetGroupsUseCase(gh<_i957.GroupRepository>())); + gh.factory<_i759.DeleteGroupUseCase>( + () => _i759.DeleteGroupUseCase(gh<_i957.GroupRepository>())); gh.factory<_i146.ListenToGroupsUseCase>( () => _i146.ListenToGroupsUseCase(gh<_i957.GroupRepository>())); + gh.factory<_i353.AddGroupUseCase>( + () => _i353.AddGroupUseCase(gh<_i957.GroupRepository>())); + gh.factory<_i820.UpdateGroupUseCase>( + () => _i820.UpdateGroupUseCase(gh<_i957.GroupRepository>())); gh.factory<_i1068.WalletCubit>(() => _i1068.WalletCubit( getWalletsUseCase: gh<_i713.GetWalletsUseCase>(), addWalletUseCase: gh<_i80.AddWalletUseCase>(), diff --git a/lib/domain/usecases/sync/check_pending_changes_usecase.dart b/lib/domain/usecases/sync/check_pending_changes_usecase.dart new file mode 100644 index 00000000..2ccd4444 --- /dev/null +++ b/lib/domain/usecases/sync/check_pending_changes_usecase.dart @@ -0,0 +1,23 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/data/database/app_database.dart'; + +@injectable +class CheckPendingChangesUsecase implements UseCase { + final AppDatabase _appDatabase; + + CheckPendingChangesUsecase(this._appDatabase); + + @override + Future> call(NoParams params) async { + try { + final pendingChanges = await _appDatabase.getPendingLocalChanges(); + // Return true if there are pending changes (items need to be synchronized) + return Right(pendingChanges.isNotEmpty); + } catch (e) { + return Left(ServerFailure(e.toString())); + } + } +} diff --git a/lib/gen/translations/codegen_loader.g.dart b/lib/gen/translations/codegen_loader.g.dart index 6f75fd7e..57c3ba93 100644 --- a/lib/gen/translations/codegen_loader.g.dart +++ b/lib/gen/translations/codegen_loader.g.dart @@ -154,7 +154,6 @@ abstract class LocaleKeys { static const toExcel = 'toExcel'; static const family = 'family'; static const searchHint = 'searchHint'; - static const totalIncome = 'totalIncome'; static const displaySettings = 'displaySettings'; static const transactionFormDisplayMode = 'transactionFormDisplayMode'; static const themeMode = 'themeMode'; @@ -199,6 +198,7 @@ abstract class LocaleKeys { static const exchangeRateHint = 'exchangeRateHint'; static const valueRequired = 'valueRequired'; static const transferMoney = 'transferMoney'; + static const databaseViewer = 'databaseViewer'; static const unknown = 'unknown'; static const from = 'from'; static const to = 'to'; @@ -266,7 +266,7 @@ abstract class LocaleKeys { static const transactionsIn = 'transactionsIn'; static const wallets = 'wallets'; static const noData = 'noData'; - static const databaseViewer = 'databaseViewer'; + static const totalIncome = 'totalIncome'; static const totalExpense = 'totalExpense'; static const thisMonth = 'thisMonth'; static const thisWeek = 'thisWeek'; @@ -283,7 +283,6 @@ abstract class LocaleKeys { static const deleteWallet = 'deleteWallet'; static const deleteWalletConfirm = 'deleteWalletConfirm'; static const edit = 'edit'; - static const defaultName = 'defaultName'; static const officeElements = 'officeElements'; static const officeElementsDesc = 'officeElementsDesc'; static const monthly = 'monthly'; @@ -297,6 +296,7 @@ abstract class LocaleKeys { static const deletePartyConfirm = 'deletePartyConfirm'; static const general = 'general'; static const pricePerMonth = 'pricePerMonth'; + static const defaultName = 'defaultName'; static const partyTypeIndividual = 'partyTypeIndividual'; static const partyTypeOrganization = 'partyTypeOrganization'; static const partyTypeBusiness = 'partyTypeBusiness'; @@ -327,5 +327,9 @@ abstract class LocaleKeys { static const proceedWithGoogle = 'proceedWithGoogle'; static const proceedWithApple = 'proceedWithApple'; static const emailPhoneValidateDesc = 'emailPhoneValidateDesc'; + static const logoutWarningTitle = 'logoutWarningTitle'; + static const logoutWarningMessage = 'logoutWarningMessage'; + static const syncNow = 'syncNow'; + static const logoutAnyway = 'logoutAnyway'; } diff --git a/lib/presentation/add_transaction_screen.dart b/lib/presentation/add_transaction_screen.dart index a71f8178..40fef8e2 100644 --- a/lib/presentation/add_transaction_screen.dart +++ b/lib/presentation/add_transaction_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:trakli/domain/entities/transaction_complete_entity.dart'; +import 'package:trakli/domain/entities/wallet_entity.dart'; import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/transactions/add_transaction_form_compact_layout.dart'; @@ -16,12 +17,14 @@ class AddTransactionScreen extends StatefulWidget { final TransactionType transactionType; final Color accentColor; final TransactionCompleteEntity? transaction; + final WalletEntity? selectedWallet; const AddTransactionScreen({ super.key, this.transactionType = TransactionType.income, this.accentColor = const Color(0xFFEB5757), this.transaction, + this.selectedWallet, }); @override @@ -155,11 +158,13 @@ class _AddTransactionScreenState extends State transactionType: TransactionType.expense, accentColor: Theme.of(context).primaryColor, transactionCompleteEntity: widget.transaction, + selectedWallet: widget.selectedWallet, ) : AddTransactionFormCompactLayout( transactionType: TransactionType.expense, accentColor: Theme.of(context).primaryColor, transactionCompleteEntity: widget.transaction, + selectedWallet: widget.selectedWallet, )), if (widget.transaction == null || widget.transaction!.transaction.type == @@ -168,10 +173,12 @@ class _AddTransactionScreenState extends State ? AddTransactionForm( accentColor: Theme.of(context).primaryColor, transactionCompleteEntity: widget.transaction, + selectedWallet: widget.selectedWallet, ) : AddTransactionFormCompactLayout( accentColor: Theme.of(context).primaryColor, transactionCompleteEntity: widget.transaction, + selectedWallet: widget.selectedWallet, )), ], ), diff --git a/lib/presentation/history_screen.dart b/lib/presentation/history_screen.dart index 7579f6e6..deefa69f 100644 --- a/lib/presentation/history_screen.dart +++ b/lib/presentation/history_screen.dart @@ -13,6 +13,7 @@ import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/add_transaction_screen.dart'; import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/wallets/cubit/wallet_cubit.dart'; import 'package:trakli/presentation/utils/back_button.dart'; import 'package:trakli/presentation/utils/colors.dart'; import 'package:trakli/presentation/utils/custom_appbar.dart'; @@ -53,7 +54,12 @@ class _HistoryScreenState extends State { actions: [ InkWell( onTap: () { - AppNavigator.push(context, const AddTransactionScreen()); + final selectedWallet = + context.read().currentSelectedWallet; + AppNavigator.push( + context, + AddTransactionScreen(selectedWallet: selectedWallet), + ); }, child: Container( width: 42.r, diff --git a/lib/presentation/home_screen.dart b/lib/presentation/home_screen.dart index b4bdde3c..0679383c 100644 --- a/lib/presentation/home_screen.dart +++ b/lib/presentation/home_screen.dart @@ -6,14 +6,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; +import 'package:trakli/core/sync/sync_database.dart'; +import 'package:trakli/di/injection.dart'; import 'package:trakli/domain/entities/wallet_entity.dart'; import 'package:trakli/domain/entities/group_entity.dart'; import 'package:trakli/domain/entities/transaction_complete_entity.dart'; import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/auth/cubits/auth/auth_cubit.dart'; import 'package:trakli/presentation/groups/cubit/group_cubit.dart'; import 'package:trakli/presentation/history_screen.dart'; -import 'package:trakli/presentation/notification_screen.dart'; import 'package:trakli/presentation/onboarding/cubit/onboarding_cubit.dart'; import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; @@ -34,16 +36,9 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { - int currentWalletIndex = 0; // GroupEntity? group; - @override void initState() { super.initState(); - // WidgetsBinding.instance.addPostFrameCallback((_) { - // context.read().ensureDefaultGroup( - // name: LocaleKeys.defaultGroupName.tr(), - // ); - // }); } @override @@ -74,8 +69,7 @@ class _HomeScreenState extends State { final bool walletMatches = transactionWalletId == currentWalletId; final bool groupMatches = transactionGroupId == selectedGroupId; - final bool isDefaultGroup = - transactionGroupId == null && selectedGroupId == defaultGroupId; + final bool isDefaultGroup = selectedGroupId == defaultGroupId; return walletMatches && (groupMatches || isDefaultGroup); }).toList(); @@ -83,7 +77,9 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { - final wallets = context.watch().state.wallets; + final walletState = context.watch().state; + final wallets = walletState.wallets; + final currentWalletIndex = walletState.currentSelectedWalletIndex; final groups = context.watch().state.groups; final entity = context.watch().state.entity; @@ -109,7 +105,12 @@ class _HomeScreenState extends State { actions: [ GestureDetector( onTap: () { - AppNavigator.push(context, const NotificationScreen()); + final isAuthenticated = + context.read().state.isAuthenticated; + + if (isAuthenticated) { + getIt().doSync(); + } }, child: Container( width: 42.r, @@ -120,7 +121,7 @@ class _HomeScreenState extends State { ), // padding: EdgeInsets.all(14.r), child: Icon( - Icons.notifications, + Icons.refresh, size: 20.sp, color: Theme.of(context).primaryColor, ), @@ -172,9 +173,9 @@ class _HomeScreenState extends State { enlargeCenterPage: true, enlargeFactor: 0.2, onPageChanged: (index, reason) { - setState(() { - currentWalletIndex = index; - }); + context + .read() + .setCurrentSelectedWalletIndex(index); }, ), itemCount: wallets.length, diff --git a/lib/presentation/profile_screen.dart b/lib/presentation/profile_screen.dart index ea3c8727..37806cfd 100644 --- a/lib/presentation/profile_screen.dart +++ b/lib/presentation/profile_screen.dart @@ -4,6 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:trakli/core/sync/sync_database.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/di/injection.dart'; +import 'package:trakli/domain/usecases/sync/check_pending_changes_usecase.dart'; import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/account_info_screen.dart'; @@ -21,6 +25,63 @@ import 'package:trakli/presentation/utils/premium_tile.dart'; class ProfileScreen extends StatelessWidget { const ProfileScreen({super.key}); + Future _handleLogout(BuildContext context) async { + // Check for pending changes + final checkPendingChangesUsecase = getIt(); + final result = await checkPendingChangesUsecase(NoParams()); + + result.fold( + (failure) { + // If there's an error checking pending changes, show regular logout dialog + _showLogoutDialog(context); + }, + (hasPendingChanges) { + if (!hasPendingChanges) { + // Show warning dialog with sync option + _showLogoutWarningDialog(context); + } else { + // No pending changes, show regular logout dialog + _showLogoutDialog(context); + } + }, + ); + } + + void _showLogoutDialog(BuildContext context) { + showCustomDialog( + widget: PopUpDialog( + title: LocaleKeys.logOut.tr(), + subTitle: LocaleKeys.logoutConfirm.tr(), + dialogType: DialogType.negative, + mainAction: () { + context.read().logout(); + }, + ), + ); + } + + void _showLogoutWarningDialog(BuildContext context) { + showCustomDialog( + widget: PopUpDialog( + title: LocaleKeys.logoutWarningTitle.tr(), + subTitle: LocaleKeys.logoutWarningMessage.tr(), + dialogType: DialogType.negative, + mainActionText: LocaleKeys.logoutAnyway.tr(), + secondaryActionText: LocaleKeys.syncNow.tr(), + buttonLayout: ButtonLayout.vertical, + mainAction: () { + // Logout anyway + context.read().logout(); + }, + secondaryAction: () { + // Sync now + getIt().doSync(); + AppNavigator.pop(context); + }, + ), + ); + } + @override Widget build(BuildContext context) { final user = context.read().state.user; @@ -126,17 +187,8 @@ class ProfileScreen extends StatelessWidget { title: LocaleKeys.logOut.tr(), iconPath: Assets.images.logout, actionColor: Colors.red, - onTap: () { - showCustomDialog( - widget: PopUpDialog( - title: LocaleKeys.logOut.tr(), - subTitle: LocaleKeys.logoutConfirm.tr(), - dialogType: DialogType.negative, - mainAction: () { - context.read().logout(); - }, - ), - ); + onTap: () async { + await _handleLogout(context); }, ), ], diff --git a/lib/presentation/root/main_navigation_screen.dart b/lib/presentation/root/main_navigation_screen.dart index b29a7e04..029f6196 100644 --- a/lib/presentation/root/main_navigation_screen.dart +++ b/lib/presentation/root/main_navigation_screen.dart @@ -15,6 +15,7 @@ import 'package:trakli/presentation/utils/bottom_nav.dart'; import 'package:trakli/presentation/utils/custom_drawer.dart'; import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/globals.dart'; +import 'package:trakli/presentation/wallets/cubit/wallet_cubit.dart'; class MainNavigationScreen extends StatelessWidget { MainNavigationScreen({super.key}); @@ -60,7 +61,12 @@ class MainNavigationScreen extends StatelessWidget { shape: const CircleBorder(), backgroundColor: Theme.of(context).primaryColor, onPressed: () { - AppNavigator.push(context, const AddTransactionScreen()); + final selectedWallet = + context.read().currentSelectedWallet; + AppNavigator.push( + context, + AddTransactionScreen(selectedWallet: selectedWallet), + ); }, elevation: 0, child: SvgPicture.asset( diff --git a/lib/presentation/transactions/add_transaction_form_compact_layout.dart b/lib/presentation/transactions/add_transaction_form_compact_layout.dart index 84cbec21..14e0285f 100644 --- a/lib/presentation/transactions/add_transaction_form_compact_layout.dart +++ b/lib/presentation/transactions/add_transaction_form_compact_layout.dart @@ -30,12 +30,14 @@ class AddTransactionFormCompactLayout extends StatefulWidget { final TransactionType transactionType; final Color accentColor; final TransactionCompleteEntity? transactionCompleteEntity; + final WalletEntity? selectedWallet; const AddTransactionFormCompactLayout({ super.key, this.transactionType = TransactionType.income, this.accentColor = const Color(0xFFEB5757), this.transactionCompleteEntity, + this.selectedWallet, }); @override @@ -105,6 +107,12 @@ class _AddTransactionFormCompactLayoutState final onboardingEntity = context.read().state.entity; currentCurrency = onboardingEntity?.selectedCurrency ?? currentCurrency; + + // Set the selected wallet if provided + if (widget.selectedWallet != null) { + _selectedWallet = widget.selectedWallet; + setCurrency(_selectedWallet); + } } } @@ -193,6 +201,7 @@ class _AddTransactionFormCompactLayoutState return CustomAutoCompleteSearch( label: LocaleKeys.wallet.tr(), accentColor: widget.accentColor, + initialValue: _selectedWallet, optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text.isEmpty) { diff --git a/lib/presentation/transactions/cubit/transaction_cubit.dart b/lib/presentation/transactions/cubit/transaction_cubit.dart index 0ca468c4..5682b49b 100644 --- a/lib/presentation/transactions/cubit/transaction_cubit.dart +++ b/lib/presentation/transactions/cubit/transaction_cubit.dart @@ -87,7 +87,6 @@ class TransactionCubit extends Cubit { required DateTime datetime, required String walletClientId, String? partyClientId, - // String? groupClientId, }) async { emit(state.copyWith(isSaving: true, failure: const Failure.none())); final result = await createTransactionUseCase( diff --git a/lib/presentation/utils/bottom_sheets/pick_group_bottom_sheet.dart b/lib/presentation/utils/bottom_sheets/pick_group_bottom_sheet.dart index 0ced08a7..8fd1227b 100644 --- a/lib/presentation/utils/bottom_sheets/pick_group_bottom_sheet.dart +++ b/lib/presentation/utils/bottom_sheets/pick_group_bottom_sheet.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:trakli/gen/assets.gen.dart'; -import 'package:trakli/presentation/groups/add_group_screen.dart'; -import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/colors.dart'; import 'package:trakli/presentation/utils/pick_group_tile.dart'; import 'package:trakli/domain/entities/group_entity.dart'; @@ -116,18 +114,6 @@ class _PickGroupBottomSheetState extends State { ), ), SizedBox(height: 16.h), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - iconAlignment: IconAlignment.end, - onPressed: () { - AppNavigator.push(context, const AddGroupScreen()); - }, - label: Text(LocaleKeys.addGroup.tr()), - icon: const Icon(Icons.add), - ), - ), - SizedBox(height: 16.h), Row( spacing: 16.w, children: [ @@ -162,7 +148,7 @@ class _PickGroupBottomSheetState extends State { ), ], ), - SizedBox(height: 20.h), + SizedBox(height: 30.h), ], ), ); diff --git a/lib/presentation/utils/custom_drawer.dart b/lib/presentation/utils/custom_drawer.dart index 26b93905..2033501c 100644 --- a/lib/presentation/utils/custom_drawer.dart +++ b/lib/presentation/utils/custom_drawer.dart @@ -1,5 +1,4 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -12,7 +11,6 @@ import 'package:trakli/presentation/groups/my_groups_screen.dart'; import 'package:trakli/presentation/history_screen.dart'; import 'package:trakli/presentation/parties/party_screen.dart'; import 'package:trakli/presentation/root/bloc/main_navigation_page_cubit.dart'; -import 'package:trakli/presentation/savings/my_savings_screen.dart'; import 'package:trakli/presentation/settings_screen.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/premium_tile.dart'; @@ -64,19 +62,6 @@ class CustomDrawer extends StatelessWidget { ), title: Text(LocaleKeys.groups.tr()), ), - ListTile( - onTap: () { - AppNavigator.push(context, const MySavingsScreen()); - }, - leading: SvgPicture.asset( - Assets.images.walletAdd, - colorFilter: const ColorFilter.mode( - Color(0XFF3B4E45), - BlendMode.srcIn, - ), - ), - title: Text(LocaleKeys.savings.tr()), - ), ListTile( onTap: () { AppNavigator.push(context, const PartyScreen()); @@ -143,7 +128,7 @@ class CustomDrawer extends StatelessWidget { ), title: Text(LocaleKeys.settings.tr()), ), - if (kDebugMode) ...[ + ...[ const Divider(), ListTile( onTap: () { diff --git a/lib/presentation/utils/dialogs/pop_up_dialog.dart b/lib/presentation/utils/dialogs/pop_up_dialog.dart index 09405ea3..6a29fa90 100644 --- a/lib/presentation/utils/dialogs/pop_up_dialog.dart +++ b/lib/presentation/utils/dialogs/pop_up_dialog.dart @@ -18,6 +18,7 @@ class PopUpDialog extends StatelessWidget { final String subTitle; final String? mainActionText; final String? secondaryActionText; + final ButtonLayout buttonLayout; const PopUpDialog({ super.key, @@ -30,8 +31,64 @@ class PopUpDialog extends StatelessWidget { this.iconPath, this.mainActionText, this.secondaryActionText, + this.buttonLayout = ButtonLayout.horizontal, }); + Widget _buildActionButtons(BuildContext context) { + final secondaryButton = SizedBox( + height: 52.h, + child: ElevatedButton( + onPressed: secondaryAction ?? + () { + AppNavigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: neutralN40, + foregroundColor: neutralN900, + ), + child: Text(secondaryActionText ?? LocaleKeys.cancel.tr()), + ), + ); + + final mainButton = SizedBox( + height: 52.h, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: accentColor ?? + (dialogType == DialogType.positive + ? appPrimaryColor + : appDangerColor), + ), + onPressed: mainAction ?? () {}, + child: Text(mainActionText ?? LocaleKeys.confirm.tr()), + ), + ); + + if (buttonLayout == ButtonLayout.vertical) { + return Column( + children: [ + SizedBox( + width: double.infinity, + child: secondaryButton, + ), + SizedBox(height: 12.h), + SizedBox( + width: double.infinity, + child: mainButton, + ), + ], + ); + } else { + return Row( + spacing: 16.w, + children: [ + Expanded(child: secondaryButton), + Expanded(child: mainButton), + ], + ); + } + } + @override Widget build(BuildContext context) { return Dialog( @@ -88,43 +145,7 @@ class PopUpDialog extends StatelessWidget { textAlign: TextAlign.center, ), SizedBox(height: 16.h), - Row( - spacing: 16.w, - children: [ - Expanded( - child: SizedBox( - height: 52.h, - child: ElevatedButton( - onPressed: secondaryAction ?? - () { - AppNavigator.pop(context); - }, - style: ElevatedButton.styleFrom( - backgroundColor: neutralN40, - foregroundColor: neutralN900, - ), - child: - Text(secondaryActionText ?? LocaleKeys.cancel.tr()), - ), - ), - ), - Expanded( - child: SizedBox( - height: 52.h, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: accentColor ?? - (dialogType == DialogType.positive - ? appPrimaryColor - : appDangerColor), - ), - onPressed: mainAction ?? () {}, - child: Text(mainActionText ?? LocaleKeys.confirm.tr()), - ), - ), - ), - ], - ), + _buildActionButtons(context), ], ), ), diff --git a/lib/presentation/utils/enums.dart b/lib/presentation/utils/enums.dart index 0aded67d..57b84128 100644 --- a/lib/presentation/utils/enums.dart +++ b/lib/presentation/utils/enums.dart @@ -91,6 +91,11 @@ enum DialogType { negative, } +enum ButtonLayout { + horizontal, + vertical, +} + enum PlanType { monthly, yearly; diff --git a/lib/presentation/utils/forms/add_transaction_form.dart b/lib/presentation/utils/forms/add_transaction_form.dart index c9ce07f4..ce4d1d48 100644 --- a/lib/presentation/utils/forms/add_transaction_form.dart +++ b/lib/presentation/utils/forms/add_transaction_form.dart @@ -29,12 +29,14 @@ class AddTransactionForm extends StatefulWidget { final TransactionType transactionType; final Color accentColor; final TransactionCompleteEntity? transactionCompleteEntity; + final WalletEntity? selectedWallet; const AddTransactionForm({ super.key, this.transactionType = TransactionType.income, this.accentColor = const Color(0xFFEB5757), this.transactionCompleteEntity, + this.selectedWallet, }); @override @@ -62,10 +64,6 @@ class _AddTransactionFormState extends State { final _formKey = GlobalKey(); setAmountController(Currency? currency) { - // amountController.text = convertAmountFromCurrencyWihContext(context, - // widget.transactionCompleteEntity!.transaction.amount, currency) - // .toStringAsFixed(decimalDigits); - amountController.text = widget.transactionCompleteEntity!.transaction.amount.toString(); } @@ -91,6 +89,15 @@ class _AddTransactionFormState extends State { date = DateTime.now(); dateController.text = dateFormat.format(date); timeController.text = timeFormat.format(date); + + // Set the selected wallet if provided + if (widget.selectedWallet != null) { + selectedWallet = widget.selectedWallet; + walletController.text = selectedWallet!.name; + } else { + selectedWallet = widget.selectedWallet; + walletController.text = widget.selectedWallet?.name ?? ''; + } } } diff --git a/lib/presentation/wallets/cubit/wallet_cubit.dart b/lib/presentation/wallets/cubit/wallet_cubit.dart index 685c71a6..90d4eb8a 100644 --- a/lib/presentation/wallets/cubit/wallet_cubit.dart +++ b/lib/presentation/wallets/cubit/wallet_cubit.dart @@ -194,4 +194,18 @@ class WalletCubit extends Cubit { ), ); } + + void setCurrentSelectedWalletIndex(int index) { + if (index >= 0 && index < state.wallets.length) { + emit(state.copyWith(currentSelectedWalletIndex: index)); + } + } + + WalletEntity? get currentSelectedWallet { + if (state.wallets.isEmpty || + state.currentSelectedWalletIndex >= state.wallets.length) { + return null; + } + return state.wallets[state.currentSelectedWalletIndex]; + } } diff --git a/lib/presentation/wallets/cubit/wallet_cubit.freezed.dart b/lib/presentation/wallets/cubit/wallet_cubit.freezed.dart index b404cd6f..153f103c 100644 --- a/lib/presentation/wallets/cubit/wallet_cubit.freezed.dart +++ b/lib/presentation/wallets/cubit/wallet_cubit.freezed.dart @@ -21,6 +21,7 @@ mixin _$WalletState { bool get isSaving => throw _privateConstructorUsedError; bool get isDeleting => throw _privateConstructorUsedError; Failure get failure => throw _privateConstructorUsedError; + int get currentSelectedWalletIndex => throw _privateConstructorUsedError; /// Create a copy of WalletState /// with the given fields replaced by the non-null parameter values. @@ -40,7 +41,8 @@ abstract class $WalletStateCopyWith<$Res> { bool isLoading, bool isSaving, bool isDeleting, - Failure failure}); + Failure failure, + int currentSelectedWalletIndex}); $FailureCopyWith<$Res> get failure; } @@ -65,6 +67,7 @@ class _$WalletStateCopyWithImpl<$Res, $Val extends WalletState> Object? isSaving = null, Object? isDeleting = null, Object? failure = null, + Object? currentSelectedWalletIndex = null, }) { return _then(_value.copyWith( wallets: null == wallets @@ -87,6 +90,10 @@ class _$WalletStateCopyWithImpl<$Res, $Val extends WalletState> ? _value.failure : failure // ignore: cast_nullable_to_non_nullable as Failure, + currentSelectedWalletIndex: null == currentSelectedWalletIndex + ? _value.currentSelectedWalletIndex + : currentSelectedWalletIndex // ignore: cast_nullable_to_non_nullable + as int, ) as $Val); } @@ -114,7 +121,8 @@ abstract class _$$WalletStateImplCopyWith<$Res> bool isLoading, bool isSaving, bool isDeleting, - Failure failure}); + Failure failure, + int currentSelectedWalletIndex}); @override $FailureCopyWith<$Res> get failure; @@ -138,6 +146,7 @@ class __$$WalletStateImplCopyWithImpl<$Res> Object? isSaving = null, Object? isDeleting = null, Object? failure = null, + Object? currentSelectedWalletIndex = null, }) { return _then(_$WalletStateImpl( wallets: null == wallets @@ -160,6 +169,10 @@ class __$$WalletStateImplCopyWithImpl<$Res> ? _value.failure : failure // ignore: cast_nullable_to_non_nullable as Failure, + currentSelectedWalletIndex: null == currentSelectedWalletIndex + ? _value.currentSelectedWalletIndex + : currentSelectedWalletIndex // ignore: cast_nullable_to_non_nullable + as int, )); } } @@ -172,7 +185,8 @@ class _$WalletStateImpl implements _WalletState { required this.isLoading, required this.isSaving, required this.isDeleting, - required this.failure}) + required this.failure, + this.currentSelectedWalletIndex = 0}) : _wallets = wallets; final List _wallets; @@ -191,10 +205,13 @@ class _$WalletStateImpl implements _WalletState { final bool isDeleting; @override final Failure failure; + @override + @JsonKey() + final int currentSelectedWalletIndex; @override String toString() { - return 'WalletState(wallets: $wallets, isLoading: $isLoading, isSaving: $isSaving, isDeleting: $isDeleting, failure: $failure)'; + return 'WalletState(wallets: $wallets, isLoading: $isLoading, isSaving: $isSaving, isDeleting: $isDeleting, failure: $failure, currentSelectedWalletIndex: $currentSelectedWalletIndex)'; } @override @@ -209,7 +226,11 @@ class _$WalletStateImpl implements _WalletState { other.isSaving == isSaving) && (identical(other.isDeleting, isDeleting) || other.isDeleting == isDeleting) && - (identical(other.failure, failure) || other.failure == failure)); + (identical(other.failure, failure) || other.failure == failure) && + (identical(other.currentSelectedWalletIndex, + currentSelectedWalletIndex) || + other.currentSelectedWalletIndex == + currentSelectedWalletIndex)); } @override @@ -219,7 +240,8 @@ class _$WalletStateImpl implements _WalletState { isLoading, isSaving, isDeleting, - failure); + failure, + currentSelectedWalletIndex); /// Create a copy of WalletState /// with the given fields replaced by the non-null parameter values. @@ -236,7 +258,8 @@ abstract class _WalletState implements WalletState { required final bool isLoading, required final bool isSaving, required final bool isDeleting, - required final Failure failure}) = _$WalletStateImpl; + required final Failure failure, + final int currentSelectedWalletIndex}) = _$WalletStateImpl; @override List get wallets; @@ -248,6 +271,8 @@ abstract class _WalletState implements WalletState { bool get isDeleting; @override Failure get failure; + @override + int get currentSelectedWalletIndex; /// Create a copy of WalletState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/presentation/wallets/cubit/wallet_state.dart b/lib/presentation/wallets/cubit/wallet_state.dart index 956bef53..a8cf660f 100644 --- a/lib/presentation/wallets/cubit/wallet_state.dart +++ b/lib/presentation/wallets/cubit/wallet_state.dart @@ -8,6 +8,7 @@ class WalletState with _$WalletState { required bool isSaving, required bool isDeleting, required Failure failure, + @Default(0) int currentSelectedWalletIndex, }) = _WalletState; factory WalletState.initial() => const WalletState(