(Feat) Add google

This commit is contained in:
2026-07-18 13:11:03 +02:00
parent 01a0f1029f
commit 8dd5873f25
20 changed files with 596 additions and 379 deletions
+31 -6
View File
@@ -1,11 +1,22 @@
import java.util.Properties
import java.io.FileInputStream
plugins { plugins {
id("com.android.application") id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
// Charge les identifiants de la clé de signature release depuis key.properties
// (fichier hors versionnement git). Absent → build debug-signé possible.
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android { android {
namespace = "com.magasin.app_fideliter_client" namespace = "com.letreveriennais.fidelite"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
@@ -15,8 +26,7 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.letreveriennais.fidelite"
applicationId = "com.magasin.app_fideliter_client"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
@@ -25,11 +35,26 @@ android {
versionName = flutter.versionName versionName = flutter.versionName
} }
signingConfigs {
create("release") {
if (keystorePropertiesFile.exists()) {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
}
}
}
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. // Signature avec la clé release (upload-keystore.jks) si key.properties
// Signing with the debug keys for now, so `flutter run --release` works. // est présent, sinon repli sur la clé debug pour un build local.
signingConfig = signingConfigs.getByName("debug") signingConfig = if (keystorePropertiesFile.exists()) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
} }
} }
} }
+7 -2
View File
@@ -1,9 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Connexion au backend Supabase. --> <!-- Connexion au backend PocketBase. -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:label="Ma fidélité" android:label="Le Trévériennais"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
@@ -44,5 +44,10 @@
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain"/>
</intent> </intent>
<!-- Ouverture du navigateur pour l'authentification OAuth (url_launcher). -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries> </queries>
</manifest> </manifest>
@@ -1,4 +1,4 @@
package com.magasin.app_fideliter_client package com.letreveriennais.fidelite
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
+65
View File
@@ -0,0 +1,65 @@
# Activer « Continuer avec Google »
L'app est déjà codée pour afficher le bouton Google **automatiquement** dès que le
fournisseur est activé côté PocketBase. Il reste uniquement à créer les
identifiants OAuth chez Google (étapes 1→4) puis à les brancher (étape 5).
PocketBase utilise le **flux web** : Google redirige vers PocketBase, pas
directement vers l'app. On crée donc un client OAuth de type **« Application Web »**
(pas « Android ») — inutile de fournir le nom de package ou l'empreinte SHA-1.
---
## 1. Créer un projet Google Cloud
1. Aller sur https://console.cloud.google.com/
2. En haut, sélecteur de projet → **Nouveau projet** (ex. « Fideliter »).
## 2. Configurer l'écran de consentement OAuth
1. Menu → **API et services → Écran de consentement OAuth**.
2. Type d'utilisateur : **Externe** → Créer.
3. Renseigner : nom de l'app (« Ma fidélité »), email d'assistance, logo (facultatif),
email du développeur.
4. **Domaines autorisés** : ajouter `tailb756e1.ts.net`.
5. Étape « Champs d'application » (scopes) : ajouter `.../auth/userinfo.email`,
`.../auth/userinfo.profile` et `openid`. Ce sont des scopes **non sensibles**
→ pas de longue procédure de validation Google.
6. Pour la mise en production (Play Store) : renseigner une **URL de politique de
confidentialité** (aussi obligatoire pour le Play Store), puis passer l'app
« En production » (bouton **Publier l'application**). Sinon, tant qu'elle est en
« Test », seuls les comptes ajoutés en « utilisateurs de test » peuvent se connecter.
## 3. Créer l'identifiant OAuth
1. Menu → **API et services → Identifiants**.
2. **Créer des identifiants → ID client OAuth**.
3. Type d'application : **Application Web**.
4. Nom : « PocketBase Fideliter » (peu importe).
5. **URI de redirection autorisés** → Ajouter EXACTEMENT :
```
https://db.tailb756e1.ts.net/api/oauth2-redirect
```
6. Créer → Google affiche le **Client ID** et le **Client Secret**. Les copier.
## 4. (Optionnel) URI de redirection supplémentaire
Rien d'autre à ajouter pour le flux actuel.
## 5. Brancher dans PocketBase
Deux options :
**A. Tu me donnes le Client ID + Client Secret** → je les injecte dans PocketBase
(collection `users` → OAuth2 → Google) et le bouton apparaît dans l'app.
**B. Tu le fais toi-même** dans l'admin PocketBase :
1. https://db.tailb756e1.ts.net/_/ → connexion superuser.
2. Collections → `users` → onglet **Options****OAuth2** → activer → **+ Add provider → Google**.
3. Coller Client ID + Client Secret → Enregistrer.
Dès que c'est fait, le bouton « Continuer avec Google » s'affiche tout seul sur
l'écran d'accueil (l'app lit la liste des fournisseurs activés au démarrage).
---
## Notes Play Store
- Un compte créé via Google arrive sur l'écran **« Finaliser mon profil »** (nom +
téléphone) puis obtient son code de fidélité, comme une inscription classique.
- Le consentement OAuth en production exige une **politique de confidentialité**
de toute façon requise par le Play Store. Prévois une page web (même simple).
+4 -8
View File
@@ -1,9 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'pocketbase_config.dart';
import 'screens/auth_gate.dart'; import 'screens/auth_gate.dart';
import 'supabase_config.dart';
import 'theme.dart'; import 'theme.dart';
Future<void> main() async { Future<void> main() async {
@@ -11,11 +10,8 @@ Future<void> main() async {
await initializeDateFormatting('fr_FR', null); await initializeDateFormatting('fr_FR', null);
if (SupabaseConfig.estConfigure) { if (PbConfig.estConfigure) {
await Supabase.initialize( await initPocketBase();
url: SupabaseConfig.url,
publishableKey: SupabaseConfig.anonKey,
);
} }
runApp(const MonApp()); runApp(const MonApp());
@@ -27,7 +23,7 @@ class MonApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Ma fidélité', title: 'Le Trévériennais',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: AppTheme.clair(), theme: AppTheme.clair(),
darkTheme: AppTheme.sombre(), darkTheme: AppTheme.sombre(),
+1 -1
View File
@@ -1,6 +1,6 @@
/// Le compte fidélité du client connecté. Le [code] est encodé dans son QR. /// Le compte fidélité du client connecté. Le [code] est encodé dans son QR.
class Client { class Client {
final String id; // uuid Supabase final String id; // id PocketBase
final String code; // ex : « FID-3F9K2A » final String code; // ex : « FID-3F9K2A »
final String nom; final String nom;
final String? prenom; final String? prenom;
+1 -1
View File
@@ -32,7 +32,7 @@ class Mouvement {
montantEuros: (map['montant_euros'] as num?)?.toDouble(), montantEuros: (map['montant_euros'] as num?)?.toDouble(),
points: (map['points'] as num?)?.toDouble() ?? 0, points: (map['points'] as num?)?.toDouble() ?? 0,
libelle: (map['libelle'] as String?) ?? '', libelle: (map['libelle'] as String?) ?? '',
date: DateTime.tryParse(map['created_at']?.toString() ?? '')?.toLocal() ?? date: DateTime.tryParse(map['created']?.toString() ?? '')?.toLocal() ??
DateTime.fromMillisecondsSinceEpoch(0), DateTime.fromMillisecondsSinceEpoch(0),
); );
} }
+43
View File
@@ -0,0 +1,43 @@
import 'package:pocketbase/pocketbase.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Configuration de connexion au backend PocketBase (LE MÊME que l'app magasin).
///
/// L'URL pointe vers l'instance PocketBase auto-hébergée. La session (token
/// d'auth) est persistée localement via [SharedPreferences] pour rester
/// connecté entre deux lancements de l'app.
class PbConfig {
PbConfig._();
static const String url = 'https://db.tailb756e1.ts.net';
static bool get estConfigure => !url.contains('VOTRE-URL');
}
/// ID client OAuth « Web » du projet Google Cloud. Passé à google_sign_in comme
/// `serverClientId` → l'ID token renvoyé a cette audience, que le hook serveur
/// PocketBase (`/api/google-native-auth`) vérifie.
const String googleWebClientId =
'831733439319-45cergb7cf8r728fja4m0kcvu2r7fbj9.apps.googleusercontent.com';
/// Client PocketBase global (initialisé une fois via [initPocketBase]).
late final PocketBase pb;
/// Initialise le client PocketBase avec une auth store persistée sur disque.
/// À appeler au démarrage, avant `runApp`.
Future<void> initPocketBase() async {
final prefs = await SharedPreferences.getInstance();
final store = AsyncAuthStore(
save: (String data) async => prefs.setString('pb_auth', data),
clear: () async => prefs.remove('pb_auth'),
initial: prefs.getString('pb_auth'),
);
pb = PocketBase(PbConfig.url, authStore: store);
}
/// Extrait un message lisible d'une erreur PocketBase (sinon [parDefaut]).
String messageErreurPb(ClientException e, String parDefaut) {
final data = e.response['message'];
if (data is String && data.trim().isNotEmpty) return data;
return parDefaut;
}
+10 -10
View File
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:pocketbase/pocketbase.dart';
import '../pocketbase_config.dart';
import '../services/session_client.dart'; import '../services/session_client.dart';
import '../supabase_config.dart';
import 'complete_profile_screen.dart'; import 'complete_profile_screen.dart';
import 'home_client_screen.dart'; import 'home_client_screen.dart';
import 'welcome_screen.dart'; import 'welcome_screen.dart';
@@ -13,14 +13,14 @@ class AuthGate extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!SupabaseConfig.estConfigure) return const _EcranNonConfigure(); if (!PbConfig.estConfigure) return const _EcranNonConfigure();
return StreamBuilder<AuthState>( return StreamBuilder<AuthStoreEvent>(
stream: supabase.auth.onAuthStateChange, stream: pb.authStore.onChange,
builder: (context, snapshot) { builder: (context, snapshot) {
final session = supabase.auth.currentSession; if (!pb.authStore.isValid) return const WelcomeScreen();
if (session == null) return const WelcomeScreen(); final uid = pb.authStore.record?.id ?? '';
return _SessionChargee(key: ValueKey(session.user.id)); return _SessionChargee(key: ValueKey(uid));
}, },
); );
} }
@@ -82,11 +82,11 @@ class _EcranNonConfigure extends StatelessWidget {
children: [ children: [
Icon(Icons.cloud_off, size: 56, color: Colors.grey), Icon(Icons.cloud_off, size: 56, color: Colors.grey),
SizedBox(height: 16), SizedBox(height: 16),
Text('Supabase non configuré', Text('Backend non configuré',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
SizedBox(height: 8), SizedBox(height: 8),
Text( Text(
'Renseignez l\'URL et la clé anon dans lib/supabase_config.dart.', 'Renseignez l\'URL PocketBase dans lib/pocketbase_config.dart.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: Color(0xFF6B6B72)), style: TextStyle(color: Color(0xFF6B6B72)),
), ),
+2 -2
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../pocketbase_config.dart';
import '../services/session_client.dart'; import '../services/session_client.dart';
import '../supabase_config.dart';
import '../theme.dart'; import '../theme.dart';
/// Affiché quand le compte est connecté mais n'a pas encore de fiche fidélité /// Affiché quand le compte est connecté mais n'a pas encore de fiche fidélité
@@ -61,7 +61,7 @@ class _CompleteProfileScreenState extends State<CompleteProfileScreen> {
IconButton( IconButton(
tooltip: 'Se déconnecter', tooltip: 'Se déconnecter',
icon: const Icon(Icons.logout), icon: const Icon(Icons.logout),
onPressed: () => supabase.auth.signOut(), onPressed: () => pb.authStore.clear(),
), ),
], ],
), ),
+2 -2
View File
@@ -3,8 +3,8 @@ import 'package:qr_flutter/qr_flutter.dart';
import '../models/mouvement.dart'; import '../models/mouvement.dart';
import '../models/recompense.dart'; import '../models/recompense.dart';
import '../pocketbase_config.dart';
import '../services/session_client.dart'; import '../services/session_client.dart';
import '../supabase_config.dart';
import '../theme.dart'; import '../theme.dart';
import '../utils/format.dart'; import '../utils/format.dart';
@@ -58,7 +58,7 @@ class _HomeClientScreenState extends State<HomeClientScreen>
); );
if (ok == true) { if (ok == true) {
_session.vider(); _session.vider();
await supabase.auth.signOut(); pb.authStore.clear();
} }
} }
+11 -8
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:pocketbase/pocketbase.dart';
import '../supabase_config.dart'; import '../pocketbase_config.dart';
/// Connexion d'un client existant (email + mot de passe). /// Connexion d'un client existant (email + mot de passe).
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
@@ -33,14 +33,17 @@ class _LoginScreenState extends State<LoginScreen> {
_erreur = null; _erreur = null;
}); });
try { try {
await supabase.auth.signInWithPassword( await pb.collection('users').authWithPassword(
email: _emailCtrl.text.trim(), _emailCtrl.text.trim(),
password: _mdpCtrl.text, _mdpCtrl.text,
); );
// L'AuthGate prend le relais automatiquement (pop de cet écran). // L'AuthGate prend le relais automatiquement (pop de cet écran).
if (mounted) Navigator.of(context).pop(); if (mounted) Navigator.of(context).pop();
} on AuthException catch (e) { } on ClientException catch (e) {
if (mounted) setState(() => _erreur = e.message); if (mounted) {
setState(() =>
_erreur = messageErreurPb(e, 'Email ou mot de passe incorrect.'));
}
} catch (e) { } catch (e) {
if (mounted) setState(() => _erreur = 'Erreur : $e'); if (mounted) setState(() => _erreur = 'Erreur : $e');
} finally { } finally {
+23 -40
View File
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:pocketbase/pocketbase.dart';
import '../pocketbase_config.dart';
import '../services/session_client.dart'; import '../services/session_client.dart';
import '../supabase_config.dart';
import '../theme.dart'; import '../theme.dart';
/// Création d'un compte client : email + mot de passe + profil (nom, téléphone). /// Création d'un compte client : email + mot de passe + profil (nom, téléphone).
@@ -42,27 +42,29 @@ class _SignupScreenState extends State<SignupScreen> {
_erreur = null; _erreur = null;
}); });
try { try {
final res = await supabase.auth.signUp( final email = _emailCtrl.text.trim();
email: _emailCtrl.text.trim(), // Création du compte, puis connexion immédiate (pas de confirmation
password: _mdpCtrl.text, // d'email requise). On ne transmet PAS is_staff → compte client par défaut.
); await pb.collection('users').create(body: {
'email': email,
'password': _mdpCtrl.text,
'passwordConfirm': _mdpCtrl.text,
});
await pb.collection('users').authWithPassword(email, _mdpCtrl.text);
if (res.session != null) { // Compte connecté → on crée le profil fidélité (nom + téléphone + code).
// Connecté directement (confirmation d'email désactivée) → on crée le // L'AuthGate basculera ensuite automatiquement vers l'accueil.
// profil fidélité. L'AuthGate basculera ensuite vers l'accueil. await SessionClient.instance.creerProfil(
await SessionClient.instance.creerProfil( nom: _nomCtrl.text,
nom: _nomCtrl.text, prenom: _prenomCtrl.text,
prenom: _prenomCtrl.text, telephone: _telCtrl.text,
telephone: _telCtrl.text, );
); if (mounted) Navigator.of(context).pop();
if (mounted) Navigator.of(context).pop(); } on ClientException catch (e) {
} else { if (mounted) {
// Confirmation d'email requise : le profil sera finalisé à la 1re setState(() => _erreur =
// connexion (après validation du mail). messageErreurPb(e, 'Impossible de créer le compte.'));
if (mounted) await _popupVerifierEmail();
} }
} on AuthException catch (e) {
if (mounted) setState(() => _erreur = e.message);
} catch (e) { } catch (e) {
if (mounted) setState(() => _erreur = 'Erreur : $e'); if (mounted) setState(() => _erreur = 'Erreur : $e');
} finally { } finally {
@@ -70,25 +72,6 @@ class _SignupScreenState extends State<SignupScreen> {
} }
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
+3
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme.dart'; import '../theme.dart';
import '../widgets/oauth_boutons.dart';
import 'login_screen.dart'; import 'login_screen.dart';
import 'signup_screen.dart'; import 'signup_screen.dart';
@@ -57,6 +58,8 @@ class WelcomeScreen extends StatelessWidget {
), ),
child: const Text('J\'ai déjà un compte'), child: const Text('J\'ai déjà un compte'),
), ),
const SizedBox(height: 16),
const OAuthBoutons(),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
), ),
+89 -40
View File
@@ -1,9 +1,13 @@
import 'dart:math';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:pocketbase/pocketbase.dart';
import '../models/client.dart'; import '../models/client.dart';
import '../models/mouvement.dart'; import '../models/mouvement.dart';
import '../models/recompense.dart'; import '../models/recompense.dart';
import '../supabase_config.dart'; import '../pocketbase_config.dart';
/// Données du client connecté (son compte, ses points, son historique) + le /// 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. /// contexte magasin (nom, récompenses proposées). Source de vérité côté client.
@@ -34,47 +38,46 @@ class SessionClient extends ChangeNotifier {
_erreur = null; _erreur = null;
notifyListeners(); notifyListeners();
try { try {
final uid = supabase.auth.currentUser?.id; final uid = pb.authStore.record?.id;
if (uid == null) { if (uid == null) {
_monClient = null; _monClient = null;
return; return;
} }
final row = await supabase // Fiche fidélité du compte (une par utilisateur). Absente → profil à créer.
.from('clients') try {
.select() final row = await pb
.eq('user_id', uid) .collection('clients')
.maybeSingle(); .getFirstListItem('user = "$uid"');
_monClient = row == null ? null : Client.fromMap(row); _monClient = Client.fromMap(row.toJson());
} on ClientException catch (e) {
// Contexte magasin (lisible par tout compte connecté). if (e.statusCode == 404) {
final reglages = await supabase _monClient = null;
.from('reglages') } else {
.select('euros_par_point, nom_magasin') rethrow;
.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 // Contexte magasin (lisible par tout compte connecté) : ligne unique.
.from('recompenses') final reglages = await pb.collection('reglages').getList(perPage: 1);
.select() if (reglages.items.isNotEmpty) {
.eq('actif', true) final r = reglages.items.first;
.order('cout_points'); _eurosParPoint = (r.data['euros_par_point'] as num?)?.toDouble() ?? 0;
_recompenses = _nomMagasin = (r.data['nom_magasin'] as String?) ?? '';
(recs as List).map((e) => Recompense.fromMap(e)).toList(); }
final recs = await pb.collection('recompenses').getFullList(
filter: 'actif = true',
sort: 'cout_points',
);
_recompenses = recs.map((e) => Recompense.fromMap(e.toJson())).toList();
if (_monClient != null) { if (_monClient != null) {
final mvts = await supabase final mvts = await pb.collection('mouvements').getFullList(
.from('mouvements') filter: 'client = "${_monClient!.id}"',
.select() sort: '-created',
.eq('client_id', _monClient!.id) );
.order('created_at', ascending: false); _mouvements = mvts.map((e) => Mouvement.fromMap(e.toJson())).toList();
_mouvements =
(mvts as List).map((e) => Mouvement.fromMap(e)).toList();
} else { } else {
_mouvements = []; _mouvements = [];
} }
@@ -86,26 +89,72 @@ class SessionClient extends ChangeNotifier {
} }
} }
/// Crée le profil fidélité du compte connecté (nom + téléphone). Le code et le /// Crée le profil fidélité du compte connecté (nom + téléphone). Le code de
/// QR sont générés côté base. Utilisé à l'inscription / finalisation de profil. /// fidélité (encodé dans le QR) est généré ici, côté client, car le backend
/// n'en fournit pas. Utilisé à l'inscription / finalisation de profil.
Future<void> creerProfil({ Future<void> creerProfil({
required String nom, required String nom,
String? prenom, String? prenom,
String? telephone, String? telephone,
}) async { }) async {
final uid = supabase.auth.currentUser?.id; final uid = pb.authStore.record?.id;
if (uid == null) throw Exception('Non connecté'); if (uid == null) throw Exception('Non connecté');
String? ouNull(String? v) => String? ouNull(String? v) =>
(v == null || v.trim().isEmpty) ? null : v.trim(); (v == null || v.trim().isEmpty) ? null : v.trim();
await supabase.from('clients').insert({ await pb.collection('clients').create(body: {
'user_id': uid, 'user': uid,
'code': _genererCode(),
'nom': nom.trim(), 'nom': nom.trim(),
'prenom': ouNull(prenom), if (ouNull(prenom) != null) 'prenom': ouNull(prenom),
'telephone': ouNull(telephone), if (ouNull(telephone) != null) 'telephone': ouNull(telephone),
}); });
await charger(); await charger();
} }
bool _googleInit = false;
/// Connexion / inscription native avec Google (sélecteur de compte in-app).
///
/// Récupère l'ID token Google via Credential Manager, l'envoie au hook
/// PocketBase `/api/google-native-auth` qui vérifie le token et renvoie une
/// session PocketBase. Un nouveau compte n'a pas encore de fiche fidélité →
/// l'AuthGate affichera l'écran « Finaliser mon profil ».
Future<void> connexionGoogleNative() async {
if (!_googleInit) {
await GoogleSignIn.instance
.initialize(serverClientId: googleWebClientId);
_googleInit = true;
}
final account = await GoogleSignIn.instance.authenticate();
final idToken = account.authentication.idToken;
if (idToken == null) {
throw Exception('Jeton Google introuvable');
}
final reponse = await pb.send(
'/api/google-native-auth',
method: 'POST',
body: {'idToken': idToken},
);
final map = reponse as Map<String, dynamic>;
pb.authStore.save(
map['token'] as String,
RecordModel.fromJson(map['record'] as Map<String, dynamic>),
);
}
/// Génère un code de fidélité lisible, ex. « FID-3F9K2A ».
static String _genererCode() {
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // sans I,O,0,1
final rnd = Random.secure();
final suffixe = List.generate(
6,
(_) => alphabet[rnd.nextInt(alphabet.length)],
).join();
return 'FID-$suffixe';
}
void vider() { void vider() {
_monClient = null; _monClient = null;
_mouvements = []; _mouvements = [];
-19
View File
@@ -1,19 +0,0 @@
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;
+124
View File
@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../services/session_client.dart';
import '../theme.dart';
/// Logo « G » officiel de Google (SVG des guidelines de marque). Ne pas altérer
/// (couleurs / proportions) sous peine de non-conformité (validation Play Store).
const String _googleGSvg = '''
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
</svg>
''';
/// Bouton officiel « Se connecter avec Google » (connexion native, sélecteur de
/// compte in-app), conforme aux guidelines de marque Google. Un compte inconnu
/// est créé côté serveur puis dirigé vers l'écran « Finaliser mon profil ».
class OAuthBoutons extends StatefulWidget {
const OAuthBoutons({super.key});
@override
State<OAuthBoutons> createState() => _OAuthBoutonsState();
}
class _OAuthBoutonsState extends State<OAuthBoutons> {
bool _enCours = false;
Future<void> _google() async {
setState(() => _enCours = true);
try {
await SessionClient.instance.connexionGoogleNative();
// Succès : l'AuthGate bascule automatiquement (ce widget est démonté).
} catch (e) {
if (mounted) {
setState(() => _enCours = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connexion Google annulée ou échouée.')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text('ou',
style: TextStyle(color: AppTheme.grisTexte, fontSize: 13)),
),
const Expanded(child: Divider()),
],
),
const SizedBox(height: 12),
_BoutonGoogle(enCours: _enCours, onPressed: _enCours ? null : _google),
],
);
}
}
/// Rendu conforme charte Google : fond blanc, bordure #747775, logo G 18dp,
/// libellé Roboto Medium #1F1F1F.
class _BoutonGoogle extends StatelessWidget {
const _BoutonGoogle({required this.enCours, required this.onPressed});
final bool enCours;
final VoidCallback? onPressed;
static const Color _texte = Color(0xFF1F1F1F);
static const Color _bordure = Color(0xFF747775);
@override
Widget build(BuildContext context) {
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: onPressed,
child: Ink(
height: 52,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: _bordure),
),
child: Center(
child: enCours
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.string(_googleGSvg, width: 18, height: 18),
const SizedBox(width: 12),
const Text(
'Se connecter avec Google',
style: TextStyle(
color: _texte,
fontSize: 15,
fontWeight: FontWeight.w500,
fontFamily: 'Roboto',
),
),
],
),
),
),
),
);
}
}
+70 -238
View File
@@ -1,38 +1,6 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: 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: archive:
dependency: transitive dependency: transitive
description: description:
@@ -105,22 +73,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" 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: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -129,14 +81,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.9" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -182,6 +126,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -192,30 +144,54 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
functions_client: google_identity_services_web:
dependency: transitive dependency: transitive
description: description:
name: functions_client name: google_identity_services_web
sha256: "5b17b8dcf5ae1cd6a6428e7a4ca8b569477297a7f158955b5407dda23baeaa5b" sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.6.3" version: "0.3.3+1"
gotrue: google_sign_in:
dependency: "direct main"
description:
name: google_sign_in
sha256: "521031b65853b4409b8213c0387d57edaad7e2a949ce6dea0d8b2afc9cb29763"
url: "https://pub.dev"
source: hosted
version: "7.2.0"
google_sign_in_android:
dependency: transitive dependency: transitive
description: description:
name: gotrue name: google_sign_in_android
sha256: "54e8abe49c8596234c503f4b64d07022c7913a6475c7a85c1f1eacc1a93af166" sha256: a01d4fd5a0e4eb0c1961434a0289a436abf9225b0933963c8e4e5ed88c26e40b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.24.0" version: "7.2.15"
gtk: google_sign_in_ios:
dependency: transitive dependency: transitive
description: description:
name: gtk name: google_sign_in_ios
sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" sha256: ac1e4c1205267cb7999d1d81333fccffdfda29e853f434bbaf71525498bb6950
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "6.3.0"
google_sign_in_platform_interface:
dependency: transitive
description:
name: google_sign_in_platform_interface
sha256: "7f59208c42b415a3cca203571128d6f84f885fead2d5b53eb65a9e27f2965bb5"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
google_sign_in_web:
dependency: transitive
description:
name: google_sign_in_web
sha256: d473003eeca892f96a01a64fc803378be765071cb0c265ee872c7f8683245d14
url: "https://pub.dev"
source: hosted
version: "1.1.3"
http: http:
dependency: transitive dependency: transitive
description: description:
@@ -256,14 +232,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.12.0" 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: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -296,14 +264,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -328,22 +288,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" 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: path:
dependency: transitive dependency: transitive
description: description:
@@ -352,6 +296,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -400,14 +352,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pointycastle: pocketbase:
dependency: transitive dependency: "direct main"
description: description:
name: pointycastle name: pocketbase
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" sha256: f70353ea9c583fabe4165880e054504c5036c428f59c0fc588d64bb19218df05
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "0.22.0"
posix: posix:
dependency: transitive dependency: transitive
description: description:
@@ -416,14 +368,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.5.0" version: "6.5.0"
postgrest:
dependency: transitive
description:
name: postgrest
sha256: "801f6659cffbb6372d38cb4deb0cd892cb0cb38d75e0686ff24f4a724e9f4999"
url: "https://pub.dev"
source: hosted
version: "2.7.4"
qr: qr:
dependency: transitive dependency: transitive
description: description:
@@ -440,32 +384,8 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" 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: shared_preferences:
dependency: transitive dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
@@ -541,14 +461,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.12.1" 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: stream_channel:
dependency: transitive dependency: transitive
description: description:
@@ -565,22 +477,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" 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: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -605,70 +501,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
url_launcher: vector_graphics:
dependency: transitive dependency: transitive
description: description:
name: url_launcher name: vector_graphics
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.2" version: "1.2.2"
url_launcher_android: vector_graphics_codec:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: vector_graphics_codec
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.32" version: "1.1.13"
url_launcher_ios: vector_graphics_compiler:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: vector_graphics_compiler
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.4.1" version: "1.2.6"
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: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -693,22 +549,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" 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: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@@ -733,14 +573,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" 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: sdks:
dart: ">=3.12.2 <4.0.0" dart: ">=3.12.2 <4.0.0"
flutter: ">=3.44.0" flutter: ">=3.44.0"
+4 -1
View File
@@ -34,7 +34,10 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
supabase_flutter: ^2.8.0 # backend : auth client + données de fidélité pocketbase: ^0.22.0 # backend : auth client + données de fidélité
shared_preferences: ^2.3.2 # persistance de la session PocketBase
google_sign_in: ^7.0.0 # connexion native Google (sélecteur de compte in-app)
flutter_svg: ^2.0.10 # rendu du logo G officiel (bouton Google conforme)
qr_flutter: ^4.1.0 # affichage du QR code de fidélité du client qr_flutter: ^4.1.0 # affichage du QR code de fidélité du client
intl: ^0.20.3 # formatage € et dates (locale fr_FR) intl: ^0.20.3 # formatage € et dates (locale fr_FR)
+105
View File
@@ -0,0 +1,105 @@
/// Connexion native Google (Credential Manager) pour l'app cliente.
///
/// L'app obtient un « ID token » Google via le sélecteur de compte natif, puis
/// l'envoie ici. On le vérifie auprès de Google, on retrouve (ou on crée) le
/// compte `users`, on s'assure qu'une fiche fidélité `clients` existe (créée
/// automatiquement à partir du nom/prénom Google), et on renvoie une session.
///
/// Endpoint : POST /api/google-native-auth body: { "idToken": "..." }
/// Réponse : { "token": "<jwt pocketbase>", "record": { ...compte... } }
routerAdd("POST", "/api/google-native-auth", (e) => {
// ID client OAuth « Web » (celui passé comme serverClientId côté app).
const WEB_CLIENT_ID =
"831733439319-45cergb7cf8r728fja4m0kcvu2r7fbj9.apps.googleusercontent.com";
const body = new DynamicModel({ idToken: "" });
e.bindBody(body);
const idToken = (body.idToken || "").trim();
if (!idToken) {
throw new BadRequestError("idToken manquant");
}
// Vérification de l'ID token auprès de Google (signature + expiration).
const res = $http.send({
url:
"https://oauth2.googleapis.com/tokeninfo?id_token=" +
encodeURIComponent(idToken),
method: "GET",
timeout: 15,
});
if (res.statusCode !== 200) {
throw new BadRequestError("ID token Google invalide");
}
const p = res.json;
if (p.aud !== WEB_CLIENT_ID) {
throw new BadRequestError("audience inattendue");
}
if (
p.iss !== "accounts.google.com" &&
p.iss !== "https://accounts.google.com"
) {
throw new BadRequestError("issuer inattendu");
}
const email = (p.email || "").toLowerCase();
if (!email) {
throw new BadRequestError("email absent du token");
}
if (String(p.email_verified) !== "true") {
throw new BadRequestError("email Google non verifie");
}
// Compte existant ?
let user = null;
try {
user = $app.findFirstRecordByFilter("users", "email = {:email}", {
email: email,
});
} catch (_) {
user = null;
}
// Sinon création d'un compte client (is_staff = false).
if (!user) {
const collection = $app.findCollectionByNameOrId("users");
user = new Record(collection);
user.set("email", email);
user.set("emailVisibility", false);
user.set("verified", true);
user.set("name", p.name || "");
user.set("is_staff", false);
user.setPassword($security.randomString(30));
$app.save(user);
}
// Fiche fidélité déjà présente ? Sinon on la crée à partir du nom/prénom
// Google → l'app cliente arrive directement sur sa carte (pas de saisie).
let hasProfile = false;
try {
$app.findFirstRecordByFilter("clients", "user = {:uid}", { uid: user.id });
hasProfile = true;
} catch (_) {
hasProfile = false;
}
if (!hasProfile) {
const prenom = p.given_name || "";
let nom = p.family_name || "";
if (!nom) {
nom = p.name || prenom || email.split("@")[0];
}
const code =
"FID-" +
$security.randomStringWithAlphabet(6, "ABCDEFGHJKLMNPQRSTUVWXYZ23456789");
const clientsCol = $app.findCollectionByNameOrId("clients");
const client = new Record(clientsCol);
client.set("user", user.id);
client.set("nom", nom);
client.set("prenom", prenom);
client.set("code", code);
$app.save(client);
}
const token = user.newAuthToken();
return e.json(200, { token: token, record: user.publicExport() });
});