refactor: Refactor login flow and add product form route

Refactors the login page and model for improved separation of concerns. Moves navigation logic from the model to the page using success callbacks. Integrates a dedicated primary button component for the login action, showing loading state.

Adds a new secure route for a product form page and updates the scanner page to navigate to it when viewing scanned product details.

Simplifies the callback signature for viewing product details in the scanned product component. Also includes minor adjustments to the splash screen delay and theme definition.
This commit is contained in:
mandreshope 2025-06-23 10:22:24 +03:00
parent aaeae104c5
commit 94b4fc1723
12 changed files with 250 additions and 46 deletions

View File

@ -24,7 +24,6 @@ class _InnerApp extends ConsumerWidget {
theme: ThemeData(
fontFamily: 'Roboto',
scaffoldBackgroundColor: Colors.white,
primarySwatch: Colors.blue,
),
locale: Locale('fr'),
supportedLocales: [Locale('fr', 'FR'), Locale('en', 'US')],

View File

@ -0,0 +1,41 @@
import 'package:barcode_scanner/components/loading_progress_component.dart';
import 'package:flutter/material.dart';
class PrimaryButtonComponent extends StatelessWidget {
const PrimaryButtonComponent({
super.key,
required this.text,
required this.onPressed,
this.loading = false,
});
final void Function()? onPressed;
final String text;
final bool loading;
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: loading ? null : onPressed,
child: loading
? SizedBox(
height: 20,
width: 20,
child: Center(
child: const LoadingProgressComponent(color: Colors.white),
),
)
: Text(
text,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
);
}
}

View File

