samre-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/bin/samre.js +89 -0
- package/package.json +37 -0
- package/src/commands/inject.js +92 -0
- package/src/commands/remove.js +32 -0
- package/src/templates/flutter_sdk.js +303 -0
- package/src/utils/api.js +41 -0
- package/src/utils/flutter.js +131 -0
- package/src/utils/logger.js +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# samre-cli
|
|
2
|
+
|
|
3
|
+
> CLI officiel d'injection automatique du SDK **Samré** pour les applications mobiles (Flutter & React Native).
|
|
4
|
+
|
|
5
|
+
Permet aux développeurs de connecter instantanément leur application mobile à la plateforme **Samré** pour mener à bien leurs campagnes de test fermé Google Play (12 jours, 12 panélistes).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🚀 Utilisation Rapide
|
|
10
|
+
|
|
11
|
+
Aucune installation préalable n'est nécessaire. Exécutez simplement `npx` dans le répertoire racine de votre projet mobile :
|
|
12
|
+
|
|
13
|
+
### 1. Injection automatique du SDK
|
|
14
|
+
```bash
|
|
15
|
+
npx samre-cli inject --token=VOTRE_TOKEN_INTEGRATION
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Si votre serveur Samré est hébergé sur une URL personnalisée ou un tunnel (ngrok) :
|
|
19
|
+
```bash
|
|
20
|
+
npx samre-cli inject --token=VOTRE_TOKEN_INTEGRATION --api-url=https://mon-serveur-samre.com
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Tester votre application
|
|
24
|
+
```bash
|
|
25
|
+
flutter run
|
|
26
|
+
```
|
|
27
|
+
Un badge discret de test apparaîtra sur votre écran. Après 25 secondes d'activité, vos panélistes pourront soumettre leur code quotidien en un clic.
|
|
28
|
+
|
|
29
|
+
### 3. Désinstallation post-campagne
|
|
30
|
+
Une fois vos 12 jours de test terminés :
|
|
31
|
+
```bash
|
|
32
|
+
npx samre-cli remove
|
|
33
|
+
```
|
|
34
|
+
Restaure votre application dans son état d'origine en supprimant le SDK sans laisser aucun résidu.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🔒 Sécurité & Confidentialité
|
|
39
|
+
- Les clés HMAC secrètes ne quittent jamais le backend Samré.
|
|
40
|
+
- Les validations sont sécurisées contre le rejeu et les fraudes.
|
|
41
|
+
- Aucun impact sur les performances de votre application.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 📄 Licence
|
|
46
|
+
MIT © [Samré Team](https://samre.app)
|
package/bin/samre.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { handleInject } from '../src/commands/inject.js';
|
|
4
|
+
import { handleRemove } from '../src/commands/remove.js';
|
|
5
|
+
import { logger } from '../src/utils/logger.js';
|
|
6
|
+
|
|
7
|
+
function parseArgs(args) {
|
|
8
|
+
const result = {
|
|
9
|
+
command: null,
|
|
10
|
+
token: null,
|
|
11
|
+
apiUrl: 'http://localhost:8000',
|
|
12
|
+
help: false,
|
|
13
|
+
version: false,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
const arg = args[i];
|
|
18
|
+
|
|
19
|
+
if (arg === 'inject' || arg === 'remove') {
|
|
20
|
+
result.command = arg;
|
|
21
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
22
|
+
result.help = true;
|
|
23
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
24
|
+
result.version = true;
|
|
25
|
+
} else if (arg.startsWith('--token=')) {
|
|
26
|
+
result.token = arg.slice(8);
|
|
27
|
+
} else if (arg === '--token' && i + 1 < args.length) {
|
|
28
|
+
result.token = args[++i];
|
|
29
|
+
} else if (arg.startsWith('--api-url=')) {
|
|
30
|
+
result.apiUrl = arg.slice(10);
|
|
31
|
+
} else if (arg === '--api-url' && i + 1 < args.length) {
|
|
32
|
+
result.apiUrl = args[++i];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function showHelp() {
|
|
40
|
+
logger.banner();
|
|
41
|
+
console.log(` ${"\x1b[1m"}Usage:${"\x1b[0m"}
|
|
42
|
+
npx samre-cli <commande> [options]
|
|
43
|
+
|
|
44
|
+
${"\x1b[1m"}Commandes:${"\x1b[0m"}
|
|
45
|
+
inject Injecte automatiquement le SDK Samré dans votre projet Flutter
|
|
46
|
+
remove Désinstalle proprement le SDK et restaure les fichiers d'origine
|
|
47
|
+
|
|
48
|
+
${"\x1b[1m"}Options pour inject :${"\x1b[0m"}
|
|
49
|
+
--token=<TOKEN> Token unique d'intégration de votre application (Requis)
|
|
50
|
+
--api-url=<URL> URL de l'API Samré (Optionnel, ex: https://votredomaine.com ou ngrok)
|
|
51
|
+
|
|
52
|
+
${"\x1b[1m"}Options générales :${"\x1b[0m"}
|
|
53
|
+
--help, -h Affiche cette aide
|
|
54
|
+
--version, -v Affiche la version du CLI
|
|
55
|
+
|
|
56
|
+
${"\x1b[1m"}Exemples :${"\x1b[0m"}
|
|
57
|
+
npx samre-cli inject --token=integ_a1b2c3d4e5f6
|
|
58
|
+
npx samre-cli inject --token=integ_a1b2c3d4e5f6 --api-url=https://mon-serveur.com
|
|
59
|
+
npx samre-cli remove
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
const args = process.argv.slice(2);
|
|
65
|
+
const options = parseArgs(args);
|
|
66
|
+
|
|
67
|
+
if (options.version) {
|
|
68
|
+
console.log('samre-cli v1.0.0');
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options.help || !options.command) {
|
|
73
|
+
showHelp();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
if (options.command === 'inject') {
|
|
79
|
+
await handleInject(options);
|
|
80
|
+
} else if (options.command === 'remove') {
|
|
81
|
+
await handleRemove(options);
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
logger.error(`Erreur inattendue : ${err.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "samre-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI officiel d'injection automatique du SDK Samré dans les applications mobiles (Flutter, React Native)",
|
|
5
|
+
"main": "bin/samre.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"samre-cli": "bin/samre.js",
|
|
8
|
+
"samre": "bin/samre.js"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18.0.0"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node bin/samre.js --help"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"samre",
|
|
19
|
+
"samre-sdk",
|
|
20
|
+
"flutter",
|
|
21
|
+
"react-native",
|
|
22
|
+
"testing",
|
|
23
|
+
"google-play",
|
|
24
|
+
"closed-testing",
|
|
25
|
+
"daily-code"
|
|
26
|
+
],
|
|
27
|
+
"author": "Samré Team",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/lydiedouti09-crypto/samre-cli.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/lydiedouti09-crypto/samre-cli/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://samre.app"
|
|
37
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { logger } from '../utils/logger.js';
|
|
2
|
+
import { fetchIntegrationInfo } from '../utils/api.js';
|
|
3
|
+
import { FlutterUtil } from '../utils/flutter.js';
|
|
4
|
+
|
|
5
|
+
export async function handleInject(options) {
|
|
6
|
+
const { token, apiUrl = 'http://localhost:8000', cwd = process.cwd() } = options;
|
|
7
|
+
|
|
8
|
+
if (!token) {
|
|
9
|
+
logger.error("Le paramètre --token est requis pour l'injection.");
|
|
10
|
+
console.log(" Usage : npx samre-cli inject --token=<VOTRE_TOKEN> [--api-url=<URL>]\n");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
logger.banner();
|
|
15
|
+
logger.info(`Récupération de la configuration pour le token : ${token}...`);
|
|
16
|
+
logger.info(`Serveur Samré cible : ${apiUrl}`);
|
|
17
|
+
|
|
18
|
+
// 1. Récupération des informations d'intégration depuis l'API
|
|
19
|
+
let appData;
|
|
20
|
+
let normalizedUrl;
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetchIntegrationInfo(apiUrl, token);
|
|
23
|
+
appData = res.data;
|
|
24
|
+
normalizedUrl = res.normalizedApiUrl;
|
|
25
|
+
logger.success(`Application identifiée : "${appData.application.nom}" (v${appData.application.version})`);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
logger.error(err.message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Détection du type de projet
|
|
32
|
+
const isFlutter = FlutterUtil.isFlutterProject(cwd);
|
|
33
|
+
|
|
34
|
+
if (!isFlutter) {
|
|
35
|
+
logger.error("Aucun projet Flutter détecté dans le répertoire courant (pubspec.yaml introuvable).");
|
|
36
|
+
logger.info("Assurez-vous d'exécuter cette commande à la racine de votre projet mobile.");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logger.step('1/3', 'Configuration des dépendances...');
|
|
41
|
+
try {
|
|
42
|
+
const depRes = FlutterUtil.ensureHttpDependency(cwd);
|
|
43
|
+
if (depRes.alreadyPresent) {
|
|
44
|
+
logger.info('Dépendance "http" déjà présente dans pubspec.yaml');
|
|
45
|
+
} else {
|
|
46
|
+
logger.success('Dépendance "http: ^1.2.0" ajoutée avec succès');
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
logger.warn(`Attention lors de l'ajout de dépendance : ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
logger.step('2/3', 'Génération du SDK Samré Flutter...');
|
|
53
|
+
try {
|
|
54
|
+
const sdkPath = FlutterUtil.writeSdkFile(cwd, {
|
|
55
|
+
apiUrl: normalizedUrl,
|
|
56
|
+
apiKey: appData.apiKey,
|
|
57
|
+
appId: appData.application.id,
|
|
58
|
+
appName: appData.application.nom,
|
|
59
|
+
dureeJours: appData.application.dureeJours,
|
|
60
|
+
});
|
|
61
|
+
logger.success(`Fichier créé : ${sdkPath}`);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
logger.error(`Échec de création du SDK : ${err.message}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
logger.step('3/3', "Activation de l'Overlay Samré...");
|
|
68
|
+
try {
|
|
69
|
+
const mainRes = FlutterUtil.injectIntoMain(cwd);
|
|
70
|
+
if (mainRes.alreadyInjected) {
|
|
71
|
+
logger.info("SamreOverlay est déjà activé dans lib/main.dart");
|
|
72
|
+
} else if (mainRes.modifiedHome) {
|
|
73
|
+
logger.success("SamreOverlay a été automatiquement branché sur votre écran d'accueil !");
|
|
74
|
+
logger.info(`Sauvegarde de sécurité créée sous : lib/main.dart.samre_bak`);
|
|
75
|
+
} else {
|
|
76
|
+
logger.success("Import ajouté dans lib/main.dart.");
|
|
77
|
+
logger.info("Pour finaliser, entourez votre écran d'accueil avec SamreOverlay :");
|
|
78
|
+
logger.code("home: const SamreOverlay(\n child: MyHomePage(),\n)");
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.warn(`Note sur main.dart : ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log('\n' + '─'.repeat(60));
|
|
85
|
+
logger.success("🎉 Intégration du SDK Samré terminée avec succès !");
|
|
86
|
+
console.log('─'.repeat(60));
|
|
87
|
+
console.log('\n 👉 Prochaine étape : Lancez votre application pour tester :');
|
|
88
|
+
logger.code('flutter run');
|
|
89
|
+
console.log(' 👉 Pour désinstaller proprement après votre campagne :');
|
|
90
|
+
logger.code('npx samre-cli remove');
|
|
91
|
+
console.log('');
|
|
92
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { logger } from '../utils/logger.js';
|
|
2
|
+
import { FlutterUtil } from '../utils/flutter.js';
|
|
3
|
+
|
|
4
|
+
export async function handleRemove(options) {
|
|
5
|
+
const { cwd = process.cwd() } = options;
|
|
6
|
+
|
|
7
|
+
logger.banner();
|
|
8
|
+
logger.info("Désinstallation du SDK Samré...");
|
|
9
|
+
|
|
10
|
+
const isFlutter = FlutterUtil.isFlutterProject(cwd);
|
|
11
|
+
|
|
12
|
+
if (!isFlutter) {
|
|
13
|
+
logger.error("Aucun projet Flutter détecté dans le répertoire courant (pubspec.yaml introuvable).");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { sdkDeleted, mainRestored } = FlutterUtil.removeSdk(cwd);
|
|
18
|
+
|
|
19
|
+
if (sdkDeleted) {
|
|
20
|
+
logger.success("Fichier lib/samre_sdk.dart supprimé.");
|
|
21
|
+
} else {
|
|
22
|
+
logger.info("Fichier lib/samre_sdk.dart déjà absent.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (mainRestored) {
|
|
26
|
+
logger.success("Fichier lib/main.dart restauré dans son état initial.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log('\n' + '─'.repeat(60));
|
|
30
|
+
logger.success("✨ Désinstallation propre terminée sans résidu !");
|
|
31
|
+
console.log('─'.repeat(60) + '\n');
|
|
32
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// Modèle complet pour lib/samre_sdk.dart
|
|
2
|
+
|
|
3
|
+
export function generateFlutterSdkCode({ apiUrl, apiKey, appId, appName, dureeJours }) {
|
|
4
|
+
return `// =================================================================
|
|
5
|
+
// SDK Samré Mobile pour Flutter (Généré automatiquement par samre-cli)
|
|
6
|
+
// Application : ${appName || 'App Samré'} (ID: ${appId})
|
|
7
|
+
// Campagne : ${dureeJours || 12} jours
|
|
8
|
+
// =================================================================
|
|
9
|
+
|
|
10
|
+
import 'dart:async';
|
|
11
|
+
import 'dart:convert';
|
|
12
|
+
import 'dart:io';
|
|
13
|
+
import 'package:flutter/material.dart';
|
|
14
|
+
import 'package:http/http.dart' as http;
|
|
15
|
+
|
|
16
|
+
/// Configuration du SDK Samré
|
|
17
|
+
class SamreConfig {
|
|
18
|
+
static const String apiUrl = "${apiUrl}";
|
|
19
|
+
static const String apiKey = "${apiKey}";
|
|
20
|
+
static const String appId = "${appId}";
|
|
21
|
+
static const int requiredDurationSeconds = 25; // Compteur de sécurité anti-triche
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Overlay automatique avec compteur de sécurité de 25 secondes
|
|
25
|
+
class SamreOverlay extends StatefulWidget {
|
|
26
|
+
final Widget child;
|
|
27
|
+
const SamreOverlay({Key? key, required this.child}) : super(key: key);
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
State<SamreOverlay> createState() => _SamreOverlayState();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class _SamreOverlayState extends State<SamreOverlay> {
|
|
34
|
+
int _secondsRemaining = SamreConfig.requiredDurationSeconds;
|
|
35
|
+
bool _canSubmit = false;
|
|
36
|
+
Timer? _timer;
|
|
37
|
+
|
|
38
|
+
@override
|
|
39
|
+
void initState() {
|
|
40
|
+
super.initState();
|
|
41
|
+
_startTimer();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void _startTimer() {
|
|
45
|
+
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
46
|
+
if (!mounted) return;
|
|
47
|
+
if (_secondsRemaining > 1) {
|
|
48
|
+
setState(() => _secondsRemaining--);
|
|
49
|
+
} else {
|
|
50
|
+
setState(() {
|
|
51
|
+
_secondsRemaining = 0;
|
|
52
|
+
_canSubmit = true;
|
|
53
|
+
});
|
|
54
|
+
timer.cancel();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
void dispose() {
|
|
61
|
+
_timer?.cancel();
|
|
62
|
+
super.dispose();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
void _openValidationDialog() {
|
|
66
|
+
showDialog(
|
|
67
|
+
context: context,
|
|
68
|
+
barrierDismissible: false,
|
|
69
|
+
builder: (ctx) => const SamreValidationDialog(),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@override
|
|
74
|
+
Widget build(BuildContext context) {
|
|
75
|
+
return Stack(
|
|
76
|
+
children: [
|
|
77
|
+
widget.child,
|
|
78
|
+
Positioned(
|
|
79
|
+
bottom: 24,
|
|
80
|
+
right: 20,
|
|
81
|
+
child: Material(
|
|
82
|
+
elevation: 8,
|
|
83
|
+
borderRadius: BorderRadius.circular(24),
|
|
84
|
+
color: _canSubmit ? const Color(0xFF2563EB) : Colors.black.withOpacity(0.75),
|
|
85
|
+
child: InkWell(
|
|
86
|
+
borderRadius: BorderRadius.circular(24),
|
|
87
|
+
onTap: _canSubmit ? _openValidationDialog : null,
|
|
88
|
+
child: Padding(
|
|
89
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
90
|
+
child: Row(
|
|
91
|
+
mainAxisSize: MainAxisSize.min,
|
|
92
|
+
children: [
|
|
93
|
+
Icon(
|
|
94
|
+
_canSubmit ? Icons.verified_user : Icons.timer_outlined,
|
|
95
|
+
color: Colors.white,
|
|
96
|
+
size: 18,
|
|
97
|
+
),
|
|
98
|
+
const SizedBox(width: 8),
|
|
99
|
+
Text(
|
|
100
|
+
_canSubmit ? 'Valider Journée' : 'Test en cours (\${_secondsRemaining}s)',
|
|
101
|
+
style: const TextStyle(
|
|
102
|
+
color: Colors.white,
|
|
103
|
+
fontWeight: FontWeight.bold,
|
|
104
|
+
fontSize: 12,
|
|
105
|
+
decoration: TextDecoration.none,
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
],
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
],
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Boîte de dialogue de validation du code quotidien par le panéliste
|
|
120
|
+
class SamreValidationDialog extends StatefulWidget {
|
|
121
|
+
const SamreValidationDialog({Key? key}) : super(key: key);
|
|
122
|
+
|
|
123
|
+
@override
|
|
124
|
+
State<SamreValidationDialog> createState() => _SamreValidationDialogState();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class _SamreValidationDialogState extends State<SamreValidationDialog> {
|
|
128
|
+
final _panelisteCtrl = TextEditingController();
|
|
129
|
+
final _codeCtrl = TextEditingController();
|
|
130
|
+
bool _loading = false;
|
|
131
|
+
String? _message;
|
|
132
|
+
bool _success = false;
|
|
133
|
+
|
|
134
|
+
@override
|
|
135
|
+
void dispose() {
|
|
136
|
+
_panelisteCtrl.dispose();
|
|
137
|
+
_codeCtrl.dispose();
|
|
138
|
+
super.dispose();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Future<void> _verify() async {
|
|
142
|
+
final paneliste = _panelisteCtrl.text.trim();
|
|
143
|
+
final code = _codeCtrl.text.trim();
|
|
144
|
+
|
|
145
|
+
if (paneliste.isEmpty || code.isEmpty) {
|
|
146
|
+
setState(() {
|
|
147
|
+
_message = 'Veuillez renseigner votre ID panéliste et le code du jour.';
|
|
148
|
+
_success = false;
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setState(() {
|
|
154
|
+
_loading = true;
|
|
155
|
+
_message = null;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
final uri = Uri.parse('\${SamreConfig.apiUrl}/api/sdk/verify-day');
|
|
160
|
+
final response = await http.post(
|
|
161
|
+
uri,
|
|
162
|
+
headers: {
|
|
163
|
+
'Content-Type': 'application/json',
|
|
164
|
+
'Accept': 'application/json',
|
|
165
|
+
'X-App-Key': SamreConfig.apiKey,
|
|
166
|
+
},
|
|
167
|
+
body: jsonEncode({
|
|
168
|
+
'apiKey': SamreConfig.apiKey,
|
|
169
|
+
'app_id': SamreConfig.appId,
|
|
170
|
+
'panelisteId': paneliste,
|
|
171
|
+
'panelisteUid': paneliste,
|
|
172
|
+
'code': code,
|
|
173
|
+
'deviceId': Platform.isAndroid ? 'android-\${paneliste}' : 'ios-\${paneliste}',
|
|
174
|
+
}),
|
|
175
|
+
).timeout(const Duration(seconds: 15));
|
|
176
|
+
|
|
177
|
+
final data = jsonDecode(response.body);
|
|
178
|
+
|
|
179
|
+
if (response.statusCode >= 200 && response.statusCode < 300 && data['success'] == true) {
|
|
180
|
+
setState(() {
|
|
181
|
+
_success = true;
|
|
182
|
+
_message = data['message'] ?? 'Félicitations ! Votre journée de test a été validée avec succès.';
|
|
183
|
+
});
|
|
184
|
+
} else {
|
|
185
|
+
setState(() {
|
|
186
|
+
_success = false;
|
|
187
|
+
_message = data['message'] ?? data['error'] ?? 'Code invalide ou expiré.';
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
setState(() {
|
|
192
|
+
_success = false;
|
|
193
|
+
_message = 'Erreur de connexion au serveur Samré. Vérifiez votre connexion internet.';
|
|
194
|
+
});
|
|
195
|
+
} finally {
|
|
196
|
+
setState(() => _loading = false);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@override
|
|
201
|
+
Widget build(BuildContext context) {
|
|
202
|
+
return AlertDialog(
|
|
203
|
+
backgroundColor: const Color(0xFF0F172A),
|
|
204
|
+
shape: RoundedRectangleBorder(
|
|
205
|
+
borderRadius: BorderRadius.circular(20),
|
|
206
|
+
side: const BorderSide(color: Color(0xFF334155)),
|
|
207
|
+
),
|
|
208
|
+
title: Row(
|
|
209
|
+
children: [
|
|
210
|
+
Container(
|
|
211
|
+
padding: const EdgeInsets.all(8),
|
|
212
|
+
decoration: BoxDecoration(
|
|
213
|
+
color: const Color(0xFF2563EB).withOpacity(0.2),
|
|
214
|
+
borderRadius: BorderRadius.circular(10),
|
|
215
|
+
),
|
|
216
|
+
child: const Icon(Icons.verified, color: Color(0xFF60A5FA), size: 22),
|
|
217
|
+
),
|
|
218
|
+
const SizedBox(width: 12),
|
|
219
|
+
const Expanded(
|
|
220
|
+
child: Text(
|
|
221
|
+
'Validation Samré',
|
|
222
|
+
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
],
|
|
226
|
+
),
|
|
227
|
+
content: SingleChildScrollView(
|
|
228
|
+
child: Column(
|
|
229
|
+
mainAxisSize: MainAxisSize.min,
|
|
230
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
231
|
+
children: [
|
|
232
|
+
const Text(
|
|
233
|
+
'Saisissez vos identifiants fournis par la plateforme Samré pour enregistrer votre journée.',
|
|
234
|
+
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 13),
|
|
235
|
+
),
|
|
236
|
+
const SizedBox(height: 16),
|
|
237
|
+
TextField(
|
|
238
|
+
controller: _panelisteCtrl,
|
|
239
|
+
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600),
|
|
240
|
+
decoration: InputDecoration(
|
|
241
|
+
labelText: 'Identifiant Panéliste',
|
|
242
|
+
labelStyle: const TextStyle(color: Color(0xFF94A3B8)),
|
|
243
|
+
hintText: 'Ex: TST-7A8B9C',
|
|
244
|
+
hintStyle: const TextStyle(color: Color(0xFF475569)),
|
|
245
|
+
filled: true,
|
|
246
|
+
fillColor: const Color(0xFF1E293B),
|
|
247
|
+
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
|
248
|
+
),
|
|
249
|
+
),
|
|
250
|
+
const SizedBox(height: 12),
|
|
251
|
+
TextField(
|
|
252
|
+
controller: _codeCtrl,
|
|
253
|
+
textCapitalization: TextCapitalization.characters,
|
|
254
|
+
style: const TextStyle(color: Colors.white, letterSpacing: 2, fontWeight: FontWeight.bold),
|
|
255
|
+
decoration: InputDecoration(
|
|
256
|
+
labelText: 'Code Unique du Jour',
|
|
257
|
+
labelStyle: const TextStyle(color: Color(0xFF94A3B8)),
|
|
258
|
+
hintText: 'Ex: 7K9P-4MX2',
|
|
259
|
+
hintStyle: const TextStyle(color: Color(0xFF475569), letterSpacing: 0),
|
|
260
|
+
filled: true,
|
|
261
|
+
fillColor: const Color(0xFF1E293B),
|
|
262
|
+
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
|
263
|
+
),
|
|
264
|
+
),
|
|
265
|
+
if (_message != null) ...[
|
|
266
|
+
const SizedBox(height: 14),
|
|
267
|
+
Container(
|
|
268
|
+
padding: const EdgeInsets.all(12),
|
|
269
|
+
decoration: BoxDecoration(
|
|
270
|
+
color: _success ? const Color(0xFF065F46).withOpacity(0.3) : const Color(0xFF991B1B).withOpacity(0.3),
|
|
271
|
+
borderRadius: BorderRadius.circular(10),
|
|
272
|
+
border: Border.all(color: _success ? const Color(0xFF10B981) : const Color(0xFFEF4444)),
|
|
273
|
+
),
|
|
274
|
+
child: Text(
|
|
275
|
+
_message!,
|
|
276
|
+
style: TextStyle(color: _success ? const Color(0xFF34D399) : const Color(0xFFF87171), fontSize: 12, fontWeight: FontWeight.w600),
|
|
277
|
+
),
|
|
278
|
+
),
|
|
279
|
+
],
|
|
280
|
+
],
|
|
281
|
+
),
|
|
282
|
+
),
|
|
283
|
+
actions: [
|
|
284
|
+
TextButton(
|
|
285
|
+
onPressed: () => Navigator.pop(context),
|
|
286
|
+
child: const Text('Fermer', style: TextStyle(color: Color(0xFF94A3B8))),
|
|
287
|
+
),
|
|
288
|
+
ElevatedButton(
|
|
289
|
+
onPressed: _loading ? null : _verify,
|
|
290
|
+
style: ElevatedButton.styleFrom(
|
|
291
|
+
backgroundColor: const Color(0xFF2563EB),
|
|
292
|
+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
293
|
+
),
|
|
294
|
+
child: _loading
|
|
295
|
+
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
|
296
|
+
: const Text('Valider', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
|
297
|
+
),
|
|
298
|
+
],
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
`;
|
|
303
|
+
}
|
package/src/utils/api.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Récupération des informations d'intégration depuis l'API Samré
|
|
2
|
+
|
|
3
|
+
export async function fetchIntegrationInfo(apiUrl, token) {
|
|
4
|
+
// Normaliser l'URL
|
|
5
|
+
let baseUrl = apiUrl.trim();
|
|
6
|
+
if (baseUrl.endsWith('/')) {
|
|
7
|
+
baseUrl = baseUrl.slice(0, -1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const endpoint = `${baseUrl}/api/public/integration/${encodeURIComponent(token)}`;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(endpoint, {
|
|
14
|
+
method: 'GET',
|
|
15
|
+
headers: {
|
|
16
|
+
'Accept': 'application/json',
|
|
17
|
+
'User-Agent': 'samre-cli/1.0.0'
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
if (response.status === 404) {
|
|
23
|
+
throw new Error(`Token d'intégration invalide ou introuvable : "${token}"`);
|
|
24
|
+
}
|
|
25
|
+
const errText = await response.text();
|
|
26
|
+
throw new Error(`Erreur serveur (${response.status}) : ${errText || response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
data,
|
|
33
|
+
normalizedApiUrl: baseUrl
|
|
34
|
+
};
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err.cause && err.cause.code === 'ECONNREFUSED') {
|
|
37
|
+
throw new Error(`Impossible de joindre le serveur Samré sur "${baseUrl}". Vérifiez que l'API ou le tunnel ngrok est actif.`);
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { generateFlutterSdkCode } from '../templates/flutter_sdk.js';
|
|
5
|
+
|
|
6
|
+
export const FlutterUtil = {
|
|
7
|
+
isFlutterProject(cwd = process.cwd()) {
|
|
8
|
+
const pubspecPath = path.join(cwd, 'pubspec.yaml');
|
|
9
|
+
return fs.existsSync(pubspecPath);
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
ensureHttpDependency(cwd = process.cwd()) {
|
|
13
|
+
const pubspecPath = path.join(cwd, 'pubspec.yaml');
|
|
14
|
+
if (!fs.existsSync(pubspecPath)) {
|
|
15
|
+
throw new Error("pubspec.yaml introuvable.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let pubspecContent = fs.readFileSync(pubspecPath, 'utf-8');
|
|
19
|
+
|
|
20
|
+
// Vérifier si http est déjà présent
|
|
21
|
+
if (pubspecContent.includes('http:') || pubspecContent.includes('http :')) {
|
|
22
|
+
return { alreadyPresent: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Essayer avec flutter pub add http si possible
|
|
26
|
+
try {
|
|
27
|
+
execSync('flutter pub add http', { cwd, stdio: 'ignore' });
|
|
28
|
+
return { addedViaCli: true };
|
|
29
|
+
} catch {
|
|
30
|
+
// Fallback : injection directe dans pubspec.yaml
|
|
31
|
+
const lines = pubspecContent.split('\n');
|
|
32
|
+
const depIndex = lines.findIndex(l => /^dependencies\s*:/.test(l));
|
|
33
|
+
|
|
34
|
+
if (depIndex === -1) {
|
|
35
|
+
throw new Error("Section 'dependencies:' introuvable dans pubspec.yaml");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Insérer http: ^1.2.0 juste après 'dependencies:'
|
|
39
|
+
lines.splice(depIndex + 1, 0, ' http: ^1.2.0');
|
|
40
|
+
fs.writeFileSync(pubspecPath, lines.join('\n'), 'utf-8');
|
|
41
|
+
return { addedDirectly: true };
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
writeSdkFile(cwd = process.cwd(), config) {
|
|
46
|
+
const libDir = path.join(cwd, 'lib');
|
|
47
|
+
if (!fs.existsSync(libDir)) {
|
|
48
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sdkPath = path.join(libDir, 'samre_sdk.dart');
|
|
52
|
+
const sdkCode = generateFlutterSdkCode(config);
|
|
53
|
+
fs.writeFileSync(sdkPath, sdkCode, 'utf-8');
|
|
54
|
+
return sdkPath;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
injectIntoMain(cwd = process.cwd()) {
|
|
58
|
+
const mainPath = path.join(cwd, 'lib', 'main.dart');
|
|
59
|
+
const backupPath = path.join(cwd, 'lib', 'main.dart.samre_bak');
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(mainPath)) {
|
|
62
|
+
return { injected: false, reason: 'main.dart introuvable' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const content = fs.readFileSync(mainPath, 'utf-8');
|
|
66
|
+
|
|
67
|
+
// Sauvegarde de secours si pas encore existante
|
|
68
|
+
if (!fs.existsSync(backupPath)) {
|
|
69
|
+
fs.writeFileSync(backupPath, content, 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Si déjà injecté
|
|
73
|
+
if (content.includes('SamreOverlay') || content.includes('samre_sdk.dart')) {
|
|
74
|
+
return { injected: true, alreadyInjected: true, backupPath };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let modified = content;
|
|
78
|
+
|
|
79
|
+
// 1. Ajouter l'import en haut
|
|
80
|
+
const importStatement = "import 'package:flutter/material.dart';\nimport 'samre_sdk.dart';\n";
|
|
81
|
+
if (!modified.includes('samre_sdk.dart')) {
|
|
82
|
+
modified = importStatement + modified;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Tenter d'envelopper builder ou home dans MaterialApp
|
|
86
|
+
// Exemple de pattern courant : home: QuelqueChose() -> home: SamreOverlay(child: QuelqueChose())
|
|
87
|
+
const homeRegex = /home\s*:\s*([^,\n\);]+)/;
|
|
88
|
+
if (homeRegex.test(modified) && !modified.includes('SamreOverlay')) {
|
|
89
|
+
modified = modified.replace(homeRegex, (match, p1) => {
|
|
90
|
+
return `home: SamreOverlay(child: ${p1.trim()})`;
|
|
91
|
+
});
|
|
92
|
+
fs.writeFileSync(mainPath, modified, 'utf-8');
|
|
93
|
+
return { injected: true, modifiedHome: true, backupPath };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Si injection automatique du widget home n'a pas pu matcher le pattern exact,
|
|
97
|
+
// on sauvegarde le fichier avec l'import et on informe le dev
|
|
98
|
+
fs.writeFileSync(mainPath, modified, 'utf-8');
|
|
99
|
+
return { injected: false, importAdded: true, backupPath };
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
removeSdk(cwd = process.cwd()) {
|
|
103
|
+
const libDir = path.join(cwd, 'lib');
|
|
104
|
+
const sdkPath = path.join(libDir, 'samre_sdk.dart');
|
|
105
|
+
const mainPath = path.join(libDir, 'main.dart');
|
|
106
|
+
const backupPath = path.join(libDir, 'main.dart.samre_bak');
|
|
107
|
+
|
|
108
|
+
let sdkDeleted = false;
|
|
109
|
+
let mainRestored = false;
|
|
110
|
+
|
|
111
|
+
if (fs.existsSync(sdkPath)) {
|
|
112
|
+
fs.unlinkSync(sdkPath);
|
|
113
|
+
sdkDeleted = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (fs.existsSync(backupPath)) {
|
|
117
|
+
fs.copyFileSync(backupPath, mainPath);
|
|
118
|
+
fs.unlinkSync(backupPath);
|
|
119
|
+
mainRestored = true;
|
|
120
|
+
} else if (fs.existsSync(mainPath)) {
|
|
121
|
+
// Nettoyage manuel si pas de backup
|
|
122
|
+
let content = fs.readFileSync(mainPath, 'utf-8');
|
|
123
|
+
content = content.replace(/import\s+['"]samre_sdk\.dart['"];\s*\n?/g, '');
|
|
124
|
+
content = content.replace(/SamreOverlay\s*\(\s*child\s*:\s*([^)]+)\)/g, '$1');
|
|
125
|
+
fs.writeFileSync(mainPath, content, 'utf-8');
|
|
126
|
+
mainRestored = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { sdkDeleted, mainRestored };
|
|
130
|
+
}
|
|
131
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Utilitaires de log stylisé pour le terminal avec codes de couleurs ANSI
|
|
2
|
+
|
|
3
|
+
const colors = {
|
|
4
|
+
reset: '\x1b[0m',
|
|
5
|
+
bright: '\x1b[1m',
|
|
6
|
+
dim: '\x1b[2m',
|
|
7
|
+
|
|
8
|
+
// Couleurs de texte
|
|
9
|
+
cyan: '\x1b[36m',
|
|
10
|
+
green: '\x1b[32m',
|
|
11
|
+
yellow: '\x1b[33m',
|
|
12
|
+
red: '\x1b[31m',
|
|
13
|
+
blue: '\x1b[34m',
|
|
14
|
+
magenta: '\x1b[35m',
|
|
15
|
+
white: '\x1b[37m',
|
|
16
|
+
gray: '\x1b[90m',
|
|
17
|
+
|
|
18
|
+
// Arrière-plans
|
|
19
|
+
bgBlue: '\x1b[44m',
|
|
20
|
+
bgGreen: '\x1b[42m',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const logger = {
|
|
24
|
+
banner() {
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log(` ${colors.bright}${colors.cyan}███████╗ █████╗ ███╗ ███╗██████╗ ███████╗${colors.reset}`);
|
|
27
|
+
console.log(` ${colors.bright}${colors.cyan}██╔════╝██╔══██╗████╗ ████║██╔══██╗██╔════╝${colors.reset}`);
|
|
28
|
+
console.log(` ${colors.bright}${colors.blue}███████╗███████║██╔████╔██║██████╔╝█████╗ ${colors.reset}`);
|
|
29
|
+
console.log(` ${colors.bright}${colors.blue}╚════██║██╔══██║██║╚██╔╝██║██╔══██╗██╔══╝ ${colors.reset}`);
|
|
30
|
+
console.log(` ${colors.bright}${colors.magenta}███████║██║ ██║██║ ╚═╝ ██║██║ ██║███████╗${colors.reset}`);
|
|
31
|
+
console.log(` ${colors.dim}╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ - CLI v1.0.0${colors.reset}`);
|
|
32
|
+
console.log('');
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
info(msg) {
|
|
36
|
+
console.log(` ${colors.blue}ℹ${colors.reset} ${msg}`);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
success(msg) {
|
|
40
|
+
console.log(` ${colors.green}✔${colors.reset} ${colors.bright}${colors.green}${msg}${colors.reset}`);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
warn(msg) {
|
|
44
|
+
console.log(` ${colors.yellow}⚠${colors.reset} ${colors.yellow}${msg}${colors.reset}`);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
error(msg) {
|
|
48
|
+
console.error(` ${colors.red}✖${colors.reset} ${colors.bright}${colors.red}${msg}${colors.reset}`);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
step(num, title) {
|
|
52
|
+
console.log(`\n ${colors.bgBlue}${colors.white}${colors.bright} [${num}] ${colors.reset} ${colors.bright}${title}${colors.reset}`);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
code(snippet) {
|
|
56
|
+
console.log(`\n ${colors.gray}┌──────────────────────────────────────────┐${colors.reset}`);
|
|
57
|
+
snippet.split('\n').forEach(line => {
|
|
58
|
+
console.log(` ${colors.gray}│${colors.reset} ${colors.cyan}${line}${colors.reset}`);
|
|
59
|
+
});
|
|
60
|
+
console.log(` ${colors.gray}└──────────────────────────────────────────┘${colors.reset}\n`);
|
|
61
|
+
}
|
|
62
|
+
};
|