7 Commits

Author SHA1 Message Date
balvarez
7487efd9dc Merge branch 'release/1.1.1'
All checks were successful
DitesLeEnChanson/site/pipeline/tag This commit looks good
2026-01-03 21:19:58 +01:00
balvarez
460ac07f0f env 2026-01-03 21:19:43 +01:00
balvarez
fd3dcfee51 Merge tag '1.1.0' into develop
1.1.0
2026-01-03 21:12:24 +01:00
balvarez
d4583f5e50 Merge branch 'release/1.1.0'
All checks were successful
DitesLeEnChanson/site/pipeline/tag This commit looks good
2026-01-03 21:12:16 +01:00
balvarez
2dda7c334a version 1.1.0 2026-01-03 21:12:05 +01:00
balvarez
4293fa76af connexion nouvelle api 2026-01-03 21:11:17 +01:00
balvarez
40ce29753d Merge tag '1.0.0' into develop
1.0.0
2025-12-31 17:19:59 +01:00
8 changed files with 270 additions and 122 deletions

123
MIGRATION_API.md Normal file
View File

@@ -0,0 +1,123 @@
# Migration du site vers l'API Laravel
Ce document décrit la migration du projet `site` de Supabase vers l'API Laravel.
## Changements effectués
### 1. Remplacement du client Supabase
**Fichier supprimé :**
- `src/lib/supabaseClient.js`
**Fichier créé :**
- `src/lib/apiClient.js` - Client API pour communiquer avec l'API Laravel
### 2. Adaptation des pages
#### OrderPage.jsx
- **Avant :** Utilisait `supabase.functions.invoke('create-checkout-session')`
- **Après :** Utilise `api.stripe.createCheckoutSession()`
- Suppression de l'utilisation de `loadStripe` et `redirectToCheckout` car l'API retourne directement l'URL de checkout
- Simplification du code de redirection vers Stripe
#### ConfirmationPage.jsx
- **Avant :** Utilisait `supabase.functions.invoke('send-order-confirmation-email')`
- **Après :** Utilise `api.orders.confirmOrder()`
### 3. Dépendances
**Supprimé :**
- `@supabase/supabase-js` (plus nécessaire)
**Conservé :**
- `@stripe/stripe-js` (toujours utilisé pour la validation côté client si nécessaire)
## Configuration
### Variables d'environnement
Ajoutez dans votre fichier `.env` :
```env
VITE_API_URL=http://localhost:8000
```
Pour la production, utilisez :
```env
VITE_API_URL=https://api.ditesleenchanson.fr
```
## Structure de l'API Client
Le client API (`apiClient.js`) expose :
### `api.stripe.createCheckoutSession(options)`
Crée une session de checkout Stripe.
**Paramètres :**
- `priceId` (string, requis) - ID du prix Stripe
- `orderData` (object, requis) - Données de la commande
- `successUrl` (string, requis) - URL de succès
- `cancelUrl` (string, requis) - URL d'annulation
- `quantity` (number, optionnel) - Quantité (défaut: 1)
- `customerEmail` (string, optionnel) - Email du client
**Retour :**
```javascript
{
data: {
sessionId: string,
url: string // URL de redirection Stripe
},
error: null | { message: string, status: number }
}
```
### `api.orders.confirmOrder(orderData, sessionId)`
Confirme une commande et envoie les emails.
**Paramètres :**
- `orderData` (object, requis) - Données de la commande
- `sessionId` (string, requis) - ID de session Stripe
**Retour :**
```javascript
{
data: {
success: boolean,
orderId: string,
customerEmail: string,
emailSent: boolean,
customerEmailError: string | null,
adminEmailSent: boolean,
adminEmailError: string | null
},
error: null | { message: string, status: number }
}
```
## Différences avec Supabase
1. **Pas de gestion d'authentification** : Le site n'a pas besoin d'authentification, contrairement au backoffice
2. **URLs directes** : L'API retourne directement l'URL de checkout Stripe, pas besoin de `redirectToCheckout`
3. **Format de réponse** : Les réponses sont structurées différemment mais compatibles avec le code existant
## Tests
Pour tester la migration :
1. Vérifiez que `VITE_API_URL` est correctement configuré
2. Testez le processus de commande complet :
- Sélection d'un produit
- Remplissage du formulaire
- Création de la session Stripe
- Redirection vers Stripe
- Confirmation après paiement
- Réception des emails
## Notes
- Le code de redirection vers Stripe a été simplifié car l'API retourne directement l'URL
- Les données de commande sont toujours stockées dans `localStorage` pour la page de confirmation
- La structure des données reste identique pour assurer la compatibilité

