add firebase, add screen flow
This commit is contained in:
@@ -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<GoRouter>((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<GoRouter>((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<GoRouter>((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<GoRouter>((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<GoRouter>((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<GoRouter>((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<GoRouter>((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
|
||||
|
||||
88
lib/core/services/analytics_service.dart
Normal file
88
lib/core/services/analytics_service.dart
Normal file
@@ -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<void> 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<void> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<List<CartItemModel>> addToCart({
|
||||
/// Returns true if successful
|
||||
Future<bool> addToCart({
|
||||
required List<Map<String, dynamic>> items,
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource {
|
||||
final DioClient _dioClient;
|
||||
|
||||
@override
|
||||
Future<List<CartItemModel>> addToCart({
|
||||
Future<bool> addToCart({
|
||||
required List<Map<String, dynamic>> 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) {
|
||||
|
||||
@@ -32,7 +32,7 @@ class CartRepositoryImpl implements CartRepository {
|
||||
final CartLocalDataSource _localDataSource;
|
||||
|
||||
@override
|
||||
Future<List<CartItem>> addToCart({
|
||||
Future<bool> addToCart({
|
||||
required List<String> itemIds,
|
||||
required List<double> quantities,
|
||||
required List<double> 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<List<CartItem>> updateQuantity({
|
||||
Future<bool> updateQuantity({
|
||||
required String itemId,
|
||||
required double quantity,
|
||||
required double price,
|
||||
|
||||
@@ -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<List<CartItem>> addToCart({
|
||||
Future<bool> addToCart({
|
||||
required List<String> itemIds,
|
||||
required List<double> quantities,
|
||||
required List<double> 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<List<CartItem>> updateQuantity({
|
||||
Future<bool> updateQuantity({
|
||||
required String itemId,
|
||||
required double quantity,
|
||||
required double price,
|
||||
|
||||
@@ -419,7 +419,7 @@ class _CartPageState extends ConsumerState<CartPage> {
|
||||
),
|
||||
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'),
|
||||
),
|
||||
|
||||
@@ -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<ProductDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
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<ProductDetailPage> {
|
||||
action: SnackBarAction(
|
||||
label: 'Xem giỏ hàng',
|
||||
onPressed: () {
|
||||
// TODO: Navigate to cart
|
||||
context.push('/cart');
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -245,20 +264,15 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
),
|
||||
|
||||
// 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
68
lib/firebase_options.dart
Normal file
68
lib/firebase_options.dart
Normal file
@@ -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',
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user