diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 00000000..1c21ee05 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,109 @@ +name: Flutter CI + +on: + push: + branches: [ "dev" , "main" ] + pull_request: + branches: [ "dev" , "main" ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + cache: true + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Cache pub dependencies + uses: actions/cache@v3 + with: + path: | + ${{ env.PUB_CACHE }} + ~/.pub-cache + key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pub- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install Firebase CLI + run: npm install -g firebase-tools + + - name: Install FlutterFire CLI + run: dart pub global activate flutterfire_cli + - name: Install dependencies + run: flutter pub get + + - name: Generate local code + run: flutter pub run build_runner build --delete-conflicting-outputs + + - name: Generate Firebase Options + env: + FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }} + FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} + run: flutterfire configure --project=$FIREBASE_PROJECT_ID -y --platforms=android --out=lib/firebase_options.dart --token=$FIREBASE_TOKEN --android-package-name=com.pennypilot.moneyplus + + - name: Configure Gradle memory + run: | + mkdir -p ~/.gradle + cat > ~/.gradle/gradle.properties << EOF + org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError + org.gradle.daemon=false + org.gradle.parallel=true + org.gradle.caching=true + android.useAndroidX=true + android.enableJetifier=true + EOF + + - name: Decode keystore + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.SIGNING_KEY_BASE64 }} + run: echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > android/app/upload-keystore.jks + + - name: Create key.properties + env: + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS_PASSWORD: ${{ secrets.ANDROID_KEY_ALIAS_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + run: | + cat < android/key.properties + storePassword=$ANDROID_KEYSTORE_PASSWORD + keyPassword=$ANDROID_KEY_ALIAS_PASSWORD + keyAlias=$ANDROID_KEY_ALIAS + storeFile=upload-keystore.jks + EOF + + - name: Build release APK + timeout-minutes: 30 + run: flutter build apk --release --obfuscate --split-debug-info=build/app/outputs/symbols + + - name: Distribute to Firebase + env: + FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }} + FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} + run: | + firebase appdistribution:distribute build/app/outputs/flutter-apk/app-release.apk \ + --app "$FIREBASE_APP_ID" \ + --groups "closed-beta-testers" \ + --release-notes "Build from ${{ github.ref_name }} - Commit: ${{ github.sha }}" diff --git a/.github/workflows/flutter_build.yml b/.github/workflows/flutter_build.yml deleted file mode 100644 index 59f8763f..00000000 --- a/.github/workflows/flutter_build.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Flutter CI - -on: - push: - branches: [dev, main] - pull_request: - branches: [dev, main] -jobs: - build: - name: App Build (Android & iOS) - runs-on: macos-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable - - - name: Cache Gradle packages - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Cache pub dependencies - uses: actions/cache@v3 - with: - path: | - ${{ env.PUB_CACHE }} - ~/.pub-cache - key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }} - restore-keys: | - ${{ runner.os }}-pub- - - name: Generate local code - run: flutter pub run build_runner build --delete-conflicting-outputs - - - name: Create secrets.env file - run: | - echo "SUPA_BASE_URL=${{ secrets.SUPA_BASE_URL }}" > secrets.env - echo "SUPA_API_KEY=${{ secrets.SUPA_API_KEY }}" >> secrets.env - - - name: Flutter clean - run: flutter clean - - - name: Flutter pub get - run: flutter pub get - - - name: Build Android - run: flutter build apk --debug - - - name: Build iOS (simulator) - run: flutter build ios --simulator diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml new file mode 100644 index 00000000..18348a34 --- /dev/null +++ b/.github/workflows/ios.yml @@ -0,0 +1,66 @@ +name: Flutter CI iOS + +on: + push: + branches: [ "dev", "main" ] + pull_request: + branches: [ "dev", "main" ] + workflow_dispatch: + +jobs: + build-ios: + runs-on: macos-latest + timeout-minutes: 60 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + cache: true + + - name: Cache pub dependencies + uses: actions/cache@v3 + with: + path: | + ${{ env.PUB_CACHE }} + ~/.pub-cache + key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pub- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install Firebase CLI + run: npm install -g firebase-tools + + - name: Install FlutterFire CLI + run: dart pub global activate flutterfire_cli + + - name: Install dependencies + run: flutter pub get + + - name: Generate local code + run: flutter pub run build_runner build --delete-conflicting-outputs + + - name: Generate Firebase Options + env: + FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }} + FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} + run: | + flutterfire configure \ + --project=$FIREBASE_PROJECT_ID \ + -y \ + --platforms=ios \ + --out=lib/firebase_options.dart \ + --token=$FIREBASE_TOKEN \ + --ios-bundle-id=com.pennypilot.moneyplus + + - name: Build iOS (simulator) + run: flutter build ios --simulator --no-codesign \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4cb152db..f8432080 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,15 +1,34 @@ +import java.util.Properties +import java.io.FileInputStream plugins { id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + id("com.google.firebase.crashlytics") + // END: FlutterFire Configuration id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } +val keystoreProperties = Properties() +val keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} + android { namespace = "com.pennypilot.moneyplus" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion + defaultConfig { + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -19,26 +38,33 @@ android { jvmTarget = JavaVersion.VERSION_17.toString() } - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.pennypilot.moneyplus" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion - versionCode = flutter.versionCode - versionName = flutter.versionName + signingConfigs { + create("release") { + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = file(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + } } buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") + + getByName("release") { + signingConfig = signingConfigs.getByName("release") + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + ndk { + abiFilters.clear() + abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a")) + } + } } } - flutter { source = "../.." } diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index ca7fe065..d6b1b1bf 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -20,6 +20,10 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.11.1" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + id("com.google.firebase.crashlytics") version("2.8.1") apply false + // END: FlutterFire Configuration id("org.jetbrains.kotlin.android") version "2.2.20" apply false } diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..0d140800 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 13.0 + 15.0 diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 00000000..c7a0964b --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,46 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '15.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' + end + end +end diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2f669c9e..82128e75 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -657,7 +657,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart index f8606b98..8e3ab8da 100644 --- a/lib/core/constants/app_constants.dart +++ b/lib/core/constants/app_constants.dart @@ -8,4 +8,7 @@ class AppConstants { // Deep link paths static const String resetPasswordRedirect = "com.pennypilot.moneyplus://reset-password"; + + static const String googleWebClientId = "GOOGLE_WEB_CLIENT_ID"; + static const String googleIosClientId = "GOOGLE_IOS_CLIENT_ID"; } diff --git a/lib/core/di/cubit_di.dart b/lib/core/di/cubit_di.dart new file mode 100644 index 00000000..7265b750 --- /dev/null +++ b/lib/core/di/cubit_di.dart @@ -0,0 +1,73 @@ +import 'package:moneyplus/domain/repository/account_repository.dart'; +import 'package:moneyplus/domain/repository/authentication_repository.dart'; +import 'package:moneyplus/domain/repository/statistics_repository.dart'; +import 'package:moneyplus/domain/repository/transaction_repository.dart'; +import 'package:moneyplus/domain/repository/user_money_repository.dart'; +import 'package:moneyplus/domain/validator/authentication_validator.dart'; +import 'package:moneyplus/presentation/account_setup/cubit/account_setup_cubit.dart'; +import 'package:moneyplus/presentation/createAccount/cubit/create_account_cubit.dart'; +import 'package:moneyplus/presentation/home/cubit/home_cubit.dart'; +import 'package:moneyplus/presentation/income/cubit/add_income_cubit.dart'; +import 'package:moneyplus/presentation/login/cubit/login_cubit.dart'; +import 'package:moneyplus/presentation/statistics/cubit/statistics_cubit.dart'; +import 'package:moneyplus/presentation/transactions/cubit/transaction_cubit.dart'; +import 'package:moneyplus/presentation/trasnaction_details/trasnaction_details_cubit.dart'; + +import '../../presentation/expense/cubit/add_expense_cubit.dart'; +import 'injection.dart'; + +void initCubitDI() { + getIt.registerLazySingleton( + () => AuthenticationValidator(), + ); + + getIt.registerFactory( + () => HomeCubit(userMoneyRepository: getIt()), + ); + + getIt.registerFactory( + () => LoginCubit( + authRepository: getIt(), + validator: getIt(), + ), + ); + + getIt.registerLazySingleton( + () => AccountSetupCubit(getIt()), + ); + + getIt.registerFactory( + () => AddExpenseCubit( + transactionRepository: getIt(), + userMoneyRepository: getIt(), + ), + ); + getIt.registerFactory( + () => AddIncomeCubit( + transactionRepository: getIt(), + userMoneyRepository: getIt(), + ), + ); + + getIt.registerFactory( + () => + TransactionCubit(transactionRepository: getIt()), + ); + + getIt.registerFactory( + () => StatisticsCubit(repository: getIt()), + ); + + getIt.registerFactory( + () => TransactionDetailsCubit( + transactionRepository: getIt(), + ), + ); + + getIt.registerFactory( + () => CreateAccountCubit( + getIt(), + getIt(), + ), + ); +} diff --git a/lib/core/di/injection.dart b/lib/core/di/injection.dart index 42fd3ff7..cb77a090 100644 --- a/lib/core/di/injection.dart +++ b/lib/core/di/injection.dart @@ -1,108 +1,12 @@ import 'package:get_it/get_it.dart'; -import 'package:moneyplus/domain/repository/transaction_repository.dart'; -import 'package:moneyplus/presentation/account_setup/cubit/account_setup_cubit.dart'; -import 'package:moneyplus/presentation/transactions/cubit/transaction_cubit.dart'; - -import '../../data/repository/account_repository.dart'; -import '../../data/repository/authentication_repository.dart'; -import '../../data/repository/statistics_repository_impl.dart'; -import '../../data/repository/transaction_repository.dart'; -import '../../data/repository/user_money_repository.dart'; -import '../../data/service/app_secrets_provider.dart'; -import '../../data/service/supabase_service.dart'; -import '../../domain/repository/account_repository.dart'; -import '../../domain/repository/authentication_repository.dart'; -import '../../domain/repository/statistics_repository.dart'; -import '../../domain/repository/user_money_repository.dart'; -import '../../domain/validator/authentication_validator.dart'; -import '../../presentation/createAccount/cubit/create_account_cubit.dart'; -import '../../presentation/expense/cubit/add_expense_cubit.dart'; -import '../../presentation/home/cubit/home_cubit.dart'; -import '../../presentation/income/cubit/add_income_cubit.dart'; -import '../../presentation/login/cubit/login_cubit.dart'; -import '../../presentation/statistics/cubit/statistics_cubit.dart'; -import '../../presentation/trasnaction_details/trasnaction_details_cubit.dart'; +import 'package:moneyplus/core/di/cubit_di.dart'; +import 'package:moneyplus/core/di/repository_di.dart'; +import 'package:moneyplus/core/di/service_di.dart'; final getIt = GetIt.instance; -void initDI() { - getIt.registerLazySingleton(() => AppSecretsProvider()); - getIt.registerLazySingleton( - () => SupabaseService(appSecretsProvider: getIt()), - ); - - getIt.registerLazySingleton( - () => AuthenticationRepositoryImpl( - supabaseService: getIt(), - appSecrets: getIt(), - ), - ); - getIt.registerLazySingleton( - () => UserRepositoryImpl(service: getIt()), - ); - - getIt.registerFactory( - () => HomeCubit(userMoneyRepository: getIt()), - ); - getIt.registerLazySingleton( - () => AuthenticationValidator(), - ); - - getIt.registerFactory( - () => LoginCubit( - authRepository: getIt(), - validator: getIt(), - ), - ); - - getIt.registerLazySingleton( - () => AccountRepositoryImpl(supabaseService: getIt()), - ); - - getIt.registerLazySingleton( - () => TransactionRepositoryImpl(service: getIt()), - ); - - getIt.registerLazySingleton( - () => AccountSetupCubit(getIt()), - ); - - getIt.registerFactory( - () => AddExpenseCubit( - transactionRepository: getIt(), - userMoneyRepository: getIt(), - ), - ); - - getIt.registerFactory( - () => AddIncomeCubit( - transactionRepository: getIt(), - userMoneyRepository: getIt(), - ), - ); - - getIt.registerFactory( - () => - TransactionCubit(transactionRepository: getIt()), - ); - - getIt.registerLazySingleton( - () => StatisticsRepositoryImpl(supabaseService: getIt()), - ); - - getIt.registerFactory( - () => StatisticsCubit(repository: getIt()), - ); - - getIt.registerFactory( - () => TransactionDetailsCubit( - transactionRepository: getIt(), - ), - ); - getIt.registerFactory( - () => CreateAccountCubit( - getIt(), - getIt(), - ), - ); +void initDI() { + initServiceDI(); + initRepositoryDI(); + initCubitDI(); } diff --git a/lib/core/di/repository_di.dart b/lib/core/di/repository_di.dart new file mode 100644 index 00000000..d0707591 --- /dev/null +++ b/lib/core/di/repository_di.dart @@ -0,0 +1,33 @@ +import '../../data/repository/account_repository.dart'; +import '../../data/repository/authentication_repository.dart'; +import '../../data/repository/statistics_repository_impl.dart'; +import '../../data/repository/transaction_repository.dart'; +import '../../data/repository/user_money_repository.dart'; +import '../../domain/repository/account_repository.dart'; +import '../../domain/repository/authentication_repository.dart'; +import '../../domain/repository/statistics_repository.dart'; +import '../../domain/repository/transaction_repository.dart'; +import '../../domain/repository/user_money_repository.dart'; +import '../service/supabase_service.dart'; +import 'injection.dart'; + +void initRepositoryDI() { + getIt.registerLazySingleton( + () => AuthenticationRepositoryImpl( + supabaseService: getIt(), + appSecrets: getIt(), + ), + ); + getIt.registerLazySingleton( + () => UserRepositoryImpl(service: getIt()), + ); + getIt.registerLazySingleton( + () => AccountRepositoryImpl(supabaseService: getIt()), + ); + getIt.registerLazySingleton( + () => TransactionRepositoryImpl(service: getIt()), + ); + getIt.registerLazySingleton( + () => StatisticsRepositoryImpl(supabaseService: getIt()), + ); +} diff --git a/lib/core/di/service_di.dart b/lib/core/di/service_di.dart new file mode 100644 index 00000000..8622e523 --- /dev/null +++ b/lib/core/di/service_di.dart @@ -0,0 +1,25 @@ +import 'package:moneyplus/core/security/app_secrets.dart'; + +import '../service/firebase_service.dart'; +import '../service/supabase_service.dart'; +import 'injection.dart'; + +void initServiceDI() { + getIt.registerSingletonAsync(() async { + final service = FirebaseService(); + await service.init(); + return service; + }); + + getIt.registerSingletonAsync( + () async => AppSecrets( + firebaseRemoteConfig: getIt().remoteConfig, + ), + dependsOn: [FirebaseService], + ); + + getIt.registerSingletonAsync( + () async => SupabaseService(appSecrets: getIt()), + dependsOn: [AppSecrets], + ); +} \ No newline at end of file diff --git a/lib/core/security/app_secrets.dart b/lib/core/security/app_secrets.dart new file mode 100644 index 00000000..d6376a11 --- /dev/null +++ b/lib/core/security/app_secrets.dart @@ -0,0 +1,29 @@ +import 'package:firebase_remote_config/firebase_remote_config.dart'; + +import '../constants/app_constants.dart'; + +class AppSecrets { + + final FirebaseRemoteConfig firebaseRemoteConfig; + AppSecrets({required this.firebaseRemoteConfig}); + + Future fetchRemoteConfig() async { + await firebaseRemoteConfig.fetchAndActivate(); + } + + String getRemoteConfigSupaBaseUrl() { + return firebaseRemoteConfig.getString(AppConstants.supabaseUrl); + } + + String getRemoteConfigSupaBaseApiKey() { + return firebaseRemoteConfig.getString(AppConstants.supabaseApiKey); + } + + String getRemoteConfigGoogeWebClientId() { + return firebaseRemoteConfig.getString(AppConstants.googleWebClientId); + } + + String getRemoteConfigGoogeIosClientId() { + return firebaseRemoteConfig.getString(AppConstants.googleIosClientId); + } +} \ No newline at end of file diff --git a/lib/core/service/firebase_service.dart b/lib/core/service/firebase_service.dart new file mode 100644 index 00000000..c7fc15b7 --- /dev/null +++ b/lib/core/service/firebase_service.dart @@ -0,0 +1,38 @@ + + +import 'dart:ui'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:flutter/cupertino.dart'; + +import '../../firebase_options.dart'; + +class FirebaseService { + late final FirebaseRemoteConfig remoteConfig; + + Future init() async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + FlutterError.onError = (errorDetails) { + FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails); + }; + PlatformDispatcher.instance.onError = (error, stack) { + FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); + return true; + }; + + remoteConfig = FirebaseRemoteConfig.instance; + await remoteConfig.setConfigSettings(RemoteConfigSettings( + fetchTimeout: const Duration(seconds: 10), + minimumFetchInterval: const Duration(hours: 1), + )); + await remoteConfig.fetchAndActivate(); + } + + Future getRemoteConfig() async { + return remoteConfig; + } +} diff --git a/lib/data/service/supabase_service.dart b/lib/core/service/supabase_service.dart similarity index 51% rename from lib/data/service/supabase_service.dart rename to lib/core/service/supabase_service.dart index adc22da6..f6ddb783 100644 --- a/lib/data/service/supabase_service.dart +++ b/lib/core/service/supabase_service.dart @@ -1,29 +1,27 @@ import 'package:flutter/foundation.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import '../../core/constants/app_constants.dart'; -import 'app_secrets_provider.dart'; +import '../security/app_secrets.dart'; class SupabaseService { SupabaseClient? _supabaseClient; - final AppSecretsProvider appSecretsProvider; + AppSecrets appSecrets; - SupabaseService({required this.appSecretsProvider}); + SupabaseService({required this.appSecrets}); Future getClient() async { if (_supabaseClient != null) return _supabaseClient!; - final dotEnvInstance = await appSecretsProvider.getEnvVariables(); + final url = appSecrets.getRemoteConfigSupaBaseUrl(); + final anonKey = appSecrets.getRemoteConfigSupaBaseApiKey(); final supabase = await Supabase.initialize( - url: dotEnvInstance.env[AppConstants.supabaseUrl] ?? "", - anonKey: dotEnvInstance.env[AppConstants.supabaseApiKey] ?? "", + url: url, + anonKey: anonKey, debug: kDebugMode, ); _supabaseClient = supabase.client; - - return _supabaseClient!; } } diff --git a/lib/data/repository/account_repository.dart b/lib/data/repository/account_repository.dart index f99ffb1f..b936dcf5 100644 --- a/lib/data/repository/account_repository.dart +++ b/lib/data/repository/account_repository.dart @@ -1,7 +1,7 @@ import 'package:moneyplus/domain/entity/currency.dart'; import '../../domain/repository/account_repository.dart'; -import '../service/supabase_service.dart'; +import '../../core/service/supabase_service.dart'; class AccountRepositoryImpl extends AccountRepository { final SupabaseService supabaseService; diff --git a/lib/data/repository/authentication_repository.dart b/lib/data/repository/authentication_repository.dart index 0df2458a..6a615eb1 100644 --- a/lib/data/repository/authentication_repository.dart +++ b/lib/data/repository/authentication_repository.dart @@ -2,20 +2,20 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:moneyplus/core/security/app_secrets.dart'; import 'package:supabase_flutter/supabase_flutter.dart' hide User; import '../../core/constants/app_constants.dart'; import '../../core/errors/error_model.dart'; import '../../core/errors/result.dart'; import '../../core/errors/supabase_auth_error.dart'; +import '../../core/service/supabase_service.dart'; import '../../domain/entity/user.dart'; import '../../domain/repository/authentication_repository.dart'; -import '../service/app_secrets_provider.dart'; -import '../service/supabase_service.dart'; class AuthenticationRepositoryImpl implements AuthenticationRepository { final SupabaseService supabaseService; - final AppSecretsProvider appSecrets; + final AppSecrets appSecrets; AuthenticationRepositoryImpl({ required this.supabaseService, @@ -50,14 +50,10 @@ class AuthenticationRepositoryImpl implements AuthenticationRepository { @override Future> signInWithGoogle() async { try { - final env = await appSecrets.getEnvVariables(); + final serverClientId = appSecrets.getRemoteConfigGoogeWebClientId(); + final clientId = appSecrets.getRemoteConfigGoogeIosClientId(); final GoogleSignIn signIn = GoogleSignIn.instance; - ( - signIn.initialize( - serverClientId: env.env[_googleWebClientId] ?? "", - clientId: env.env[_googleIosClientId] ?? "", - ), - ); + (signIn.initialize(serverClientId: serverClientId, clientId: clientId),); final googleAccount = await signIn.authenticate(); final googleAuthorization = await googleAccount.authorizationClient @@ -151,8 +147,6 @@ class AuthenticationRepositoryImpl implements AuthenticationRepository { } } - static const String _googleWebClientId = "GOOGLE_WEB_CLIENT_ID"; - static const String _googleIosClientId = "GOOGLE_IOS_CLIENT_ID"; static const List _googleScopes = ['email', 'profile', 'openid']; @override diff --git a/lib/data/repository/statistics_repository_impl.dart b/lib/data/repository/statistics_repository_impl.dart index 70cca751..2ea33b3b 100644 --- a/lib/data/repository/statistics_repository_impl.dart +++ b/lib/data/repository/statistics_repository_impl.dart @@ -1,4 +1,4 @@ -import '../../data/service/supabase_service.dart'; +import '../../core/service/supabase_service.dart'; import '../../domain/entity/monthly_overview.dart'; import '../../domain/repository/statistics_repository.dart'; diff --git a/lib/data/repository/transaction_repository.dart b/lib/data/repository/transaction_repository.dart index 801368aa..889136d9 100644 --- a/lib/data/repository/transaction_repository.dart +++ b/lib/data/repository/transaction_repository.dart @@ -1,12 +1,12 @@ import 'package:moneyplus/core/errors/error_model.dart'; import 'package:moneyplus/core/errors/result.dart'; -import 'package:moneyplus/data/service/supabase_service.dart'; import 'package:moneyplus/domain/entity/transaction.dart'; import 'package:moneyplus/domain/entity/transaction_category.dart'; import 'package:moneyplus/domain/entity/transaction_type.dart'; import 'package:moneyplus/domain/repository/model/top_spending_category.dart'; import 'package:moneyplus/domain/repository/transaction_repository.dart'; +import '../../core/service/supabase_service.dart'; import '../../domain/entity/currency.dart'; diff --git a/lib/data/repository/user_money_repository.dart b/lib/data/repository/user_money_repository.dart index fb7d9eef..4fab5672 100644 --- a/lib/data/repository/user_money_repository.dart +++ b/lib/data/repository/user_money_repository.dart @@ -1,9 +1,10 @@ -import 'package:moneyplus/data/service/supabase_service.dart'; import 'package:moneyplus/domain/entity/currency.dart'; import 'package:moneyplus/domain/entity/transaction_category.dart'; import 'package:moneyplus/domain/repository/model/top_spending_category.dart'; import 'package:moneyplus/domain/repository/user_money_repository.dart'; +import '../../core/service/supabase_service.dart'; + class UserRepositoryImpl implements UserMoneyRepository { final SupabaseService service; diff --git a/lib/data/service/app_secrets_provider.dart b/lib/data/service/app_secrets_provider.dart deleted file mode 100644 index 05399656..00000000 --- a/lib/data/service/app_secrets_provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter_dotenv/flutter_dotenv.dart'; - -import '../../core/constants/app_constants.dart'; - - -class AppSecretsProvider { - DotEnv? _dotenv; - - Future getEnvVariables() async { - if (_dotenv != null) return _dotenv!; - _dotenv = DotEnv(); - await _dotenv!.load(fileName: AppConstants.secretsEnvFile); - return _dotenv!; - } -} diff --git a/lib/main.dart b/lib/main.dart index fc7f6a58..f0393d0c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,7 @@ import 'package:logging/logging.dart'; import 'package:moneyplus/money_app.dart'; import 'core/di/injection.dart'; -void main() { +void main() async { WidgetsFlutterBinding.ensureInitialized(); Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) { @@ -13,6 +13,7 @@ void main() { } }); initDI(); + await getIt.allReady(); runApp(const MoneyApp()); } diff --git a/lib/presentation/navigation/routes.g.dart b/lib/presentation/navigation/routes.g.dart deleted file mode 100644 index cb8c27cd..00000000 --- a/lib/presentation/navigation/routes.g.dart +++ /dev/null @@ -1,277 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'routes.dart'; - -// ************************************************************************** -// GoRouterGenerator -// ************************************************************************** - -List get $appRoutes => [ - $onBoardingRoute, - $loginRoute, - $mainRoute, - $createAccountRoute, - $statisticsRoute, - $transactionDetailsRoute, - $forgetPasswordRoute, - $updatePasswordRoute, - $addIncomeRoute, - $addExpenseRoute, -]; - -RouteBase get $onBoardingRoute => - GoRouteData.$route(path: '/', factory: $OnBoardingRoute._fromState); - -mixin $OnBoardingRoute on GoRouteData { - static OnBoardingRoute _fromState(GoRouterState state) => - const OnBoardingRoute(); - - @override - String get location => GoRouteData.$location('/'); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} - -RouteBase get $loginRoute => - GoRouteData.$route(path: '/login', factory: $LoginRoute._fromState); - -mixin $LoginRoute on GoRouteData { - static LoginRoute _fromState(GoRouterState state) => const LoginRoute(); - - @override - String get location => GoRouteData.$location('/login'); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} - -RouteBase get $mainRoute => - GoRouteData.$route(path: '/main', factory: $MainRoute._fromState); - -mixin $MainRoute on GoRouteData { - static MainRoute _fromState(GoRouterState state) => const MainRoute(); - - @override - String get location => GoRouteData.$location('/main'); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} - -RouteBase get $createAccountRoute => GoRouteData.$route( - path: '/createAccount', - factory: $CreateAccountRoute._fromState, -); - -mixin $CreateAccountRoute on GoRouteData { - static CreateAccountRoute _fromState(GoRouterState state) => - const CreateAccountRoute(); - - @override - String get location => GoRouteData.$location('/createAccount'); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} - -RouteBase get $statisticsRoute => GoRouteData.$route( - path: '/statistics', - factory: $StatisticsRoute._fromState, -); - -mixin $StatisticsRoute on GoRouteData { - static StatisticsRoute _fromState(GoRouterState state) => - const StatisticsRoute(); - - @override - String get location => GoRouteData.$location('/statistics'); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} - -RouteBase get $transactionDetailsRoute => GoRouteData.$route( - path: '/transaction_details', - factory: $TransactionDetailsRoute._fromState, -); - -mixin $TransactionDetailsRoute on GoRouteData { - static TransactionDetailsRoute _fromState(GoRouterState state) => - TransactionDetailsRoute(state.uri.queryParameters['transaction-id']!); - - TransactionDetailsRoute get _self => this as TransactionDetailsRoute; - - @override - String get location => GoRouteData.$location( - '/transaction_details', - queryParams: {'transaction-id': _self.transactionId}, - ); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} - -RouteBase get $forgetPasswordRoute => GoRouteData.$route( - path: '/forget_password', - factory: $ForgetPasswordRoute._fromState, -); - -mixin $ForgetPasswordRoute on GoRouteData { - static ForgetPasswordRoute _fromState(GoRouterState state) => - const ForgetPasswordRoute(); - - @override - String get location => GoRouteData.$location('/forget_password'); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} - -RouteBase get $updatePasswordRoute => GoRouteData.$route( - path: '/update_password', - factory: $UpdatePasswordRoute._fromState, -); - -mixin $UpdatePasswordRoute on GoRouteData { - static UpdatePasswordRoute _fromState(GoRouterState state) => - UpdatePasswordRoute(); - - @override - String get location => GoRouteData.$location('/update_password'); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} - -RouteBase get $addIncomeRoute => GoRouteData.$route( - path: '/add-income', - factory: $AddIncomeRoute._fromState, -); - -mixin $AddIncomeRoute on GoRouteData { - static AddIncomeRoute _fromState(GoRouterState state) => - const AddIncomeRoute(); - - @override - String get location => GoRouteData.$location('/add-income'); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} - -RouteBase get $addExpenseRoute => GoRouteData.$route( - path: '/add-expense', - factory: $AddExpenseRoute._fromState, -); - -mixin $AddExpenseRoute on GoRouteData { - static AddExpenseRoute _fromState(GoRouterState state) => - const AddExpenseRoute(); - - @override - String get location => GoRouteData.$location('/add-expense'); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} diff --git a/pubspec.yaml b/pubspec.yaml index 403917f0..c02f8bc2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,11 @@ dependencies: # --- Utils & Internationalization --- intl: any uuid: ^4.5.1 + firebase_core: ^4.4.0 + firebase_crashlytics: ^5.0.7 + firebase_remote_config: ^6.1.4 + firebase_analytics: ^12.1.1 + firebase_performance: ^0.11.1+4 dev_dependencies: flutter_test: @@ -59,7 +64,6 @@ flutter: assets: - assets/icons/ - assets/images/ - - secrets.env uses-material-design: true