View File

@@ -1,4 +1,4 @@
VITE_SUPABASE_URL=https://supabase.abpcode.fr #VITE_STRIPE_PUBLISHABLE_KEY=pk_live_51RPSGmEPL3QASpovp8Q6p8ehNMW7TzSrOaV6zvPE1OtflMFN5jChQBEj5kr84wontlLOe8uiHyJBiCduzxIZwj5A00DIEVs31n
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY2Nzc3OTU5LCJleHAiOjE5MjQ0NTc5NTl9.I-qytVb1ef6QMR8IUDePJzESO3bJAnsGE075XQ2xiaI VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RPSH1ERAUBjYKpgbz4GjZjDtI24rqfBky5SO6AwdBfZaqNmFN0zQSxx0Z1wfFKtKXIZXfx5IOQSt2ularULIsto00frDMNi03
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_51RPSGmEPL3QASpovp8Q6p8ehNMW7TzSrOaV6zvPE1OtflMFN5jChQBEj5kr84wontlLOe8uiHyJBiCduzxIZwj5A00DIEVs31n
VITE_STRIPE_PUBLISHABLE_KEY_DEV=pk_test_51RPSH1ERAUBjYKpgbz4GjZjDtI24rqfBky5SO6AwdBfZaqNmFN0zQSxx0Z1wfFKtKXIZXfx5IOQSt2ularULIsto00frDMNi03 VITE_API_URL=https://api.ditesleenchanson.fr

View File

@@ -1,7 +1,7 @@
{ {
"name": "web-app", "name": "web-app",
"type": "module", "type": "module",
"version": "1.0.0", "version": "1.1.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite --host :: --port 3000", "dev": "vite --host :: --port 3000",
@@ -24,7 +24,6 @@
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
"@stripe/stripe-js": "^3.0.0", "@stripe/stripe-js": "^3.0.0",
"@supabase/supabase-js": "^2.39.8",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"framer-motion": "^10.16.4", "framer-motion": "^10.16.4",

View File

@@ -38,14 +38,14 @@ import React from 'react';
stripePriceId: 'price_1RVTXtEPL3QASpovaWjXf30q', stripePriceId: 'price_1RVTXtEPL3QASpovaWjXf30q',
description: 'Un texte de chanson unique, écrit sur mesure pour vous.', description: 'Un texte de chanson unique, écrit sur mesure pour vous.',
imageUrl: 'https://files.dites-le-en-chanson.fr/products/texte-personalise.jpg' imageUrl: 'https://files.dites-le-en-chanson.fr/products/texte-personalise.jpg'
// }, },
// { {
// id: 'prod_SQMCFqFLaNHxx0', id: 'prod_SQMCFqFLaNHxx0',
// name: 'Test', name: 'Test',
// price: 17.90, price: 17.90,
// promotionPrice: null, promotionPrice: null,
// stripePriceId: 'price_1RVVUBERAUBjYKpgYuQyuI76', stripePriceId: 'price_1RVVUBERAUBjYKpgYuQyuI76',
// description: 'Un texte de chanson unique, écrit sur mesure pour vous.', description: 'Un texte de chanson unique, écrit sur mesure pour vous.',
// imageUrl: 'https://files.dites-le-en-chanson.fr/products/texte-personalise.jpg' // imageUrl: 'https://files.dites-le-en-chanson.fr/products/texte-personalise.jpg'
} }
]; ];

