From cae04b3ae7581a1b469fe70257cf08a7d0b229ed Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Wed, 3 Dec 2025 11:07:33 +0700 Subject: [PATCH] add firebase, add screen flow --- android/app/build.gradle.kts | 3 + android/app/google-services.json | 29 +++ android/settings.gradle.kts | 3 + firebase.json | 1 + ios/Podfile | 6 +- ios/Podfile.lock | 225 ++++++++++++------ ios/Runner.xcodeproj/project.pbxproj | 4 + .../xcshareddata/xcschemes/Runner.xcscheme | 6 + ios/Runner/AppDelegate.swift | 5 + ios/Runner/GoogleService-Info.plist | 30 +++ lib/core/router/app_router.dart | 41 +++- lib/core/services/analytics_service.dart | 88 +++++++ .../datasources/cart_remote_datasource.dart | 9 +- .../repositories/cart_repository_impl.dart | 27 ++- .../domain/repositories/cart_repository.dart | 9 +- .../cart/presentation/pages/cart_page.dart | 2 +- .../pages/product_detail_page.dart | 48 ++-- .../presentation/pages/products_page.dart | 17 +- lib/firebase_options.dart | 68 ++++++ lib/main.dart | 6 + pubspec.lock | 4 +- pubspec.yaml | 4 +- 22 files changed, 504 insertions(+), 131 deletions(-) create mode 100644 android/app/google-services.json create mode 100644 firebase.json create mode 100644 ios/Runner/GoogleService-Info.plist create mode 100644 lib/core/services/analytics_service.dart create mode 100644 lib/firebase_options.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 6cf099c..1b29ba6 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -3,6 +3,9 @@ import java.io.FileInputStream plugins { id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // 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") diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..0e76a06 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "147309310656", + "project_id": "dbiz-partner", + "storage_bucket": "dbiz-partner.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:147309310656:android:86613d8ffc85576fdc7325", + "android_client_info": { + "package_name": "com.dbiz.partner" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyA60iGPuHOQMJUA0m5aSimzevPAiiaB4pE" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index fb605bc..ff284ff 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -20,6 +20,9 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.9.1" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration id("org.jetbrains.kotlin.android") version "2.1.0" apply false } diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..630a9ce --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"dbiz-partner","appId":"1:147309310656:android:86613d8ffc85576fdc7325","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"dbiz-partner","appId":"1:147309310656:ios:aa59724d2c6b4620dc7325","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"dbiz-partner","configurations":{"android":"1:147309310656:android:86613d8ffc85576fdc7325","ios":"1:147309310656:ios:aa59724d2c6b4620dc7325"}}}}}} \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index ead9595..7e6a9a0 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '13.0' +platform :ios, '15.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -39,7 +39,7 @@ end # OneSignal Notification Service Extension (OUTSIDE Runner target) target 'OneSignalNotificationServiceExtension' do use_frameworks! - pod 'OneSignalXCFramework', '>= 5.0.0', '< 6.0' + pod 'OneSignalXCFramework', '5.2.14' end post_install do |installer| @@ -48,7 +48,7 @@ post_install do |installer| # Ensure consistent deployment target target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' end end end \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fdfbbf4..a8e1729 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -35,65 +35,132 @@ PODS: - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter + - Firebase/CoreOnly (12.4.0): + - FirebaseCore (~> 12.4.0) + - Firebase/Messaging (12.4.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 12.4.0) + - firebase_analytics (12.0.4): + - firebase_core + - FirebaseAnalytics (= 12.4.0) + - Flutter + - firebase_core (4.2.1): + - Firebase/CoreOnly (= 12.4.0) + - Flutter + - firebase_messaging (16.0.4): + - Firebase/Messaging (= 12.4.0) + - firebase_core + - Flutter + - FirebaseAnalytics (12.4.0): + - FirebaseAnalytics/Default (= 12.4.0) + - FirebaseCore (~> 12.4.0) + - FirebaseInstallations (~> 12.4.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - FirebaseAnalytics/Default (12.4.0): + - FirebaseCore (~> 12.4.0) + - FirebaseInstallations (~> 12.4.0) + - GoogleAppMeasurement/Default (= 12.4.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - FirebaseCore (12.4.0): + - FirebaseCoreInternal (~> 12.4.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreInternal (12.4.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseInstallations (12.4.0): + - FirebaseCore (~> 12.4.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (12.4.0): + - FirebaseCore (~> 12.4.0) + - FirebaseInstallations (~> 12.4.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Reachability (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) - Flutter (1.0.0) - flutter_secure_storage (6.0.0): - Flutter - - GoogleDataTransport (9.4.1): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30911.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleMLKit/BarcodeScanning (6.0.0): - - GoogleMLKit/MLKitCore - - MLKitBarcodeScanning (~> 5.0.0) - - GoogleMLKit/MLKitCore (6.0.0): - - MLKitCommon (~> 11.0.0) - - GoogleToolboxForMac/Defines (4.2.1) - - GoogleToolboxForMac/Logger (4.2.1): - - GoogleToolboxForMac/Defines (= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (4.2.1)": - - GoogleToolboxForMac/Defines (= 4.2.1) - - GoogleUtilities/Environment (7.13.3): + - GoogleAdsOnDeviceConversion (3.1.0): + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/Core (12.4.0): + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/Default (12.4.0): + - GoogleAdsOnDeviceConversion (~> 3.1.0) + - GoogleAppMeasurement/Core (= 12.4.0) + - GoogleAppMeasurement/IdentitySupport (= 12.4.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/IdentitySupport (12.4.0): + - GoogleAppMeasurement/Core (= 12.4.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network - GoogleUtilities/Privacy - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.3) - - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/MethodSwizzler (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilitiesComponents (1.1.0): + - GoogleUtilities/Network (8.1.0): - GoogleUtilities/Logger - - GTMSessionFetcher/Core (3.5.0) + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy - image_picker_ios (0.0.1): - Flutter - integration_test (0.0.1): - Flutter - - MLImage (1.0.0-beta5) - - MLKitBarcodeScanning (5.0.0): - - MLKitCommon (~> 11.0) - - MLKitVision (~> 7.0) - - MLKitCommon (11.0.0): - - GoogleDataTransport (< 10.0, >= 9.4.1) - - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" - - GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0) - - GoogleUtilitiesComponents (~> 1.0) - - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - - MLKitVision (7.0.0): - - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" - - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - - MLImage (= 1.0.0-beta5) - - MLKitCommon (~> 11.0) - - mobile_scanner (5.2.3): + - mobile_scanner (7.0.0): - Flutter - - GoogleMLKit/BarcodeScanning (~> 6.0.0) - - nanopb (2.30910.0): - - nanopb/decode (= 2.30910.0) - - nanopb/encode (= 2.30910.0) - - nanopb/decode (2.30910.0) - - nanopb/encode (2.30910.0) + - FlutterMacOS + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) - onesignal_flutter (5.3.4): - Flutter - OneSignalXCFramework (= 5.2.14) @@ -149,9 +216,9 @@ PODS: - Flutter - FlutterMacOS - PromisesObjC (2.4.0) - - SDWebImage (5.21.3): - - SDWebImage/Core (= 5.21.3) - - SDWebImage/Core (5.21.3) + - SDWebImage (5.21.4): + - SDWebImage/Core (= 5.21.4) + - SDWebImage/Core (5.21.4) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -167,13 +234,16 @@ PODS: DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) + - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) - onesignal_flutter (from `.symlinks/plugins/onesignal_flutter/ios`) - - OneSignalXCFramework (< 6.0, >= 5.0.0) + - OneSignalXCFramework (= 5.2.14) - open_file_ios (from `.symlinks/plugins/open_file_ios/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`) @@ -185,16 +255,16 @@ SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery + - Firebase + - FirebaseAnalytics + - FirebaseCore + - FirebaseCoreInternal + - FirebaseInstallations + - FirebaseMessaging + - GoogleAdsOnDeviceConversion + - GoogleAppMeasurement - GoogleDataTransport - - GoogleMLKit - - GoogleToolboxForMac - GoogleUtilities - - GoogleUtilitiesComponents - - GTMSessionFetcher - - MLImage - - MLKitBarcodeScanning - - MLKitCommon - - MLKitVision - nanopb - OneSignalXCFramework - PromisesObjC @@ -206,6 +276,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/connectivity_plus/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" + firebase_analytics: + :path: ".symlinks/plugins/firebase_analytics/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_messaging: + :path: ".symlinks/plugins/firebase_messaging/ios" Flutter: :path: Flutter flutter_secure_storage: @@ -215,7 +291,7 @@ EXTERNAL SOURCES: integration_test: :path: ".symlinks/plugins/integration_test/ios" mobile_scanner: - :path: ".symlinks/plugins/mobile_scanner/ios" + :path: ".symlinks/plugins/mobile_scanner/darwin" onesignal_flutter: :path: ".symlinks/plugins/onesignal_flutter/ios" open_file_ios: @@ -236,34 +312,37 @@ SPEC CHECKSUMS: DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 + Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e + firebase_analytics: 2b372cc13c077de5f1ac37e232bacd5bacb41963 + firebase_core: e6b8bb503b7d1d9856e698d4f193f7b414e6bf1f + firebase_messaging: fc7b6af84f4cd885a4999f51ea69ef20f380d70d + FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f + FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3 + FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6 + FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2 + FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a - GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065 - GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 - GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 - GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe - GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1 + GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - MLImage: 1824212150da33ef225fbd3dc49f184cf611046c - MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b - MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 - MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 - mobile_scanner: 96e91f2e1fb396bb7df8da40429ba8dfad664740 - nanopb: 438bc412db1928dac798aa6fd75726007be04262 + mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 onesignal_flutter: f69ff09eeaf41cea4b7a841de5a61e79e7fd9a5a OneSignalXCFramework: 7112f3e89563e41ebc23fe807788f11985ac541c open_file_ios: 461db5853723763573e140de3193656f91990d9e path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a + SDWebImage: d0184764be51240d49c761c37f53dd017e1ccaaf share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa -PODFILE CHECKSUM: 41022e80ca79dfdcc337fcf6a6cca3b7d3cb6958 +PODFILE CHECKSUM: d8ed968486e3d2e023338569c014e9285ba7bc53 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 9d171f5..c3a8e5c 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 41D57CDF80C517B01729B4E6 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */; }; 48D410762ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 48D4106F2ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 58215889146B2DBBD9C81410 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; @@ -67,6 +68,7 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 18121E1016DEC4038E74F1F0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; @@ -175,6 +177,7 @@ 331C8082294A63A400263BE5 /* RunnerTests */, D39C332D04678D8C49EEA401 /* Pods */, E0C416BADC6D23D3F5D8CCA9 /* Frameworks */, + 1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -365,6 +368,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 41D57CDF80C517B01729B4E6 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d4..2ee0045 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -73,6 +73,12 @@ ReferencedContainer = "container:Runner.xcodeproj"> + + + + Bool { + #if DEBUG + var args = ProcessInfo.processInfo.arguments + args.append("-FIRDebugEnabled") + ProcessInfo.processInfo.setValue(args, forKey: "arguments") + #endif GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..4a23e31 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyAMgNFpkK0ss_uzNl51OqGyQHd0vFc9SxQ + GCM_SENDER_ID + 147309310656 + PLIST_VERSION + 1 + BUNDLE_ID + com.dbiz.partner + PROJECT_ID + dbiz-partner + STORAGE_BUCKET + dbiz-partner.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:147309310656:ios:aa59724d2c6b4620dc7325 + + \ No newline at end of file diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 1ce7af6..a06292f 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -7,6 +7,7 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:worker/core/services/analytics_service.dart'; import 'package:worker/features/account/domain/entities/address.dart'; import 'package:worker/features/account/presentation/pages/address_form_page.dart'; @@ -64,7 +65,7 @@ final routerProvider = Provider((ref) { return GoRouter( // Initial route - start with splash screen initialLocation: RouteNames.splash, - + observers: [AnalyticsService.observer], // Redirect based on auth state redirect: (context, state) { final isLoading = authState.isLoading; @@ -131,16 +132,22 @@ final routerProvider = Provider((ref) { GoRoute( path: RouteNames.splash, name: RouteNames.splash, - pageBuilder: (context, state) => - MaterialPage(key: state.pageKey, child: const SplashPage()), + pageBuilder: (context, state) => MaterialPage( + key: state.pageKey, + name: RouteNames.splash, + child: const SplashPage(), + ), ), // Authentication Routes GoRoute( path: RouteNames.login, name: RouteNames.login, - pageBuilder: (context, state) => - MaterialPage(key: state.pageKey, child: const LoginPage()), + pageBuilder: (context, state) => MaterialPage( + key: state.pageKey, + name: RouteNames.login, + child: const LoginPage(), + ), ), GoRoute( path: RouteNames.forgotPassword, @@ -192,16 +199,22 @@ final routerProvider = Provider((ref) { GoRoute( path: RouteNames.home, name: RouteNames.home, - pageBuilder: (context, state) => - MaterialPage(key: state.pageKey, child: const MainScaffold()), + pageBuilder: (context, state) => MaterialPage( + key: state.pageKey, + name: 'home', + child: const MainScaffold(), + ), ), // Products Route (full screen, no bottom nav) GoRoute( path: RouteNames.products, name: RouteNames.products, - pageBuilder: (context, state) => - MaterialPage(key: state.pageKey, child: const ProductsPage()), + pageBuilder: (context, state) => MaterialPage( + key: state.pageKey, + name: 'products', + child: const ProductsPage(), + ), ), // Product Detail Route @@ -212,6 +225,7 @@ final routerProvider = Provider((ref) { final productId = state.pathParameters['id']; return MaterialPage( key: state.pageKey, + name: 'product_detail', child: ProductDetailPage(productId: productId ?? ''), ); }, @@ -224,6 +238,7 @@ final routerProvider = Provider((ref) { final productId = state.pathParameters['id']; return MaterialPage( key: state.pageKey, + name: 'write_review', child: WriteReviewPage(productId: productId ?? ''), ); }, @@ -239,6 +254,7 @@ final routerProvider = Provider((ref) { final promotionId = state.pathParameters['id']; return MaterialPage( key: state.pageKey, + name: 'promotion_detail', child: PromotionDetailPage(promotionId: promotionId), ); }, @@ -248,8 +264,11 @@ final routerProvider = Provider((ref) { GoRoute( path: RouteNames.cart, name: RouteNames.cart, - pageBuilder: (context, state) => - MaterialPage(key: state.pageKey, child: const CartPage()), + pageBuilder: (context, state) => MaterialPage( + key: state.pageKey, + name: 'cart', + child: const CartPage(), + ), ), // Checkout Route diff --git a/lib/core/services/analytics_service.dart b/lib/core/services/analytics_service.dart new file mode 100644 index 0000000..4c03e79 --- /dev/null +++ b/lib/core/services/analytics_service.dart @@ -0,0 +1,88 @@ +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Firebase Analytics service for tracking user events across the app. +/// +/// Usage: +/// ```dart +/// // Log add to cart event +/// AnalyticsService.logAddToCart( +/// productId: 'SKU123', +/// productName: 'Gạch men 60x60', +/// price: 150000, +/// quantity: 2, +/// ); +/// ``` +class AnalyticsService { + AnalyticsService._(); + + static final FirebaseAnalytics _analytics = FirebaseAnalytics.instance; + + /// Get the analytics instance for NavigatorObserver + static FirebaseAnalytics get instance => _analytics; + + /// Get the observer for automatic screen tracking in GoRouter + static FirebaseAnalyticsObserver get observer => FirebaseAnalyticsObserver( + analytics: _analytics, + nameExtractor: (settings) { + // GoRouter uses the path as the route name + final name = settings.name; + if (name != null && name.isNotEmpty && name != '/') { + return name; + } + return settings.name ?? '/'; + }, + routeFilter: (route) => route is PageRoute, + ); + + /// Log screen view manually + static Future logScreenView({ + required String screenName, + String? screenClass, + }) async { + try { + await _analytics.logScreenView( + screenName: screenName, + screenClass: screenClass, + ); + debugPrint('📊 Analytics: screen_view - $screenName'); + } catch (e) { + debugPrint('📊 Analytics error: $e'); + } + } + + /// Log add to cart event + /// + /// [productId] - Product SKU or ID + /// [productName] - Product display name + /// [price] - Unit price in VND + /// [quantity] - Quantity added + /// [category] - Optional product category + static Future logAddToCart({ + required String productId, + required String productName, + required double price, + required int quantity, + String? brand, + }) async { + try { + await _analytics.logAddToCart( + currency: 'VND', + value: price * quantity, + items: [ + AnalyticsEventItem( + itemId: productId, + itemName: productName, + price: price, + quantity: quantity, + itemBrand: brand, + ), + ], + ); + debugPrint('📊 Analytics: add_to_cart - $productName x$quantity'); + } catch (e) { + debugPrint('📊 Analytics error: $e'); + } + } +} diff --git a/lib/features/cart/data/datasources/cart_remote_datasource.dart b/lib/features/cart/data/datasources/cart_remote_datasource.dart index eaa1a6b..cc9e820 100644 --- a/lib/features/cart/data/datasources/cart_remote_datasource.dart +++ b/lib/features/cart/data/datasources/cart_remote_datasource.dart @@ -16,8 +16,8 @@ abstract class CartRemoteDataSource { /// Add items to cart /// /// [items] - List of items with item_id, quantity, and amount - /// Returns list of cart items from API - Future> addToCart({ + /// Returns true if successful + Future addToCart({ required List> items, }); @@ -47,7 +47,7 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource { final DioClient _dioClient; @override - Future> addToCart({ + Future addToCart({ required List> items, }) async { try { @@ -78,8 +78,7 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource { throw const ParseException('Invalid response format from add to cart API'); } - // After adding, fetch updated cart - return await getUserCart(); + return true; } on DioException catch (e) { throw _handleDioException(e); } catch (e) { diff --git a/lib/features/cart/data/repositories/cart_repository_impl.dart b/lib/features/cart/data/repositories/cart_repository_impl.dart index 072b93a..56b5d01 100644 --- a/lib/features/cart/data/repositories/cart_repository_impl.dart +++ b/lib/features/cart/data/repositories/cart_repository_impl.dart @@ -32,7 +32,7 @@ class CartRepositoryImpl implements CartRepository { final CartLocalDataSource _localDataSource; @override - Future> addToCart({ + Future addToCart({ required List itemIds, required List quantities, required List prices, @@ -57,17 +57,24 @@ class CartRepositoryImpl implements CartRepository { // Try API first try { - final cartItemModels = await _remoteDataSource.addToCart(items: items); + final success = await _remoteDataSource.addToCart(items: items); - // Sync to local storage - await _localDataSource.saveCartItems(cartItemModels); + // Also save to local storage for offline access + if (success) { + for (int i = 0; i < itemIds.length; i++) { + final cartItemModel = _createCartItemModel( + productId: itemIds[i], + quantity: quantities[i], + unitPrice: prices[i], + ); + await _localDataSource.addCartItem(cartItemModel); + } + } - // Convert to domain entities - return cartItemModels.map(_modelToEntity).toList(); + return success; } on NetworkException catch (e) { // If no internet, add to local cart only if (e is NoInternetException || e is TimeoutException) { - // Add items to local cart for (int i = 0; i < itemIds.length; i++) { final cartItemModel = _createCartItemModel( productId: itemIds[i], @@ -79,9 +86,7 @@ class CartRepositoryImpl implements CartRepository { // TODO: Queue for sync when online - // Return local cart items - final localItems = await _localDataSource.getCartItems(); - return localItems.map(_modelToEntity).toList(); + return true; } rethrow; } @@ -167,7 +172,7 @@ class CartRepositoryImpl implements CartRepository { } @override - Future> updateQuantity({ + Future updateQuantity({ required String itemId, required double quantity, required double price, diff --git a/lib/features/cart/domain/repositories/cart_repository.dart b/lib/features/cart/domain/repositories/cart_repository.dart index 1c1e0e6..5b7d36c 100644 --- a/lib/features/cart/domain/repositories/cart_repository.dart +++ b/lib/features/cart/domain/repositories/cart_repository.dart @@ -22,14 +22,13 @@ import 'package:worker/features/cart/domain/entities/cart_item.dart'; abstract class CartRepository { /// Add items to cart /// - /// [items] - List of cart items to add /// [itemIds] - Product ERPNext item codes /// [quantities] - Quantities for each item /// [prices] - Unit prices for each item /// - /// Returns list of cart items on success. + /// Returns true if successful. /// Throws exceptions on failure. - Future> addToCart({ + Future addToCart({ required List itemIds, required List quantities, required List prices, @@ -57,9 +56,9 @@ abstract class CartRepository { /// [quantity] - New quantity /// [price] - Unit price /// - /// Returns updated cart item list. + /// Returns true if successful. /// Throws exceptions on failure. - Future> updateQuantity({ + Future updateQuantity({ required String itemId, required double quantity, required double price, diff --git a/lib/features/cart/presentation/pages/cart_page.dart b/lib/features/cart/presentation/pages/cart_page.dart index c01a431..76cc05d 100644 --- a/lib/features/cart/presentation/pages/cart_page.dart +++ b/lib/features/cart/presentation/pages/cart_page.dart @@ -419,7 +419,7 @@ class _CartPageState extends ConsumerState { ), const SizedBox(height: 24), ElevatedButton.icon( - onPressed: () => context.go(RouteNames.products), + onPressed: () => context.push(RouteNames.products), icon: const FaIcon(FontAwesomeIcons.bagShopping, size: 20), label: const Text('Xem sản phẩm'), ), diff --git a/lib/features/products/presentation/pages/product_detail_page.dart b/lib/features/products/presentation/pages/product_detail_page.dart index 3cbdbf6..00043c3 100644 --- a/lib/features/products/presentation/pages/product_detail_page.dart +++ b/lib/features/products/presentation/pages/product_detail_page.dart @@ -7,9 +7,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:worker/core/services/analytics_service.dart'; import 'package:worker/core/widgets/loading_indicator.dart'; import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/theme/colors.dart'; +import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart'; import 'package:worker/features/products/domain/entities/product.dart'; import 'package:worker/features/products/presentation/providers/products_provider.dart'; @@ -154,8 +156,25 @@ class _ProductDetailPageState extends ConsumerState { ); } - void _addToCart(Product product) { - // TODO: Add to cart logic + void _addToCart(Product product) async { + // Add to cart via provider + await ref.read(cartProvider.notifier).addToCart( + product, + quantity: _quantity.toDouble(), + ); + + // Log analytics event + await AnalyticsService.logAddToCart( + productId: product.productId, + productName: product.name, + price: product.basePrice, + quantity: _quantity, + brand: product.itemGroupName, + ); + + if (!mounted) return; + + ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( @@ -165,7 +184,7 @@ class _ProductDetailPageState extends ConsumerState { action: SnackBarAction( label: 'Xem giỏ hàng', onPressed: () { - // TODO: Navigate to cart + context.push('/cart'); }, ), ), @@ -245,20 +264,15 @@ class _ProductDetailPageState extends ConsumerState { ), // Sticky Action Bar - Positioned( - bottom: 0, - left: 0, - right: 0, - child: StickyActionBar( - quantity: _quantity, - unit: 'm²', - conversionOfSm: product.conversionOfSm, - uomFromIntroAttributes: product.getIntroAttribute('UOM'), - onIncrease: _increaseQuantity, - onDecrease: _decreaseQuantity, - onQuantityChanged: _updateQuantity, - onAddToCart: () => _addToCart(product), - ), + StickyActionBar( + quantity: _quantity, + unit: 'm²', + conversionOfSm: product.conversionOfSm, + uomFromIntroAttributes: product.getIntroAttribute('UOM'), + onIncrease: _increaseQuantity, + onDecrease: _decreaseQuantity, + onQuantityChanged: _updateQuantity, + onAddToCart: () => _addToCart(product), ), ], ); diff --git a/lib/features/products/presentation/pages/products_page.dart b/lib/features/products/presentation/pages/products_page.dart index b05c3b9..364be4b 100644 --- a/lib/features/products/presentation/pages/products_page.dart +++ b/lib/features/products/presentation/pages/products_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:worker/core/services/analytics_service.dart'; import 'package:worker/core/widgets/loading_indicator.dart'; import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/router/app_router.dart'; @@ -44,7 +45,13 @@ class ProductsPage extends ConsumerWidget { appBar: AppBar( leading: IconButton( icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20), - onPressed: () => context.pop(), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(RouteNames.home); + } + }, ), title: Text('Sản phẩm', style: TextStyle(color: colorScheme.onSurface)), elevation: AppBarSpecs.elevation, @@ -135,6 +142,14 @@ class ProductsPage extends ConsumerWidget { // Add to cart ref.read(cartProvider.notifier).addToCart(product); + AnalyticsService.logAddToCart( + productId: product.productId, + productName: product.name, + price: product.basePrice, + quantity: 1, + brand: product.itemGroupName, + ); + // Show SnackBar with manual dismissal final messenger = ScaffoldMessenger.of(context) ..clearSnackBars(); diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..8920f9c --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,68 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyA60iGPuHOQMJUA0m5aSimzevPAiiaB4pE', + appId: '1:147309310656:android:86613d8ffc85576fdc7325', + messagingSenderId: '147309310656', + projectId: 'dbiz-partner', + storageBucket: 'dbiz-partner.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyAMgNFpkK0ss_uzNl51OqGyQHd0vFc9SxQ', + appId: '1:147309310656:ios:aa59724d2c6b4620dc7325', + messagingSenderId: '147309310656', + projectId: 'dbiz-partner', + storageBucket: 'dbiz-partner.firebasestorage.app', + iosBundleId: 'com.dbiz.partner', + ); +} diff --git a/lib/main.dart b/lib/main.dart index e83ee4c..c201914 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:worker/app.dart'; import 'package:worker/core/database/app_settings_box.dart'; import 'package:worker/core/database/hive_initializer.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'firebase_options.dart'; /// Main entry point of the Worker Mobile App /// @@ -23,6 +25,10 @@ void main() async { // Ensure Flutter is initialized before async operations WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + // Set preferred device orientations await SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, diff --git a/pubspec.lock b/pubspec.lock index e889e81..5cf1acc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1016,10 +1016,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760 + sha256: "023a71afb4d7cfb5529d0f2636aa8b43db66257905b9486d702085989769c5f2" url: "https://pub.dev" source: hosted - version: "5.2.3" + version: "7.1.3" mockito: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index f3b2e20..0e28655 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.1+22 +version: 1.0.1+23 environment: sdk: ^3.10.0 @@ -67,7 +67,7 @@ dependencies: shimmer: ^3.0.0 lottie: ^3.1.2 qr_flutter: ^4.1.0 - mobile_scanner: ^5.2.3 + mobile_scanner: ^7.0.0 font_awesome_flutter: ^10.7.0 loading_animation_widget: ^1.3.0