@ -15,7 +15,7 @@ class ProductScannedComponent extends StatefulWidget {
final String brands;
final String img;
final Future Function()? onRescan;
final Future Function()? onDetails;
final Function()? onDetails;
@override
State<ProductScannedComponent> createState() =>
@ -151,8 +151,8 @@ class _ProductScannedComponentState extends State<ProductScannedComponent> {
),
Expanded(
child: FilledButton(
onPressed: () async {
await widget.onDetails?.call();
onPressed: () {
widget.onDetails?.call();
},
child: Text('Voir détails'),
),

View File

@ -1,4 +1,6 @@
import 'package:barcode_scanner/components/primary_button_component.dart';
import 'package:barcode_scanner/pages/login_page/login_page_model.dart';
import 'package:barcode_scanner/router/go_router_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -128,30 +130,28 @@ class _LoginPageState extends ConsumerState<LoginPage> {
const SizedBox(height: 24),
/// Sign In button
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () {
ref
.read(loginPageModelProvider.notifier)
.signIn(
context,
email: email.text,
password: password.text,
);
Consumer(
builder: (context, ref, child) {
final state = ref.watch(loginPageModelProvider);
return PrimaryButtonComponent(
loading: state.loading,
onPressed: () {
ref
.read(loginPageModelProvider.notifier)
.signIn(
email: email.text,
password: password.text,
onSuccess: () {
WidgetsBinding.instance
.addPostFrameCallback((timeStamp) {
SplashRoute().go(context);
});
},
);
},
text: 'Sign In',
);
},
child: const Text(
'Sign In',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(height: 16),

View File

@ -1,8 +1,6 @@
import 'package:barcode_scanner/router/go_router_builder.dart';
import 'package:barcode_scanner/services/secure_storage.dart';
import 'package:barcode_scanner/services/token_provider.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -49,20 +47,22 @@ class LoginPageModel extends StateNotifier<LoginPageState> {
}
}
Future<void> signIn(
BuildContext context, {
Future<void> signIn({
required String email,
required String password,
VoidCallback? onSuccess,
VoidCallback? onError,
}) async {
try {
state = state.copyWith(loading: true);
await Future.delayed(Duration(seconds: 5));
if (email == "user@yopmail.com" && password == "password") {
setTokenInLocal(context, 'token');
setTokenInLocal('token');
state = state.copyWith(
loading: false,
status: LoginPageStateStatus.logOut,
);
onSuccess?.call();
} else {
state = state.copyWith(
loading: false,
@ -79,7 +79,7 @@ class LoginPageModel extends StateNotifier<LoginPageState> {
}
}
Future<void> setTokenInLocal(BuildContext context, String token) async {
Future<void> setTokenInLocal(String token) async {
await Future.wait([
tokenProvider.setToken(token),
tokenProvider.setRefreshToken(token),
@ -87,9 +87,6 @@ class LoginPageModel extends StateNotifier<LoginPageState> {
state = state.copyWith(loading: false, status: LoginPageStateStatus.logged);
debugPrint("$token");
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
SplashRoute().go(context);
});
}
Future<void> logOut() async {

5
lib/pages/pages.dart Normal file
View File

@ -0,0 +1,5 @@
export 'home_page/home_page.dart';
export 'login_page/login_page.dart';
export 'product_form_page/product_form_page.dart';
export 'scanner_page/scanner_page.dart';
export 'splash_page/splash_page.dart';

View File

@ -0,0 +1,128 @@
import 'package:barcode_scanner/router/go_secure_router_builder.dart';
import 'package:flutter/material.dart';
class ProductFormPage extends StatefulWidget {
const ProductFormPage({super.key});
@override
State<ProductFormPage> createState() => _ProductFormPageState();
}
class _ProductFormPageState extends State<ProductFormPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController name = TextEditingController();
final TextEditingController code = TextEditingController();
final TextEditingController description = TextEditingController();
final TextEditingController price = TextEditingController();
@override
void dispose() {
name.dispose();
code.dispose();
description.dispose();
price.dispose();
super.dispose();
}
InputDecoration _inputStyle(String label) {
return InputDecoration(
labelText: label,
filled: true,
fillColor: Colors.grey[100],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop();
},
),
title: const Text('Formulaire Produit'),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0,
foregroundColor: Colors.black,
),
body: Padding(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: ListView(
children: [
/// Nom du produit
TextFormField(
controller: name,
decoration: _inputStyle("Nom du produit"),
validator: (value) =>
(value == null || value.isEmpty) ? 'Champ requis' : null,
),
const SizedBox(height: 16),
/// Code barre / code produit
TextFormField(
controller: code,
decoration: _inputStyle("Code produit / Code barre"),
validator: (value) =>
(value == null || value.isEmpty) ? 'Champ requis' : null,
),
const SizedBox(height: 16),
/// Description
TextFormField(
controller: description,
decoration: _inputStyle("Description"),
maxLines: 3,
),
const SizedBox(height: 16),
/// Prix
TextFormField(
controller: price,
decoration: _inputStyle("Prix"),
keyboardType: TextInputType.number,
validator: (value) =>
(value == null || value.isEmpty) ? 'Champ requis' : null,
),
const SizedBox(height: 24),
/// Bouton sauvegarder
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Traitement ici
print("Produit : ${name.text}, Code : ${code.text}");
HomeRoute().go(context);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Sauvegarder',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:barcode_scanner/backend/api/api_calls.dart';
import 'package:barcode_scanner/components/product_scanned_component.dart';
import 'package:barcode_scanner/router/go_secure_router_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@ -121,7 +122,7 @@ class _ScannerPageState extends ConsumerState<ScannerPage>
FocusScope.of(dialogContext).unfocus();
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 0.9,
child: ProductScannedComponent(
img: product["image_url"],
@ -133,11 +134,12 @@ class _ScannerPageState extends ConsumerState<ScannerPage>
qrcodeFound = false;
mobileScannerController.start();
},
onDetails: () async {
onDetails: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute().push(context);
},
),
),
@ -199,11 +201,12 @@ class _ScannerPageState extends ConsumerState<ScannerPage>
qrcodeFound = false;
mobileScannerController.start();
},
onDetails: () async {
onDetails: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute().push(context);
},
),
),

View File

@ -10,7 +10,7 @@ class SplashPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
Future.delayed(Durations.extralong4).then((value) {
Future.delayed(Durations.extralong2).then((value) {
final authViewModel = ref.watch(loginPageModelProvider);
if (authViewModel.status.isLogged) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {

View File

@ -1,8 +1,5 @@
import 'dart:async';
import 'package:barcode_scanner/pages/login_page/login_page.dart';
import 'package:barcode_scanner/pages/login_page/login_page_model.dart';
import 'package:barcode_scanner/pages/splash_page/splash_page.dart';
import 'package:barcode_scanner/provider_container.dart';
import 'package:barcode_scanner/pages/pages.dart';
import 'package:barcode_scanner/router/go_secure_router_builder.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

View File

@ -1,8 +1,7 @@
import 'package:barcode_scanner/pages/home_page/home_page.dart';
import 'package:barcode_scanner/pages/login_page/login_page_model.dart';
import 'package:barcode_scanner/pages/scanner_page/scanner_page.dart';
import 'package:barcode_scanner/provider_container.dart';
import 'package:barcode_scanner/router/go_router_builder.dart';
import 'package:barcode_scanner/pages/pages.dart';
import 'package:flutter/material.dart';
export 'package:go_router/go_router.dart';
part 'go_secure_router_builder.g.dart';
@ -12,6 +11,7 @@ final appSecureRoutes = $appRoutes;
const String _securePage = 'SecurePage';
const String _homePage = 'HomePage';
const String _scannerPage = 'ScannerPage';
const String _productFormPage = 'ProductFormPage';
// Groupe des routes sécurisées
@TypedGoRoute<SecureRoute>(
@ -19,6 +19,7 @@ const String _scannerPage = 'ScannerPage';
routes: [
TypedGoRoute<HomeRoute>(path: _homePage),
TypedGoRoute<ScannerRoute>(path: _scannerPage),
TypedGoRoute<ProductFormRoute>(path: _productFormPage),
],
)
class SecureRoute extends GoRouteData with _$SecureRoute {
@ -51,3 +52,10 @@ class ScannerRoute extends GoRouteData with _$ScannerRoute {
@override
Widget build(BuildContext context, GoRouterState state) => ScannerPage();
}
class ProductFormRoute extends GoRouteData with _$ProductFormRoute {
const ProductFormRoute();
@override
Widget build(BuildContext context, GoRouterState state) => ProductFormPage();
}

View File

@ -15,6 +15,11 @@ RouteBase get $secureRoute => GoRouteData.$route(
routes: [
GoRouteData.$route(path: 'HomePage', factory: _$HomeRoute._fromState),
GoRouteData.$route(path: 'ScannerPage', factory: _$ScannerRoute._fromState),
GoRouteData.$route(
path: 'ProductFormPage',
factory: _$ProductFormRoute._fromState,
),
],
);
@ -77,3 +82,24 @@ mixin _$ScannerRoute on GoRouteData {
@override
void replace(BuildContext context) => context.replace(location);
}
mixin _$ProductFormRoute on GoRouteData {
static ProductFormRoute _fromState(GoRouterState state) =>
const ProductFormRoute();
@override
String get location => GoRouteData.$location('/SecurePage/ProductFormPage');
@override
void go(BuildContext context) => context.go(location);
@override
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}