(Feat) Add google
This commit is contained in:
@@ -1,11 +1,22 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
namespace = "com.magasin.app_fideliter_client"
|
||||
namespace = "com.letreveriennais.fidelite"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
@@ -15,8 +26,7 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.magasin.app_fideliter_client"
|
||||
applicationId = "com.letreveriennais.fidelite"
|
||||
// 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
|
||||
@@ -25,11 +35,26 @@ android {
|
||||
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 {
|
||||
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")
|
||||
// Signature avec la clé release (upload-keystore.jks) si key.properties
|
||||
// est présent, sinon repli sur la clé debug pour un build local.
|
||||
signingConfig = if (keystorePropertiesFile.exists()) {
|
||||
signingConfigs.getByName("release")
|
||||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<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" />
|
||||
|
||||
<application
|
||||
android:label="Ma fidélité"
|
||||
android:label="Le Trévériennais"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
@@ -44,5 +44,10 @@
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
<!-- Ouverture du navigateur pour l'authentification OAuth (url_launcher). -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.magasin.app_fideliter_client
|
||||
package com.letreveriennais.fidelite
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
@@ -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
@@ -1,9 +1,8 @@
|
||||
import 'package:flutter/material.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 'supabase_config.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
@@ -11,11 +10,8 @@ Future<void> main() async {
|
||||
|
||||
await initializeDateFormatting('fr_FR', null);
|
||||
|
||||
if (SupabaseConfig.estConfigure) {
|
||||
await Supabase.initialize(
|
||||
url: SupabaseConfig.url,
|
||||
publishableKey: SupabaseConfig.anonKey,
|
||||
);
|
||||
if (PbConfig.estConfigure) {
|
||||
await initPocketBase();
|
||||
}
|
||||
|
||||
runApp(const MonApp());
|
||||
@@ -27,7 +23,7 @@ class MonApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Ma fidélité',
|
||||
title: 'Le Trévériennais',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.clair(),
|
||||
darkTheme: AppTheme.sombre(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// Le compte fidélité du client connecté. Le [code] est encodé dans son QR.
|
||||
class Client {
|
||||
final String id; // uuid Supabase
|
||||
final String id; // id PocketBase
|
||||
final String code; // ex : « FID-3F9K2A »
|
||||
final String nom;
|
||||
final String? prenom;
|
||||
|
||||
@@ -32,7 +32,7 @@ class Mouvement {
|
||||
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() ??
|
||||
date: DateTime.tryParse(map['created']?.toString() ?? '')?.toLocal() ??
|
||||
DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -1,8 +1,8 @@
|
||||
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 '../supabase_config.dart';
|
||||
import 'complete_profile_screen.dart';
|
||||
import 'home_client_screen.dart';
|
||||
import 'welcome_screen.dart';
|
||||
@@ -13,14 +13,14 @@ class AuthGate extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!SupabaseConfig.estConfigure) return const _EcranNonConfigure();
|
||||
if (!PbConfig.estConfigure) return const _EcranNonConfigure();
|
||||
|
||||
return StreamBuilder<AuthState>(
|
||||
stream: supabase.auth.onAuthStateChange,
|
||||
return StreamBuilder<AuthStoreEvent>(
|
||||
stream: pb.authStore.onChange,
|
||||
builder: (context, snapshot) {
|
||||
final session = supabase.auth.currentSession;
|
||||
if (session == null) return const WelcomeScreen();
|
||||
return _SessionChargee(key: ValueKey(session.user.id));
|
||||
if (!pb.authStore.isValid) return const WelcomeScreen();
|
||||
final uid = pb.authStore.record?.id ?? '';
|
||||
return _SessionChargee(key: ValueKey(uid));
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -82,11 +82,11 @@ class _EcranNonConfigure extends StatelessWidget {
|
||||
children: [
|
||||
Icon(Icons.cloud_off, size: 56, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text('Supabase non configuré',
|
||||
Text('Backend 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.',
|
||||
'Renseignez l\'URL PocketBase dans lib/pocketbase_config.dart.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Color(0xFF6B6B72)),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../pocketbase_config.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é
|
||||
@@ -61,7 +61,7 @@ class _CompleteProfileScreenState extends State<CompleteProfileScreen> {
|
||||
IconButton(
|
||||
tooltip: 'Se déconnecter',
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => supabase.auth.signOut(),
|
||||
onPressed: () => pb.authStore.clear(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '../models/mouvement.dart';
|
||||
import '../models/recompense.dart';
|
||||
import '../pocketbase_config.dart';
|
||||
import '../services/session_client.dart';
|
||||
import '../supabase_config.dart';
|
||||
import '../theme.dart';
|
||||
import '../utils/format.dart';
|
||||
|
||||
@@ -58,7 +58,7 @@ class _HomeClientScreenState extends State<HomeClientScreen>
|
||||
);
|
||||
if (ok == true) {
|
||||
_session.vider();
|
||||
await supabase.auth.signOut();
|
||||
pb.authStore.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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).
|
||||
class LoginScreen extends StatefulWidget {
|
||||
@@ -33,14 +33,17 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
_erreur = null;
|
||||
});
|
||||
try {
|
||||
await supabase.auth.signInWithPassword(
|
||||
email: _emailCtrl.text.trim(),
|
||||
password: _mdpCtrl.text,
|
||||
);
|
||||
await pb.collection('users').authWithPassword(
|
||||
_emailCtrl.text.trim(),
|
||||
_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);
|
||||
} on ClientException catch (e) {
|
||||
if (mounted) {
|
||||
setState(() =>
|
||||
_erreur = messageErreurPb(e, 'Email ou mot de passe incorrect.'));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _erreur = 'Erreur : $e');
|
||||
} finally {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 '../supabase_config.dart';
|
||||
import '../theme.dart';
|
||||
|
||||
/// 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;
|
||||
});
|
||||
try {
|
||||
final res = await supabase.auth.signUp(
|
||||
email: _emailCtrl.text.trim(),
|
||||
password: _mdpCtrl.text,
|
||||
);
|
||||
final email = _emailCtrl.text.trim();
|
||||
// Création du compte, puis connexion immédiate (pas de confirmation
|
||||
// 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) {
|
||||
// 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();
|
||||
// Compte connecté → on crée le profil fidélité (nom + téléphone + code).
|
||||
// L'AuthGate basculera ensuite automatiquement vers l'accueil.
|
||||
await SessionClient.instance.creerProfil(
|
||||
nom: _nomCtrl.text,
|
||||
prenom: _prenomCtrl.text,
|
||||
telephone: _telCtrl.text,
|
||||
);
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
} on ClientException catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _erreur =
|
||||
messageErreurPb(e, 'Impossible de créer le compte.'));
|
||||
}
|
||||
} on AuthException catch (e) {
|
||||
if (mounted) setState(() => _erreur = e.message);
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _erreur = 'Erreur : $e');
|
||||
} 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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme.dart';
|
||||
import '../widgets/oauth_boutons.dart';
|
||||
import 'login_screen.dart';
|
||||
import 'signup_screen.dart';
|
||||
|
||||
@@ -57,6 +58,8 @@ class WelcomeScreen extends StatelessWidget {
|
||||
),
|
||||
child: const Text('J\'ai déjà un compte'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const OAuthBoutons(),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'dart:math';
|
||||
|
||||
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/mouvement.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
|
||||
/// contexte magasin (nom, récompenses proposées). Source de vérité côté client.
|
||||
@@ -34,47 +38,46 @@ class SessionClient extends ChangeNotifier {
|
||||
_erreur = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
final uid = supabase.auth.currentUser?.id;
|
||||
final uid = pb.authStore.record?.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?) ?? '';
|
||||
// Fiche fidélité du compte (une par utilisateur). Absente → profil à créer.
|
||||
try {
|
||||
final row = await pb
|
||||
.collection('clients')
|
||||
.getFirstListItem('user = "$uid"');
|
||||
_monClient = Client.fromMap(row.toJson());
|
||||
} on ClientException catch (e) {
|
||||
if (e.statusCode == 404) {
|
||||
_monClient = null;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
final recs = await supabase
|
||||
.from('recompenses')
|
||||
.select()
|
||||
.eq('actif', true)
|
||||
.order('cout_points');
|
||||
_recompenses =
|
||||
(recs as List).map((e) => Recompense.fromMap(e)).toList();
|
||||
// Contexte magasin (lisible par tout compte connecté) : ligne unique.
|
||||
final reglages = await pb.collection('reglages').getList(perPage: 1);
|
||||
if (reglages.items.isNotEmpty) {
|
||||
final r = reglages.items.first;
|
||||
_eurosParPoint = (r.data['euros_par_point'] as num?)?.toDouble() ?? 0;
|
||||
_nomMagasin = (r.data['nom_magasin'] as String?) ?? '';
|
||||
}
|
||||
|
||||
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) {
|
||||
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();
|
||||
final mvts = await pb.collection('mouvements').getFullList(
|
||||
filter: 'client = "${_monClient!.id}"',
|
||||
sort: '-created',
|
||||
);
|
||||
_mouvements = mvts.map((e) => Mouvement.fromMap(e.toJson())).toList();
|
||||
} else {
|
||||
_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
|
||||
/// QR sont générés côté base. Utilisé à l'inscription / finalisation de profil.
|
||||
/// Crée le profil fidélité du compte connecté (nom + téléphone). Le code de
|
||||
/// 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({
|
||||
required String nom,
|
||||
String? prenom,
|
||||
String? telephone,
|
||||
}) async {
|
||||
final uid = supabase.auth.currentUser?.id;
|
||||
final uid = pb.authStore.record?.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,
|
||||
await pb.collection('clients').create(body: {
|
||||
'user': uid,
|
||||
'code': _genererCode(),
|
||||
'nom': nom.trim(),
|
||||
'prenom': ouNull(prenom),
|
||||
'telephone': ouNull(telephone),
|
||||
if (ouNull(prenom) != null) 'prenom': ouNull(prenom),
|
||||
if (ouNull(telephone) != null) 'telephone': ouNull(telephone),
|
||||
});
|
||||
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() {
|
||||
_monClient = null;
|
||||
_mouvements = [];
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -1,38 +1,6 @@
|
||||
# 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:
|
||||
@@ -105,22 +73,6 @@ packages:
|
||||
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:
|
||||
@@ -129,14 +81,6 @@ packages:
|
||||
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:
|
||||
@@ -182,6 +126,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -192,30 +144,54 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
functions_client:
|
||||
google_identity_services_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: functions_client
|
||||
sha256: "5b17b8dcf5ae1cd6a6428e7a4ca8b569477297a7f158955b5407dda23baeaa5b"
|
||||
name: google_identity_services_web
|
||||
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.3"
|
||||
gotrue:
|
||||
version: "0.3.3+1"
|
||||
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
|
||||
description:
|
||||
name: gotrue
|
||||
sha256: "54e8abe49c8596234c503f4b64d07022c7913a6475c7a85c1f1eacc1a93af166"
|
||||
name: google_sign_in_android
|
||||
sha256: a01d4fd5a0e4eb0c1961434a0289a436abf9225b0933963c8e4e5ed88c26e40b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.24.0"
|
||||
gtk:
|
||||
version: "7.2.15"
|
||||
google_sign_in_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gtk
|
||||
sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5"
|
||||
name: google_sign_in_ios
|
||||
sha256: ac1e4c1205267cb7999d1d81333fccffdfda29e853f434bbaf71525498bb6950
|
||||
url: "https://pub.dev"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -256,14 +232,6 @@ packages:
|
||||
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:
|
||||
@@ -296,14 +264,6 @@ packages:
|
||||
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:
|
||||
@@ -328,22 +288,6 @@ packages:
|
||||
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:
|
||||
@@ -352,6 +296,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -400,14 +352,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
pocketbase:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
|
||||
name: pocketbase
|
||||
sha256: f70353ea9c583fabe4165880e054504c5036c428f59c0fc588d64bb19218df05
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "0.22.0"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -416,14 +368,6 @@ packages:
|
||||
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:
|
||||
@@ -440,32 +384,8 @@ packages:
|
||||
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
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||
@@ -541,14 +461,6 @@ packages:
|
||||
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:
|
||||
@@ -565,22 +477,6 @@ packages:
|
||||
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:
|
||||
@@ -605,70 +501,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher:
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
name: vector_graphics
|
||||
sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
version: "1.2.2"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
|
||||
name: vector_graphics_codec
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.32"
|
||||
url_launcher_ios:
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||
name: vector_graphics_compiler
|
||||
sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3"
|
||||
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"
|
||||
version: "1.2.6"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -693,22 +549,6 @@ packages:
|
||||
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:
|
||||
@@ -733,14 +573,6 @@ packages:
|
||||
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"
|
||||
|
||||
+4
-1
@@ -34,7 +34,10 @@ dependencies:
|
||||
# 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é
|
||||
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
|
||||
intl: ^0.20.3 # formatage € et dates (locale fr_FR)
|
||||
|
||||
|
||||
@@ -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() });
|
||||
});
|
||||
Reference in New Issue
Block a user