106
src/src/lib/apiClient.js Normal file
View File

@@ -0,0 +1,106 @@
/**
* Client API pour remplacer Supabase
* Communique avec l'API Laravel
*/
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
/**
* Effectue une requête HTTP vers l'API
*/
async function apiRequest(endpoint, options = {}) {
const url = `${API_URL}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
const config = {
...options,
headers,
};
if (options.body && typeof options.body === 'object') {
config.body = JSON.stringify(options.body);
}
try {
const response = await fetch(url, config);
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Erreur inconnue' }));
throw {
message: errorData.error || `Erreur HTTP ${response.status}`,
status: response.status,
data: errorData,
};
}
return await response.json();
} catch (error) {
if (error.status) {
throw error;
}
throw {
message: error.message || 'Erreur de connexion',
status: 0,
};
}
}
/**
* API client pour remplacer Supabase
*/
export const api = {
/**
* Stripe - Création de session de checkout
*/
stripe: {
/**
* Crée une session de checkout Stripe
*/
async createCheckoutSession({ priceId, orderData, successUrl, cancelUrl, quantity, customerEmail }) {
try {
const data = await apiRequest('/stripe/create-checkout-session', {
method: 'POST',
body: {
priceId,
orderData,
successUrl,
cancelUrl,
quantity,
customerEmail,
},
});
return { data, error: null };
} catch (error) {
return { data: null, error };
}
},
},
/**
* Commandes
*/
orders: {
/**
* Confirme une commande et envoie les emails
*/
async confirmOrder(orderData, sessionId) {
try {
const data = await apiRequest('/orders/confirm', {
method: 'POST',
body: {
orderData,
sessionId,
},
});
return { data, error: null };
} catch (error) {
return { data: null, error };
}
},
},
};

View File

@@ -1,6 +0,0 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseAnonKey);

View File

@@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { CheckCircle, Package, Loader2, AlertTriangle, Info } from 'lucide-react'; import { CheckCircle, Package, Loader2, AlertTriangle, Info } from 'lucide-react';
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { supabase } from '@/lib/supabaseClient'; import { api } from '@/lib/apiClient';
const ConfirmationPage = () => { const ConfirmationPage = () => {
const [orderDetails, setOrderDetails] = useState(null); const [orderDetails, setOrderDetails] = useState(null);
@@ -77,30 +77,28 @@ import React, { useEffect, useState } from 'react';
sessionStorage.setItem(processedKey, 'true'); sessionStorage.setItem(processedKey, 'true');
try { try {
const payload = { orderData: orderDataForDB, sessionId: sessionId }; const { data: functionResponse, error: functionError } = await api.orders.confirmOrder(orderDataForDB, sessionId);
const { data: functionResponse, error: functionError } = await supabase.functions.invoke('send-order-confirmation-email', {
body: JSON.stringify(payload),
});
if (functionError) { if (functionError) {
console.error("Supabase function invocation error:", functionError); console.error("API error:", functionError);
throw new Error(functionError.message || "Erreur d'invocation de la fonction Edge."); throw new Error(functionError.message || "Erreur lors de l'appel à l'API.");
} }
if (!functionResponse) { if (!functionResponse) {
console.error("No response data from edge function"); console.error("No response data from API");
throw new Error("Aucune donnée retournée par la fonction Edge."); throw new Error("Aucune donnée retournée par l'API.");
} }
if (functionResponse.orderId && functionResponse.orderId !== 'N/A') { if (functionResponse.orderId && functionResponse.orderId !== 'N/A') {
const orderIdShort = functionResponse.orderId.toString().substring(0, 8);
setOrderDetails(prev => ({ ...prev, id: functionResponse.orderId, customer_email: functionResponse.customerEmail || prev.customer_email })); setOrderDetails(prev => ({ ...prev, id: functionResponse.orderId, customer_email: functionResponse.customerEmail || prev.customer_email }));
toast({ toast({
title: "Paiement Réussi!", title: "Paiement Réussi!",
description: `Votre commande (ID: ${functionResponse.orderId.substring(0,8)}) est enregistrée et un email de confirmation vous a été envoyé.`, description: `Votre commande (ID: ${orderIdShort}) est enregistrée et un email de confirmation vous a été envoyé.`,
variant: "success", variant: "success",
duration: 7000, duration: 7000,
}); });
setMainStatus({ type: 'success', message: `Commande ${functionResponse.orderId.substring(0,8)} confirmée.` }); setMainStatus({ type: 'success', message: `Commande ${orderIdShort} confirmée.` });
} else { } else {
toast({ toast({
title: "Problème Enregistrement Commande", title: "Problème Enregistrement Commande",

View File

@@ -14,13 +14,9 @@ import {
initialFormData, initialFormData,
stepsConfig, stepsConfig,
formValidations, formValidations,
products, products
STRIPE_PUBLISHABLE_KEY
} from '@/config/orderFormConfig'; } from '@/config/orderFormConfig';
import { loadStripe } from '@stripe/stripe-js'; import { api } from '@/lib/apiClient';
import { supabase } from '@/lib/supabaseClient';
const stripePromise = loadStripe(STRIPE_PUBLISHABLE_KEY);
const OrderPage = () => { const OrderPage = () => {
const [ currentStep, setCurrentStep ] = useState(0); const [ currentStep, setCurrentStep ] = useState(0);
@@ -136,12 +132,6 @@ const OrderPage = () => {
}); });
try { try {
const stripe = await stripePromise;
if (!stripe) {
throw new Error("Stripe n'a pas pu être initialisé. Vérifiez votre clé publique.");
}
const selectedProductDetails = products.find(p => p.id === formData.selectedProduct); const selectedProductDetails = products.find(p => p.id === formData.selectedProduct);
if (!selectedProductDetails || !selectedProductDetails.stripePriceId || selectedProductDetails.stripePriceId.includes('YOUR_')) { if (!selectedProductDetails || !selectedProductDetails.stripePriceId || selectedProductDetails.stripePriceId.includes('YOUR_')) {
@@ -184,17 +174,16 @@ const OrderPage = () => {
localStorage.setItem('confirmedOrderDataForConfirmationPage', JSON.stringify(finalFormDataForConfirmation)); localStorage.setItem('confirmedOrderDataForConfirmationPage', JSON.stringify(finalFormDataForConfirmation));
localStorage.setItem('orderDataForDB', JSON.stringify(orderDataForDB)); localStorage.setItem('orderDataForDB', JSON.stringify(orderDataForDB));
// Créer la session Checkout via une fonction Supabase Edge // Créer la session Checkout via l'API Laravel
console.log("Création de la session Checkout Stripe..."); console.log("Création de la session Checkout Stripe...");
const {data: sessionData, error: sessionError} = await supabase.functions.invoke('create-checkout-session', { const {data: sessionData, error: sessionError} = await api.stripe.createCheckoutSession({
body: JSON.stringify({ priceId: selectedProductDetails.stripePriceId,
priceId: selectedProductDetails.stripePriceId, orderData: orderDataForDB,
quantity: 1, quantity: 1,
customerEmail: formData.email, customerEmail: formData.email,
successUrl: `${window.location.origin}/confirmation?session_id={CHECKOUT_SESSION_ID}`, successUrl: `${window.location.origin}/confirmation?session_id={CHECKOUT_SESSION_ID}`,
cancelUrl: `${window.location.origin}/commander`, cancelUrl: `${window.location.origin}/commander`,
}),
}); });
if (sessionError) { if (sessionError) {
@@ -211,16 +200,16 @@ const OrderPage = () => {
} }
// Vérifier la structure de la réponse // Vérifier la structure de la réponse
console.log("Réponse complète de la fonction Edge:", sessionData); console.log("Réponse complète de l'API:", sessionData);
// La réponse peut être directement sessionId ou dans un objet // La réponse contient sessionId et url
const sessionId = sessionData?.sessionId || sessionData?.id || sessionData; const checkoutUrl = sessionData?.url;
if (!sessionId || typeof sessionId !== 'string') { if (!checkoutUrl) {
console.error("Aucune session ID valide retournée:", sessionData); console.error("Aucune URL de checkout retournée:", sessionData);
toast({ toast({
title: "Erreur de paiement", title: "Erreur de paiement",
description: "La session de paiement n'a pas pu être créée. Veuillez réessayer.", description: "L'URL de paiement n'a pas pu être générée. Veuillez réessayer.",
variant: "destructive", variant: "destructive",
}); });
localStorage.removeItem('confirmedOrderDataForConfirmationPage'); localStorage.removeItem('confirmedOrderDataForConfirmationPage');
@@ -229,70 +218,9 @@ const OrderPage = () => {
return; return;
} }
// Rediriger vers Stripe avec le sessionId // Rediriger vers Stripe avec l'URL de checkout
console.log("Redirection vers Stripe avec sessionId:", sessionId); console.log("Redirection vers l'URL de checkout:", checkoutUrl);
window.location.href = checkoutUrl;
// Vérifier si on a une URL de checkout dans la réponse
const checkoutUrl = sessionData?.url || sessionData?.checkoutUrl;
if (checkoutUrl) {
// Si on a l'URL directement, l'utiliser
console.log("Redirection vers l'URL de checkout fournie:", checkoutUrl);
window.location.href = checkoutUrl;
return;
}
// Sinon, utiliser redirectToCheckout de Stripe.js
try {
// Mémoriser l'URL actuelle pour détecter si la redirection a eu lieu
const currentUrl = window.location.href;
const {error: redirectError} = await stripe.redirectToCheckout({
sessionId: sessionId,
});
if (redirectError) {
console.error("Erreur Stripe lors de la redirection:", redirectError);
toast({
title: "Erreur de paiement",
description: redirectError.message || "Une erreur est survenue lors de la redirection vers Stripe.",
variant: "destructive",
});
localStorage.removeItem('confirmedOrderDataForConfirmationPage');
localStorage.removeItem('orderDataForDB');
setIsProcessingPayment(false);
} else {
// La redirection devrait se produire
console.log("Redirection initiée avec succès via redirectToCheckout");
// Vérifier après un court délai si la redirection a réellement eu lieu
// (fallback si redirectToCheckout ne fonctionne pas silencieusement)
setTimeout(() => {
if (window.location.href === currentUrl && document.visibilityState === 'visible') {
console.warn("La redirection automatique n'a pas fonctionné, tentative alternative...");
// Essayer une redirection manuelle via l'API Stripe
// Note: Cette approche nécessite que la fonction Edge retourne l'URL de checkout
// Pour l'instant, on affiche un message à l'utilisateur
toast({
title: "Redirection en cours...",
description: "Si la redirection ne fonctionne pas, veuillez cliquer sur le lien de paiement.",
variant: "default",
});
setIsProcessingPayment(false);
}
}, 1500);
}
} catch (redirectErr) {
console.error("Exception lors de la redirection:", redirectErr);
toast({
title: "Erreur de paiement",
description: redirectErr.message || "Une erreur est survenue lors de la redirection vers Stripe.",
variant: "destructive",
});
localStorage.removeItem('confirmedOrderDataForConfirmationPage');
localStorage.removeItem('orderDataForDB');
setIsProcessingPayment(false);
}
} catch (err) { } catch (err) {
console.error("Erreur lors de la préparation du paiement:", err); console.error("Erreur lors de la préparation du paiement:", err);
toast({ toast({