From 50aed06aadd3d1a8a09e670fae34d0bd03e4240f Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Sun, 30 Nov 2025 14:48:02 +0700 Subject: [PATCH] fix --- .../presentation/pages/account_page.dart | 632 ++++++++++-------- pubspec.yaml | 2 +- 2 files changed, 367 insertions(+), 267 deletions(-) diff --git a/lib/features/account/presentation/pages/account_page.dart b/lib/features/account/presentation/pages/account_page.dart index 89da4bc..7d6c36d 100644 --- a/lib/features/account/presentation/pages/account_page.dart +++ b/lib/features/account/presentation/pages/account_page.dart @@ -8,10 +8,13 @@ /// - Logout button library; +import 'dart:async'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/database/hive_initializer.dart'; @@ -33,99 +36,40 @@ class AccountPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final userInfoAsync = ref.watch(userInfoProvider); - return Scaffold( backgroundColor: const Color(0xFFF4F6F8), body: SafeArea( - child: userInfoAsync.when( - loading: () => const Center( + child: RefreshIndicator( + onRefresh: () async { + await ref.read(userInfoProvider.notifier).refresh(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(color: AppColors.primaryBlue), - SizedBox(height: AppSpacing.md), - Text( - 'Đang tải thông tin...', - style: TextStyle( - fontSize: 14, - color: AppColors.grey500, - ), - ), - ], - ), - ), - error: (error, stack) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FaIcon( - FontAwesomeIcons.circleExclamation, - size: 64, - color: AppColors.danger, - ), - const SizedBox(height: AppSpacing.lg), - const Text( - 'Không thể tải thông tin tài khoản', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), + // Simple Header + _buildHeader(), const SizedBox(height: AppSpacing.md), - Text( - error.toString(), - style: const TextStyle( - fontSize: 14, - color: AppColors.grey500, - ), - textAlign: TextAlign.center, - ), + + // User Profile Card - only this depends on provider + const _ProfileCardSection(), + const SizedBox(height: AppSpacing.md), + + // Account Menu Section - independent + _buildAccountMenu(context), + const SizedBox(height: AppSpacing.md), + + // Support Section - independent + _buildSupportSection(context), + const SizedBox(height: AppSpacing.md), + + // Logout Button - independent (uses ref only for logout action) + _LogoutButton(), + const SizedBox(height: AppSpacing.lg), - ElevatedButton.icon( - onPressed: () => - ref.read(userInfoProvider.notifier).refresh(), - icon: - const FaIcon(FontAwesomeIcons.arrowsRotate, size: 16), - label: const Text('Thử lại'), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryBlue, - foregroundColor: AppColors.white, - ), - ), ], ), ), - data: (userInfo) => RefreshIndicator( - onRefresh: () async { - await ref.read(userInfoProvider.notifier).refresh(); - }, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Column( - spacing: AppSpacing.md, - children: [ - // Simple Header - _buildHeader(), - - // User Profile Card with API data - _buildProfileCard(context, userInfo), - - // Account Menu Section - _buildAccountMenu(context), - - // Support Section - _buildSupportSection(context), - - // Logout Button - _buildLogoutButton(context, ref), - - const SizedBox(height: AppSpacing.lg), - ], - ), - ), - ), ), ), ); @@ -157,138 +101,6 @@ class AccountPage extends ConsumerWidget { ); } - /// Build user profile card with avatar and info - Widget _buildProfileCard( - BuildContext context, - domain.UserInfo userInfo, - ) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), - padding: const EdgeInsets.all(AppSpacing.md), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(AppRadius.card), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - // Avatar with API data or gradient fallback - userInfo.avatarUrl != null - ? ClipOval( - child: CachedNetworkImage( - imageUrl: userInfo.avatarUrl!, - width: 80, - height: 80, - fit: BoxFit.cover, - placeholder: (context, url) => Container( - width: 80, - height: 80, - decoration: const BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [Color(0xFF005B9A), Color(0xFF38B6FF)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: const Center( - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ), - ), - errorWidget: (context, url, error) => Container( - width: 80, - height: 80, - decoration: const BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [Color(0xFF005B9A), Color(0xFF38B6FF)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Center( - child: Text( - userInfo.initials, - style: const TextStyle( - color: Colors.white, - fontSize: 32, - fontWeight: FontWeight.w700, - ), - ), - ), - ), - ), - ) - : Container( - width: 80, - height: 80, - decoration: const BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [Color(0xFF005B9A), Color(0xFF38B6FF)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Center( - child: Text( - userInfo.initials, - style: const TextStyle( - color: Colors.white, - fontSize: 32, - fontWeight: FontWeight.w700, - ), - ), - ), - ), - const SizedBox(width: AppSpacing.md), - - // User info from API - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: AppSpacing.xs, - children: [ - Text( - userInfo.fullName, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.grey900, - ), - ), - Text( - '${_getRoleDisplayName(userInfo.role)} · Hạng ${userInfo.tierDisplayName}', - style: const TextStyle( - fontSize: 13, - color: AppColors.grey500, - ), - ), - if (userInfo.phoneNumber != null) - Text( - userInfo.phoneNumber!, - style: const TextStyle( - fontSize: 13, - color: AppColors.primaryBlue, - ), - ), - ], - ), - ), - ], - ), - ); - } - /// Build account menu section Widget _buildAccountMenu(BuildContext context) { return Container( @@ -378,14 +190,14 @@ class AccountPage extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Section title - Padding( - padding: const EdgeInsets.fromLTRB( + const Padding( + padding: EdgeInsets.fromLTRB( AppSpacing.md, AppSpacing.md, AppSpacing.md, AppSpacing.sm, ), - child: const Text( + child: Text( 'Hỗ trợ', style: TextStyle( fontSize: 16, @@ -434,29 +246,6 @@ class AccountPage extends ConsumerWidget { ); } - /// Build logout button - Widget _buildLogoutButton(BuildContext context, WidgetRef ref) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), - width: double.infinity, - child: OutlinedButton.icon( - onPressed: () { - _showLogoutConfirmation(context, ref); - }, - icon: const FaIcon(FontAwesomeIcons.arrowRightFromBracket, size: 18), - label: const Text('Đăng xuất'), - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.danger, - side: const BorderSide(color: AppColors.danger, width: 1.5), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), - ), - ), - ), - ); - } - /// Show coming soon message void _showComingSoon(BuildContext context) { ScaffoldMessenger.of(context).showSnackBar( @@ -499,6 +288,324 @@ class AccountPage extends ConsumerWidget { ), ); } +} + +/// Profile Card Section Widget +/// +/// Isolated widget that depends on userInfoProvider. +/// Shows loading/error/data states independently. +class _ProfileCardSection extends ConsumerWidget { + const _ProfileCardSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userInfoAsync = ref.watch(userInfoProvider); + + return Container( + margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + child: userInfoAsync.when( + loading: () => _buildLoadingCard(), + error: (error, stack) => _buildErrorCard(context, ref, error), + data: (userInfo) => _buildProfileCard(context, userInfo), + ), + ); + } + + Widget _buildLoadingCard() { + return Container( + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(AppRadius.card), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // Avatar placeholder + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.grey100, + ), + child: const Center( + child: CircularProgressIndicator( + color: AppColors.primaryBlue, + strokeWidth: 2, + ), + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 20, + width: 150, + decoration: BoxDecoration( + color: AppColors.grey100, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 8), + Container( + height: 14, + width: 100, + decoration: BoxDecoration( + color: AppColors.grey100, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildErrorCard(BuildContext context, WidgetRef ref, Object error) { + return Container( + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(AppRadius.card), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: AppColors.grey100, + ), + child: const Center( + child: FaIcon( + FontAwesomeIcons.circleExclamation, + color: AppColors.danger, + size: 32, + ), + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Không thể tải thông tin', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 8), + GestureDetector( + onTap: () => ref.read(userInfoProvider.notifier).refresh(), + child: const Text( + 'Nhấn để thử lại', + style: TextStyle( + fontSize: 14, + color: AppColors.primaryBlue, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildProfileCard(BuildContext context, domain.UserInfo userInfo) { + return Container( + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(AppRadius.card), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // Avatar with API data or gradient fallback + userInfo.avatarUrl != null + ? ClipOval( + child: CachedNetworkImage( + imageUrl: userInfo.avatarUrl!, + width: 80, + height: 80, + fit: BoxFit.cover, + placeholder: (context, url) => Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [Color(0xFF005B9A), Color(0xFF38B6FF)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: const Center( + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ), + ), + errorWidget: (context, url, error) => Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [Color(0xFF005B9A), Color(0xFF38B6FF)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Center( + child: Text( + userInfo.initials, + style: const TextStyle( + color: Colors.white, + fontSize: 32, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ), + ) + : Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [Color(0xFF005B9A), Color(0xFF38B6FF)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Center( + child: Text( + userInfo.initials, + style: const TextStyle( + color: Colors.white, + fontSize: 32, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + const SizedBox(width: AppSpacing.md), + + // User info from API + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + userInfo.fullName, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.grey900, + ), + ), + const SizedBox(height: AppSpacing.xs), + Text( + '${_getRoleDisplayName(userInfo.role)} · Hạng ${userInfo.tierDisplayName}', + style: const TextStyle( + fontSize: 13, + color: AppColors.grey500, + ), + ), + if (userInfo.phoneNumber != null) ...[ + const SizedBox(height: AppSpacing.xs), + Text( + userInfo.phoneNumber!, + style: const TextStyle( + fontSize: 13, + color: AppColors.primaryBlue, + ), + ), + ], + ], + ), + ), + ], + ), + ); + } + + /// Get Vietnamese display name for user role + String _getRoleDisplayName(UserRole role) { + switch (role) { + case UserRole.customer: + return 'Khách hàng'; + case UserRole.distributor: + return 'Đại lý phân phối'; + case UserRole.admin: + return 'Quản trị viên'; + case UserRole.staff: + return 'Nhân viên'; + } + } +} + +/// Logout Button Widget +/// +/// Isolated widget that handles logout functionality. +class _LogoutButton extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () { + _showLogoutConfirmation(context, ref); + }, + icon: const FaIcon(FontAwesomeIcons.arrowRightFromBracket, size: 18), + label: const Text('Đăng xuất'), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.danger, + side: const BorderSide(color: AppColors.danger, width: 1.5), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.button), + ), + ), + ), + ); + } /// Show logout confirmation dialog void _showLogoutConfirmation(BuildContext context, WidgetRef ref) { @@ -527,16 +634,17 @@ class AccountPage extends ConsumerWidget { /// Handles the complete logout process: /// 1. Close confirmation dialog /// 2. Show loading indicator - /// 3. Clear Hive local data - /// 4. Call auth provider logout (clears session, gets new public session) - /// 5. Navigate to login screen (handled by router redirect) - /// 6. Show success message + /// 3. Clear ALL Hive local data (reset, not just user data) + /// 4. Clear ALL Flutter Secure Storage keys + /// 5. Call auth provider logout (clears session, gets new public session) + /// 6. Navigate to login screen (handled by router redirect) + /// 7. Show success message Future _performLogout(BuildContext context, WidgetRef ref) async { // Close confirmation dialog Navigator.of(context).pop(); // Show loading dialog - showDialog( + unawaited(showDialog( context: context, barrierDismissible: false, builder: (context) => const Center( @@ -554,15 +662,21 @@ class AccountPage extends ConsumerWidget { ), ), ), - ); + )); try { - // Clear Hive local data (cart, favorites, cached data) - await HiveInitializer.logout(); + // 1. Clear ALL Hive data (complete reset) + await HiveInitializer.reset(); - // Call auth provider logout + // 2. Clear ALL Flutter Secure Storage keys + const secureStorage = FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), + ); + await secureStorage.deleteAll(); + + // 3. Call auth provider logout // This will: - // - Clear FlutterSecureStorage session // - Clear FrappeAuthService session // - Get new public session for login/registration // - Update auth state to null (logged out) @@ -604,18 +718,4 @@ class AccountPage extends ConsumerWidget { } } } - - /// Get Vietnamese display name for user role - String _getRoleDisplayName(UserRole role) { - switch (role) { - case UserRole.customer: - return 'Khách hàng'; - case UserRole.distributor: - return 'Đại lý phân phối'; - case UserRole.admin: - return 'Quản trị viên'; - case UserRole.staff: - return 'Nhân viên'; - } - } } diff --git a/pubspec.yaml b/pubspec.yaml index e0b06aa..beec3e4 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+18 +version: 1.0.1+20 environment: sdk: ^3.10.0