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:
parent
aaeae104c5
commit
94b4fc1723
@ -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')],
|
||||
|
41
lib/components/primary_button_component.dart
Normal file
41
lib/components/primary_button_component.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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'),
|
||||
),
|
||||
|
@ -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),
|
||||
|
||||
|
@ -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
5
lib/pages/pages.dart
Normal 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';
|
128
lib/pages/product_form_page/product_form_page.dart
Normal file
128
lib/pages/product_form_page/product_form_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -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) {
|
||||
|
@ -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';
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user