Initial commit: app Fideliter client (Flutter)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 21:32:39 +02:00
commit 01a0f1029f
49 changed files with 2613 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+30
View File
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "ad70ec4617166f1c38e5d2bfd388af71fda14f06"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
- platform: android
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+59
View File
@@ -0,0 +1,59 @@
# Ma fidélité — App client
App mobile (Flutter) pour les **clients** du programme de fidélité. Le client crée
son compte lui-même, obtient sa **carte de fidélité (QR code)**, suit ses **points**
et voit les **récompenses** qu'il peut obtenir.
**Backend : Supabase** — le même projet que l'app magasin
[`app_fideliter`](../app_fideliter). Les points ajoutés en caisse apparaissent
côté client au rafraîchissement.
## Parcours
1. **Accueil** : choix « Créer un compte » ou « J'ai déjà un compte ».
2. **Création de compte** : email + mot de passe + nom (+ téléphone). Le code de
fidélité et le QR sont générés **automatiquement** côté serveur — le gérant n'a
rien à saisir.
3. **Accueil client** : carte avec le solde de points, le **QR code** à présenter
en caisse, la progression vers chaque récompense, et l'historique.
> Sécurité : un client ne peut voir que **ses** données et ne peut **pas** modifier
> ses points (règles RLS + triggers Supabase, voir `schema.sql` de l'app magasin).
## Configuration
Même projet Supabase que l'app magasin. Voir
**[`../app_fideliter/supabase/SETUP.md`](../app_fideliter/supabase/SETUP.md)**.
Coller l'URL + la clé anon dans [`lib/supabase_config.dart`](lib/supabase_config.dart)
(les **mêmes** valeurs que l'app magasin).
## Techno
- Flutter (Material 3, même thème que l'app magasin)
- `supabase_flutter` (auth client + données), `qr_flutter` (affichage du QR)
## Lancer l'app
```bash
flutter run
```
## Structure
```text
lib/
├── main.dart # init (locale fr, Supabase) + thème
├── supabase_config.dart # URL + clé anon (À REMPLIR)
├── theme.dart
├── models/ # client, mouvement, recompense
├── services/
│ └── session_client.dart # données du client connecté (points, historique…)
├── screens/
│ ├── auth_gate.dart # accueil ↔ profil ↔ app selon la session
│ ├── welcome_screen.dart # choix connexion / création de compte
│ ├── login_screen.dart
│ ├── signup_screen.dart
│ ├── complete_profile_screen.dart # finalisation du profil (après confirmation email)
│ └── home_client_screen.dart # carte de fidélité (points + QR), récompenses, historique
└── utils/format.dart
```
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+45
View File
@@ -0,0 +1,45 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.magasin.app_fideliter_client"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.magasin.app_fideliter_client"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+48
View File
@@ -0,0 +1,48 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Connexion au backend Supabase. -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="Ma fidélité"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.magasin.app_fideliter_client
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0E7A55</color>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+6
View File
@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+38
View File
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'screens/auth_gate.dart';
import 'supabase_config.dart';
import 'theme.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting('fr_FR', null);
if (SupabaseConfig.estConfigure) {
await Supabase.initialize(
url: SupabaseConfig.url,
publishableKey: SupabaseConfig.anonKey,
);
}
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Ma fidélité',
debugShowCheckedModeBanner: false,
theme: AppTheme.clair(),
darkTheme: AppTheme.sombre(),
themeMode: ThemeMode.light,
home: const AuthGate(),
);
}
}
+35
View File
@@ -0,0 +1,35 @@
/// Le compte fidélité du client connecté. Le [code] est encodé dans son QR.
class Client {
final String id; // uuid Supabase
final String code; // ex : « FID-3F9K2A »
final String nom;
final String? prenom;
final String? telephone;
final double points;
const Client({
required this.id,
required this.code,
required this.nom,
this.prenom,
this.telephone,
this.points = 0,
});
/// Nom complet affiché : « Prénom Nom » (ou juste le nom si pas de prénom).
String get nomComplet {
final p = (prenom ?? '').trim();
return p.isEmpty ? nom : '$p $nom';
}
factory Client.fromMap(Map<String, dynamic> map) {
return Client(
id: map['id'].toString(),
code: (map['code'] as String?) ?? '',
nom: (map['nom'] as String?) ?? '',
prenom: map['prenom'] as String?,
telephone: map['telephone'] as String?,
points: (map['points'] as num?)?.toDouble() ?? 0,
);
}
}
+39
View File
@@ -0,0 +1,39 @@
/// Type d'un mouvement de points dans l'historique.
class TypeMouvement {
static const facture = 'facture';
static const recompense = 'recompense';
static const ajustement = 'ajustement';
}
/// Une ligne d'historique : gain (facture) ou dépense (récompense).
class Mouvement {
final String id;
final String type;
final double? montantEuros;
final double points; // signé
final String libelle;
final DateTime date;
const Mouvement({
required this.id,
required this.type,
this.montantEuros,
required this.points,
required this.libelle,
required this.date,
});
bool get estGain => points >= 0;
factory Mouvement.fromMap(Map<String, dynamic> map) {
return Mouvement(
id: map['id'].toString(),
type: (map['type'] as String?) ?? TypeMouvement.ajustement,
montantEuros: (map['montant_euros'] as num?)?.toDouble(),
points: (map['points'] as num?)?.toDouble() ?? 0,
libelle: (map['libelle'] as String?) ?? '',
date: DateTime.tryParse(map['created_at']?.toString() ?? '')?.toLocal() ??
DateTime.fromMillisecondsSinceEpoch(0),
);
}
}
+23
View File
@@ -0,0 +1,23 @@
/// Une récompense du catalogue que le client peut viser avec ses points.
class Recompense {
final String id;
final String nom;
final double coutPoints;
final bool actif;
const Recompense({
required this.id,
required this.nom,
required this.coutPoints,
this.actif = true,
});
factory Recompense.fromMap(Map<String, dynamic> map) {
return Recompense(
id: map['id'].toString(),
nom: (map['nom'] as String?) ?? '',
coutPoints: (map['cout_points'] as num?)?.toDouble() ?? 0,
actif: (map['actif'] as bool?) ?? true,
);
}
}
+99
View File
@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../services/session_client.dart';
import '../supabase_config.dart';
import 'complete_profile_screen.dart';
import 'home_client_screen.dart';
import 'welcome_screen.dart';
/// Aiguille entre l'accueil non connecté, la finalisation de profil et l'app.
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context) {
if (!SupabaseConfig.estConfigure) return const _EcranNonConfigure();
return StreamBuilder<AuthState>(
stream: supabase.auth.onAuthStateChange,
builder: (context, snapshot) {
final session = supabase.auth.currentSession;
if (session == null) return const WelcomeScreen();
return _SessionChargee(key: ValueKey(session.user.id));
},
);
}
}
/// Charge les données du client connecté, puis affiche l'app (ou la
/// finalisation de profil si le compte n'a pas encore de fiche fidélité).
class _SessionChargee extends StatefulWidget {
const _SessionChargee({super.key});
@override
State<_SessionChargee> createState() => _SessionChargeeState();
}
class _SessionChargeeState extends State<_SessionChargee> {
late Future<void> _chargement;
@override
void initState() {
super.initState();
_chargement = SessionClient.instance.charger();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: _chargement,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return AnimatedBuilder(
animation: SessionClient.instance,
builder: (context, _) {
final aProfil = SessionClient.instance.monClient != null;
return aProfil
? const HomeClientScreen()
: const CompleteProfileScreen();
},
);
},
);
}
}
class _EcranNonConfigure extends StatelessWidget {
const _EcranNonConfigure();
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.cloud_off, size: 56, color: Colors.grey),
SizedBox(height: 16),
Text('Supabase non configuré',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
SizedBox(height: 8),
Text(
'Renseignez l\'URL et la clé anon dans lib/supabase_config.dart.',
textAlign: TextAlign.center,
style: TextStyle(color: Color(0xFF6B6B72)),
),
],
),
),
),
);
}
}
+134
View File
@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import '../services/session_client.dart';
import '../supabase_config.dart';
import '../theme.dart';
/// Affiché quand le compte est connecté mais n'a pas encore de fiche fidélité
/// (ex : après une inscription avec confirmation d'email). On crée le profil.
class CompleteProfileScreen extends StatefulWidget {
const CompleteProfileScreen({super.key});
@override
State<CompleteProfileScreen> createState() => _CompleteProfileScreenState();
}
class _CompleteProfileScreenState extends State<CompleteProfileScreen> {
final _formKey = GlobalKey<FormState>();
final _nomCtrl = TextEditingController();
final _prenomCtrl = TextEditingController();
final _telCtrl = TextEditingController();
bool _enCours = false;
String? _erreur;
@override
void dispose() {
_nomCtrl.dispose();
_prenomCtrl.dispose();
_telCtrl.dispose();
super.dispose();
}
Future<void> _valider() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_enCours = true;
_erreur = null;
});
try {
await SessionClient.instance.creerProfil(
nom: _nomCtrl.text,
prenom: _prenomCtrl.text,
telephone: _telCtrl.text,
);
// creerProfil recharge la session → l'AuthGate affiche l'accueil.
} catch (e) {
if (mounted) {
setState(() {
_erreur = 'Erreur : $e';
_enCours = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Finaliser mon profil'),
actions: [
IconButton(
tooltip: 'Se déconnecter',
icon: const Icon(Icons.logout),
onPressed: () => supabase.auth.signOut(),
),
],
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Encore une étape pour activer votre carte de fidélité.',
style: TextStyle(color: AppTheme.grisTexte),
),
const SizedBox(height: 20),
TextFormField(
controller: _nomCtrl,
textCapitalization: TextCapitalization.words,
autofocus: true,
decoration: const InputDecoration(
labelText: 'Nom',
prefixIcon: Icon(Icons.person_outline),
),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Nom obligatoire' : null,
),
const SizedBox(height: 14),
TextFormField(
controller: _prenomCtrl,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
labelText: 'Prénom',
prefixIcon: Icon(Icons.badge_outlined),
),
),
const SizedBox(height: 14),
TextFormField(
controller: _telCtrl,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Téléphone (facultatif)',
prefixIcon: Icon(Icons.phone_outlined),
),
),
if (_erreur != null) ...[
const SizedBox(height: 16),
Text(_erreur!,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.red.shade600)),
],
const SizedBox(height: 24),
FilledButton(
onPressed: _enCours ? null : _valider,
child: _enCours
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: const Text('Activer ma carte'),
),
],
),
),
),
),
);
}
}
+309
View File
@@ -0,0 +1,309 @@
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../models/mouvement.dart';
import '../models/recompense.dart';
import '../services/session_client.dart';
import '../supabase_config.dart';
import '../theme.dart';
import '../utils/format.dart';
/// Écran principal du client : sa carte de fidélité (points + QR), les
/// récompenses qu'il peut viser, et son historique.
class HomeClientScreen extends StatefulWidget {
const HomeClientScreen({super.key});
@override
State<HomeClientScreen> createState() => _HomeClientScreenState();
}
class _HomeClientScreenState extends State<HomeClientScreen>
with WidgetsBindingObserver {
final _session = SessionClient.instance;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Rafraîchit le solde au retour dans l'app (après un passage en caisse).
if (state == AppLifecycleState.resumed) _session.charger();
}
Future<void> _deconnexion() async {
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Se déconnecter ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Annuler'),
),
FilledButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Déconnexion'),
),
],
),
);
if (ok == true) {
_session.vider();
await supabase.auth.signOut();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_session.nomMagasin.isEmpty
? 'Ma fidélité'
: _session.nomMagasin),
actions: [
IconButton(
tooltip: 'Rafraîchir',
icon: const Icon(Icons.refresh),
onPressed: () => _session.charger(),
),
IconButton(
tooltip: 'Se déconnecter',
icon: const Icon(Icons.logout),
onPressed: _deconnexion,
),
],
),
body: AnimatedBuilder(
animation: _session,
builder: (context, _) {
final client = _session.monClient;
if (client == null) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: () => _session.charger(),
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
children: [
_carteFidelite(client.nomComplet, client.code, client.points),
const SizedBox(height: 24),
if (_session.recompenses.isNotEmpty) ...[
_titre('Mes récompenses'),
const SizedBox(height: 8),
..._session.recompenses.map(
(r) => _ligneRecompense(r, client.points),
),
const SizedBox(height: 24),
],
_titre('Historique'),
const SizedBox(height: 8),
_historique(),
],
),
);
},
),
);
}
Widget _carteFidelite(String nom, String code, double pts) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20),
child: Column(
children: [
Text(nom,
style:
const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
const Icon(Icons.stars_rounded,
color: AppTheme.accent, size: 34),
const SizedBox(width: 8),
Text(
pointsNombre(pts),
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.w800,
color: AppTheme.accent),
),
const SizedBox(width: 6),
const Padding(
padding: EdgeInsets.only(bottom: 8),
child: Text('pts',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.accent)),
),
],
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE0E0E4)),
),
child: QrImageView(
data: code,
version: QrVersions.auto,
size: 200,
eyeStyle: const QrEyeStyle(
eyeShape: QrEyeShape.square,
color: AppTheme.noir,
),
dataModuleStyle: const QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: AppTheme.noir,
),
),
),
const SizedBox(height: 12),
Text(code,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: 2)),
const SizedBox(height: 6),
const Text(
'Présentez ce code en caisse pour cumuler vos points.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12, color: AppTheme.grisTexte),
),
],
),
),
);
}
Widget _ligneRecompense(Recompense r, double solde) {
final atteignable = solde >= r.coutPoints;
final progression =
r.coutPoints <= 0 ? 1.0 : (solde / r.coutPoints).clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
CircleAvatar(
backgroundColor: atteignable
? AppTheme.accent.withValues(alpha: 0.14)
: Colors.grey.withValues(alpha: 0.12),
child: Icon(Icons.card_giftcard,
color: atteignable ? AppTheme.accent : Colors.grey),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(r.nom,
style: const TextStyle(fontWeight: FontWeight.w700)),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: progression,
minHeight: 6,
backgroundColor: Colors.grey.withValues(alpha: 0.18),
color: AppTheme.accent,
),
),
const SizedBox(height: 6),
Text(
atteignable
? 'Disponible ! (${points(r.coutPoints)})'
: 'Encore ${points(r.coutPoints - solde)} • coût ${points(r.coutPoints)}',
style: TextStyle(
fontSize: 12,
color: atteignable
? AppTheme.accent
: AppTheme.grisTexte,
fontWeight:
atteignable ? FontWeight.w700 : FontWeight.w500,
),
),
],
),
),
],
),
),
),
);
}
Widget _historique() {
final mvts = _session.mouvements;
if (mvts.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: Text('Aucun mouvement pour l\'instant.',
style: TextStyle(color: AppTheme.grisTexte)),
),
);
}
return Column(children: mvts.map(_ligneMouvement).toList());
}
Widget _ligneMouvement(Mouvement m) {
final gain = m.estGain;
final couleur = gain ? AppTheme.accent : Colors.red.shade600;
final icone = switch (m.type) {
TypeMouvement.facture => Icons.receipt_long,
TypeMouvement.recompense => Icons.card_giftcard,
_ => Icons.tune,
};
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: couleur.withValues(alpha: 0.12),
child: Icon(icone, size: 20, color: couleur),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(m.libelle,
style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 2),
Text(dateHeure(m.date),
style: const TextStyle(
fontSize: 12, color: AppTheme.grisTexte)),
],
),
),
Text(
'${gain ? '+' : ''}${points(m.points.abs())}',
style: TextStyle(fontWeight: FontWeight.w700, color: couleur),
),
],
),
);
}
Widget _titre(String texte) => Text(texte,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700));
}
+116
View File
@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../supabase_config.dart';
/// Connexion d'un client existant (email + mot de passe).
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailCtrl = TextEditingController();
final _mdpCtrl = TextEditingController();
bool _enCours = false;
bool _voirMdp = false;
String? _erreur;
@override
void dispose() {
_emailCtrl.dispose();
_mdpCtrl.dispose();
super.dispose();
}
Future<void> _connexion() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_enCours = true;
_erreur = null;
});
try {
await supabase.auth.signInWithPassword(
email: _emailCtrl.text.trim(),
password: _mdpCtrl.text,
);
// L'AuthGate prend le relais automatiquement (pop de cet écran).
if (mounted) Navigator.of(context).pop();
} on AuthException catch (e) {
if (mounted) setState(() => _erreur = e.message);
} catch (e) {
if (mounted) setState(() => _erreur = 'Erreur : $e');
} finally {
if (mounted) setState(() => _enCours = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Connexion')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 8),
TextFormField(
controller: _emailCtrl,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.mail_outline),
),
validator: (v) =>
(v == null || !v.contains('@')) ? 'Email invalide' : null,
),
const SizedBox(height: 14),
TextFormField(
controller: _mdpCtrl,
obscureText: !_voirMdp,
decoration: InputDecoration(
labelText: 'Mot de passe',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_voirMdp ? Icons.visibility_off : Icons.visibility),
onPressed: () => setState(() => _voirMdp = !_voirMdp),
),
),
validator: (v) =>
(v == null || v.isEmpty) ? 'Mot de passe requis' : null,
onFieldSubmitted: (_) => _connexion(),
),
if (_erreur != null) ...[
const SizedBox(height: 16),
Text(_erreur!,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.red.shade600)),
],
const SizedBox(height: 24),
FilledButton(
onPressed: _enCours ? null : _connexion,
child: _enCours
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: const Text('Se connecter'),
),
],
),
),
),
),
);
}
}
+192
View File
@@ -0,0 +1,192 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../services/session_client.dart';
import '../supabase_config.dart';
import '../theme.dart';
/// Création d'un compte client : email + mot de passe + profil (nom, téléphone).
/// Le code de fidélité et le QR sont générés automatiquement côté serveur.
class SignupScreen extends StatefulWidget {
const SignupScreen({super.key});
@override
State<SignupScreen> createState() => _SignupScreenState();
}
class _SignupScreenState extends State<SignupScreen> {
final _formKey = GlobalKey<FormState>();
final _nomCtrl = TextEditingController();
final _prenomCtrl = TextEditingController();
final _telCtrl = TextEditingController();
final _emailCtrl = TextEditingController();
final _mdpCtrl = TextEditingController();
bool _enCours = false;
bool _voirMdp = false;
String? _erreur;
@override
void dispose() {
_nomCtrl.dispose();
_prenomCtrl.dispose();
_telCtrl.dispose();
_emailCtrl.dispose();
_mdpCtrl.dispose();
super.dispose();
}
Future<void> _inscription() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_enCours = true;
_erreur = null;
});
try {
final res = await supabase.auth.signUp(
email: _emailCtrl.text.trim(),
password: _mdpCtrl.text,
);
if (res.session != null) {
// Connecté directement (confirmation d'email désactivée) → on crée le
// profil fidélité. L'AuthGate basculera ensuite vers l'accueil.
await SessionClient.instance.creerProfil(
nom: _nomCtrl.text,
prenom: _prenomCtrl.text,
telephone: _telCtrl.text,
);
if (mounted) Navigator.of(context).pop();
} else {
// Confirmation d'email requise : le profil sera finalisé à la 1re
// connexion (après validation du mail).
if (mounted) await _popupVerifierEmail();
}
} on AuthException catch (e) {
if (mounted) setState(() => _erreur = e.message);
} catch (e) {
if (mounted) setState(() => _erreur = 'Erreur : $e');
} finally {
if (mounted) setState(() => _enCours = false);
}
}
Future<void> _popupVerifierEmail() async {
await showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Vérifiez votre email'),
content: const Text(
'Un email de confirmation vous a été envoyé. Validez-le puis '
'connectez-vous pour finaliser votre carte de fidélité.'),
actions: [
FilledButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('OK'),
),
],
),
);
if (mounted) Navigator.of(context).pop(); // retour à l'accueil
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Créer un compte')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _nomCtrl,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
labelText: 'Nom',
prefixIcon: Icon(Icons.person_outline),
),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Nom obligatoire' : null,
),
const SizedBox(height: 14),
TextFormField(
controller: _prenomCtrl,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
labelText: 'Prénom',
prefixIcon: Icon(Icons.badge_outlined),
),
),
const SizedBox(height: 14),
TextFormField(
controller: _telCtrl,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Téléphone (facultatif)',
prefixIcon: Icon(Icons.phone_outlined),
),
),
const SizedBox(height: 14),
TextFormField(
controller: _emailCtrl,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.mail_outline),
),
validator: (v) =>
(v == null || !v.contains('@')) ? 'Email invalide' : null,
),
const SizedBox(height: 14),
TextFormField(
controller: _mdpCtrl,
obscureText: !_voirMdp,
decoration: InputDecoration(
labelText: 'Mot de passe',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_voirMdp ? Icons.visibility_off : Icons.visibility),
onPressed: () => setState(() => _voirMdp = !_voirMdp),
),
),
validator: (v) => (v == null || v.length < 6)
? '6 caractères minimum'
: null,
onFieldSubmitted: (_) => _inscription(),
),
if (_erreur != null) ...[
const SizedBox(height: 16),
Text(_erreur!,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.red.shade600)),
],
const SizedBox(height: 24),
FilledButton(
onPressed: _enCours ? null : _inscription,
child: _enCours
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: const Text('Créer mon compte'),
),
const SizedBox(height: 12),
const Text(
'Votre QR code de fidélité sera généré automatiquement.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: AppTheme.grisTexte),
),
],
),
),
),
),
);
}
}
+67
View File
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import '../theme.dart';
import 'login_screen.dart';
import 'signup_screen.dart';
/// Premier écran : le client choisit de se connecter ou de créer un compte.
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(28),
child: Column(
children: [
const Spacer(flex: 2),
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppTheme.accent.withValues(alpha: 0.12),
shape: BoxShape.circle,
),
child: const Icon(Icons.card_membership,
size: 56, color: AppTheme.accent),
),
const SizedBox(height: 28),
const Text(
'Votre carte de fidélité',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 26, fontWeight: FontWeight.w800),
),
const SizedBox(height: 10),
const Text(
'Cumulez des points à chaque achat et profitez de récompenses.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15, color: AppTheme.grisTexte),
),
const Spacer(flex: 3),
FilledButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SignupScreen()),
),
child: const Text('Créer un compte'),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const LoginScreen()),
),
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
),
child: const Text('J\'ai déjà un compte'),
),
const SizedBox(height: 12),
],
),
),
),
);
}
}
+117
View File
@@ -0,0 +1,117 @@
import 'package:flutter/foundation.dart';
import '../models/client.dart';
import '../models/mouvement.dart';
import '../models/recompense.dart';
import '../supabase_config.dart';
/// Données du client connecté (son compte, ses points, son historique) + le
/// contexte magasin (nom, récompenses proposées). Source de vérité côté client.
class SessionClient extends ChangeNotifier {
SessionClient._();
static final SessionClient instance = SessionClient._();
Client? _monClient;
List<Mouvement> _mouvements = [];
List<Recompense> _recompenses = [];
String _nomMagasin = '';
double _eurosParPoint = 0;
bool _enCours = false;
String? _erreur;
Client? get monClient => _monClient;
List<Mouvement> get mouvements => List.unmodifiable(_mouvements);
List<Recompense> get recompenses => List.unmodifiable(_recompenses);
String get nomMagasin => _nomMagasin;
double get eurosParPoint => _eurosParPoint;
bool get enCours => _enCours;
String? get erreur => _erreur;
/// Charge toutes les données du client connecté. [_monClient] reste null si
/// le compte n'a pas encore de profil fidélité (→ écran « finaliser profil »).
Future<void> charger() async {
_enCours = true;
_erreur = null;
notifyListeners();
try {
final uid = supabase.auth.currentUser?.id;
if (uid == null) {
_monClient = null;
return;
}
final row = await supabase
.from('clients')
.select()
.eq('user_id', uid)
.maybeSingle();
_monClient = row == null ? null : Client.fromMap(row);
// Contexte magasin (lisible par tout compte connecté).
final reglages = await supabase
.from('reglages')
.select('euros_par_point, nom_magasin')
.eq('id', 1)
.maybeSingle();
if (reglages != null) {
_eurosParPoint =
(reglages['euros_par_point'] as num?)?.toDouble() ?? 0;
_nomMagasin = (reglages['nom_magasin'] as String?) ?? '';
}
final recs = await supabase
.from('recompenses')
.select()
.eq('actif', true)
.order('cout_points');
_recompenses =
(recs as List).map((e) => Recompense.fromMap(e)).toList();
if (_monClient != null) {
final mvts = await supabase
.from('mouvements')
.select()
.eq('client_id', _monClient!.id)
.order('created_at', ascending: false);
_mouvements =
(mvts as List).map((e) => Mouvement.fromMap(e)).toList();
} else {
_mouvements = [];
}
} catch (e) {
_erreur = e.toString();
} finally {
_enCours = false;
notifyListeners();
}
}
/// Crée le profil fidélité du compte connecté (nom + téléphone). Le code et le
/// QR sont générés côté base. Utilisé à l'inscription / finalisation de profil.
Future<void> creerProfil({
required String nom,
String? prenom,
String? telephone,
}) async {
final uid = supabase.auth.currentUser?.id;
if (uid == null) throw Exception('Non connecté');
String? ouNull(String? v) =>
(v == null || v.trim().isEmpty) ? null : v.trim();
await supabase.from('clients').insert({
'user_id': uid,
'nom': nom.trim(),
'prenom': ouNull(prenom),
'telephone': ouNull(telephone),
});
await charger();
}
void vider() {
_monClient = null;
_mouvements = [];
_recompenses = [];
_nomMagasin = '';
_eurosParPoint = 0;
notifyListeners();
}
}
+19
View File
@@ -0,0 +1,19 @@
import 'package:supabase_flutter/supabase_flutter.dart';
/// Configuration de connexion au backend Supabase (LE MÊME que l'app magasin).
///
/// ⚠️ Renseigner les valeurs de VOTRE projet Supabase :
/// Project Settings → API → « Project URL » et « anon public ».
/// Voir le guide : ../app_fideliter/supabase/SETUP.md
class SupabaseConfig {
SupabaseConfig._();
static const String url = 'https://zfhcnzbggpdvgvosrkgp.supabase.co';
static const String anonKey = 'sb_publishable_4R3-_q1Zy_usliTff01Ilg_jhFlIdPH';
static bool get estConfigure =>
!url.contains('VOTRE-PROJET') && !anonKey.contains('VOTRE_CLE');
}
/// Raccourci vers le client Supabase (une fois [Supabase.initialize] appelé).
SupabaseClient get supabase => Supabase.instance.client;
+112
View File
@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
/// Thème identique à l'app magasin (même marque de fidélité) : noir & blanc
/// épuré + accent vert émeraude.
class AppTheme {
static const Color accent = Color(0xFF12805C); // vert émeraude
static const Color noir = Color(0xFF111114);
static const Color grisTexte = Color(0xFF6B6B72);
static ThemeData clair() {
final scheme = ColorScheme.fromSeed(
seedColor: accent,
brightness: Brightness.light,
).copyWith(
primary: noir,
onPrimary: Colors.white,
secondary: accent,
surface: Colors.white,
onSurface: noir,
);
return _base(scheme, const Color(0xFFF6F6F7));
}
static ThemeData sombre() {
final scheme = ColorScheme.fromSeed(
seedColor: accent,
brightness: Brightness.dark,
).copyWith(
primary: Colors.white,
onPrimary: noir,
secondary: accent,
);
return _base(scheme, const Color(0xFF0E0E11));
}
static ThemeData _base(ColorScheme scheme, Color fond) {
final base = ThemeData(
useMaterial3: true,
colorScheme: scheme,
scaffoldBackgroundColor: fond,
fontFamily: 'Roboto',
);
return base.copyWith(
appBarTheme: AppBarTheme(
backgroundColor: fond,
foregroundColor: scheme.onSurface,
elevation: 0,
centerTitle: false,
titleTextStyle: TextStyle(
color: scheme.onSurface,
fontSize: 24,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
),
),
cardTheme: CardThemeData(
elevation: 0,
color: scheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: scheme.outlineVariant.withValues(alpha: 0.5)),
),
margin: EdgeInsets.zero,
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary,
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: scheme.surface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: scheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: scheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: accent, width: 2),
),
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: scheme.surface,
indicatorColor: accent.withValues(alpha: 0.14),
elevation: 0,
labelTextStyle: WidgetStateProperty.resolveWith((states) {
final selected = states.contains(WidgetState.selected);
return TextStyle(
fontSize: 12,
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
color: selected ? scheme.onSurface : grisTexte,
);
}),
iconTheme: WidgetStateProperty.resolveWith((states) {
final selected = states.contains(WidgetState.selected);
return IconThemeData(color: selected ? accent : grisTexte);
}),
),
);
}
}
+28
View File
@@ -0,0 +1,28 @@
import 'package:intl/intl.dart';
final NumberFormat _euro = NumberFormat.currency(locale: 'fr_FR', symbol: '');
final DateFormat _dateHeure = DateFormat('d MMM y • HH:mm', 'fr_FR');
String euro(double v) => _euro.format(v);
/// Formate un nombre de points de façon lisible :
/// entier sans décimale (« 12 pts »), sinon jusqu'à 2 décimales sans zéros
/// inutiles (« 0,48 pt », « 2,5 pts »). Gère le singulier/pluriel.
String points(double v) {
final arrondi = (v * 100).round() / 100;
var s = arrondi == arrondi.roundToDouble()
? arrondi.toInt().toString()
: arrondi
.toStringAsFixed(2)
.replaceAll(RegExp(r'0+$'), '')
.replaceAll(RegExp(r'\.$'), '');
s = s.replaceAll('.', ',');
final pluriel = arrondi.abs() >= 2 ? 'pts' : 'pt';
return '$s $pluriel';
}
/// Comme [points] mais sans le suffixe « pt/pts » (pour les gros affichages).
String pointsNombre(double v) =>
points(v).replaceAll(RegExp(r'\s?pts?$'), '');
String dateHeure(DateTime d) => _dateHeure.format(d);
+746
View File
@@ -0,0 +1,746 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
app_links:
dependency: transitive
description:
name: app_links
sha256: "9d3c82f634c7f5b5c752f7ee46b67724246043f5e1d5fc1b433dd5b38d780dbe"
url: "https://pub.dev"
source: hosted
version: "7.2.0"
app_links_linux:
dependency: transitive
description:
name: app_links_linux
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
url: "https://pub.dev"
source: hosted
version: "1.0.3"
app_links_platform_interface:
dependency: transitive
description:
name: app_links_platform_interface
sha256: "78a18580eecac98108d1eef52a7db668bc317714f5205e616973363326efe333"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
app_links_web:
dependency: transitive
description:
name: app_links_web
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
url: "https://pub.dev"
source: hosted
version: "1.0.4"
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
dart_jsonwebtoken:
dependency: transitive
description:
name: dart_jsonwebtoken
sha256: ad84e60181696513d04d5f2078e0bbc20365b911f46f647797317414bdc88fbe
url: "https://pub.dev"
source: hosted
version: "3.4.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
functions_client:
dependency: transitive
description:
name: functions_client
sha256: "5b17b8dcf5ae1cd6a6428e7a4ca8b569477297a7f158955b5407dda23baeaa5b"
url: "https://pub.dev"
source: hosted
version: "2.6.3"
gotrue:
dependency: transitive
description:
name: gotrue
sha256: "54e8abe49c8596234c503f4b64d07022c7913a6475c7a85c1f1eacc1a93af166"
url: "https://pub.dev"
source: hosted
version: "2.24.0"
gtk:
dependency: transitive
description:
name: gtk
sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
url: "https://pub.dev"
source: hosted
version: "4.9.1"
intl:
dependency: "direct main"
description:
name: intl
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
url: "https://pub.dev"
source: hosted
version: "0.20.3"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
jwt_decode:
dependency: transitive
description:
name: jwt_decode
sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb
url: "https://pub.dev"
source: hosted
version: "0.3.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
passkeys_platform_interface:
dependency: transitive
description:
name: passkeys_platform_interface
sha256: "9610bd136b3382500390912ddd8517ee99505228b1af7b507b9c907f7e99a47d"
url: "https://pub.dev"
source: hosted
version: "2.8.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
postgrest:
dependency: transitive
description:
name: postgrest
sha256: "801f6659cffbb6372d38cb4deb0cd892cb0cb38d75e0686ff24f4a724e9f4999"
url: "https://pub.dev"
source: hosted
version: "2.7.4"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
realtime_client:
dependency: transitive
description:
name: realtime_client
sha256: "3261ba98af41fccfdbb6c66299b5bd6871907e98bb41bce9d55e0fb91287ed85"
url: "https://pub.dev"
source: hosted
version: "2.9.1"
retry:
dependency: transitive
description:
name: retry
sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5"
url: "https://pub.dev"
source: hosted
version: "2.4.26"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
storage_client:
dependency: transitive
description:
name: storage_client
sha256: "2cc5432c312c4a08687bcce795aff377d006e2892097c7f88d57512885bb4b10"
url: "https://pub.dev"
source: hosted
version: "2.5.9"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
supabase:
dependency: transitive
description:
name: supabase
sha256: d425df9330ce5f215a0ee708b084899d89031e8a8f3e096f46c38ea37df9fd7d
url: "https://pub.dev"
source: hosted
version: "2.13.3"
supabase_flutter:
dependency: "direct main"
description:
name: supabase_flutter
sha256: "262c7eda45997a308ac0311be82fe8e9134526a18125fe5e0cf0a58e9cd16cb6"
url: "https://pub.dev"
source: hosted
version: "2.15.3"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
dependency: transitive
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
url: "https://pub.dev"
source: hosted
version: "6.3.32"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
yet_another_json_isolate:
dependency: transitive
description:
name: yet_another_json_isolate
sha256: eaa26beb5990b25a49d942374fd5a0c5aa67a837e03b14b4c26134aaa1ed01a9
url: "https://pub.dev"
source: hosted
version: "2.1.1"
sdks:
dart: ">=3.12.2 <4.0.0"
flutter: ">=3.44.0"
+102
View File
@@ -0,0 +1,102 @@
name: app_fideliter_client
description: "App client du programme de fidélité — compte, points, QR, récompenses."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# 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.0+1
environment:
sdk: ^3.12.2
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
supabase_flutter: ^2.8.0 # backend : auth client + données de fidélité
qr_flutter: ^4.1.0 # affichage du QR code de fidélité du client
intl: ^0.20.3 # formatage € et dates (locale fr_FR)
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.14.4
# Icône de l'app (générée par : dart run flutter_launcher_icons)
flutter_launcher_icons:
android: true
ios: false
image_path: "assets/icon/icon.png"
adaptive_icon_background: "#0E7A55"
adaptive_icon_foreground: "assets/icon/icon_foreground.png"
min_sdk_android: 21
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
+15
View File
@@ -0,0 +1,15 @@
// Test du formatage des points (cœur de l'affichage côté client).
import 'package:flutter_test/flutter_test.dart';
import 'package:app_fideliter_client/utils/format.dart';
void main() {
test('format des points', () {
expect(points(0), '0 pt');
expect(points(1), '1 pt');
expect(points(12), '12 pts');
expect(points(0.48), '0,48 pt');
expect(points(2.5), '2,5 pts');
});
}