(Feat) Add google
This commit is contained in:
@@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
-1
@@ -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
|
||||||
|
|
||||||
@@ -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: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,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;
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: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)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
} else {
|
} on ClientException catch (e) {
|
||||||
// Confirmation d'email requise : le profil sera finalisé à la 1re
|
if (mounted) {
|
||||||
// connexion (après validation du mail).
|
setState(() => _erreur =
|
||||||
if (mounted) await _popupVerifierEmail();
|
messageErreurPb(e, 'Impossible de créer le compte.'));
|
||||||
}
|
}
|
||||||
} 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(
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
@@ -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
|
# 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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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