Compare commits
8 Commits
0.0.1
...
4293fa76af
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4293fa76af | ||
|
|
40ce29753d | ||
|
|
6ac9454004 | ||
|
|
f7aefd87e4 | ||
|
|
5eecbbf8a3 | ||
|
|
5732f1ed21 | ||
|
|
25d07b1ce9 | ||
|
|
0781d78724 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
.idea
|
||||||
src/dist/
|
src/dist/
|
||||||
src/node_modules/
|
src/node_modules/
|
||||||
src/package.json
|
src/package-lock.json
|
||||||
|
|||||||
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
6
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/site.iml" filepath="$PROJECT_DIR$/.idea/site.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
12
.idea/site.iml
generated
12
.idea/site.iml
generated
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
123
MIGRATION_API.md
Normal file
123
MIGRATION_API.md
Normal 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é
|
||||||
|
|
||||||
6
src/.env
6
src/.env
@@ -1,4 +1,4 @@
|
|||||||
VITE_SUPABASE_URL=https://supabase.abpcode.fr
|
|
||||||
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY2Nzc3OTU5LCJleHAiOjE5MjQ0NTc5NTl9.I-qytVb1ef6QMR8IUDePJzESO3bJAnsGE075XQ2xiaI
|
|
||||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_51RPSGmEPL3QASpovp8Q6p8ehNMW7TzSrOaV6zvPE1OtflMFN5jChQBEj5kr84wontlLOe8uiHyJBiCduzxIZwj5A00DIEVs31n
|
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_51RPSGmEPL3QASpovp8Q6p8ehNMW7TzSrOaV6zvPE1OtflMFN5jChQBEj5kr84wontlLOe8uiHyJBiCduzxIZwj5A00DIEVs31n
|
||||||
VITE_STRIPE_PUBLISHABLE_KEY_DEV=pk_test_51RPSH1ERAUBjYKpgbz4GjZjDtI24rqfBky5SO6AwdBfZaqNmFN0zQSxx0Z1wfFKtKXIZXfx5IOQSt2ularULIsto00frDMNi03
|
# VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RPSH1ERAUBjYKpgbz4GjZjDtI24rqfBky5SO6AwdBfZaqNmFN0zQSxx0Z1wfFKtKXIZXfx5IOQSt2ularULIsto00frDMNi03
|
||||||
|
|
||||||
|
VITE_API_URL=https://api.ditesleenchanson.fr
|
||||||
|
|||||||
50
src/package.json
Normal file
50
src/package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "web-app",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host :: --port 3000",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview --host :: --port 3000"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/is-prop-valid": "^1.2.1",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-avatar": "^1.0.3",
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
|
"@radix-ui/react-slider": "^1.1.2",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
|
"@stripe/stripe-js": "^3.0.0",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"framer-motion": "^10.16.4",
|
||||||
|
"lucide-react": "^0.292.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.16.0",
|
||||||
|
"tailwind-merge": "^1.14.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.8.3",
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"terser": "^5.39.0",
|
||||||
|
"vite": "^4.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ShoppingBag, Edit2, Music, Send } from 'lucide-react';
|
import { ShoppingBag, Edit2, Music, Send } from 'lucide-react';
|
||||||
export const STRIPE_PUBLISHABLE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY_DEV;
|
export const STRIPE_PUBLISHABLE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||||
|
|
||||||
export const products = [
|
export const products = [
|
||||||
{
|
{
|
||||||
@@ -46,7 +46,7 @@ import React from 'react';
|
|||||||
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
106
src/src/lib/apiClient.js
Normal 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 };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
@@ -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);
|
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
// 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;
|
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({
|
||||||
|
|||||||
163
supabase/functions/create-checkout-session/index.js
Normal file
163
supabase/functions/create-checkout-session/index.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
// Edge Function: create-checkout-session
|
||||||
|
// Environnement: Deno (Supabase Edge Functions self-host)
|
||||||
|
// Secrets/vars à définir (ex: volumes/functions/.env ou env docker):
|
||||||
|
// STRIPE_SECRET_KEY=sk_live_...
|
||||||
|
// SUPABASE_URL=https://supabase.abcdcode.fr (ou ce que tu utilises)
|
||||||
|
// SUPABASE_SERVICE_ROLE_KEY=xxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||||
|
import Stripe from "https://esm.sh/stripe@12.0.0";
|
||||||
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||||
|
|
||||||
|
const STRIPE_SECRET_KEY = Deno.env.get("STRIPE_SECRET_KEY") ?? "";
|
||||||
|
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
|
||||||
|
const SUPABASE_SERVICE_ROLE_KEY =
|
||||||
|
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
|
||||||
|
|
||||||
|
if (!STRIPE_SECRET_KEY) {
|
||||||
|
console.warn("⚠️ STRIPE_SECRET_KEY is not set – Stripe calls will fail.");
|
||||||
|
}
|
||||||
|
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
|
||||||
|
console.warn(
|
||||||
|
"⚠️ SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY is not set – database writes may fail.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||||
|
apiVersion: "2022-11-15",
|
||||||
|
httpClient: Stripe.createFetchHttpClient(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Headers":
|
||||||
|
"authorization, x-client-info, apikey, content-type",
|
||||||
|
};
|
||||||
|
|
||||||
|
serve(async (req) => {
|
||||||
|
// Préflight CORS
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return new Response("ok", { headers: corsHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await req.json().catch(() => null);
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: "Invalid JSON body" }),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { priceId, orderData, successUrl, cancelUrl } = body;
|
||||||
|
|
||||||
|
if (!priceId) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: "priceId is required" }),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!successUrl || !cancelUrl) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: "successUrl and cancelUrl are required",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!STRIPE_SECRET_KEY) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: "Stripe secret key is not configured on the server",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Client Supabase pour sauver la commande
|
||||||
|
const supabaseClient = createClient(
|
||||||
|
SUPABASE_URL,
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY,
|
||||||
|
{
|
||||||
|
global: {
|
||||||
|
// On forwarde l'Authorization si tu en as besoin côté RLS
|
||||||
|
headers: {
|
||||||
|
Authorization: req.headers.get("Authorization") ?? "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Insertion dans la table "orders"
|
||||||
|
const { data: order, error: orderError } = await supabaseClient
|
||||||
|
.from("orders")
|
||||||
|
.insert({
|
||||||
|
...orderData,
|
||||||
|
status: "pending_payment",
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (orderError) {
|
||||||
|
console.error("Error saving order:", orderError);
|
||||||
|
throw new Error(`Error saving order: ${orderError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Création de la session de paiement Stripe
|
||||||
|
const session = await stripe.checkout.sessions.create({
|
||||||
|
payment_method_types: ["card"],
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
price: priceId,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mode: "payment",
|
||||||
|
|
||||||
|
// ⚠️ Champs Stripe doivent être en snake_case
|
||||||
|
success_url: `${successUrl}&session_id={CHECKOUT_SESSION_ID}&order_id=${order?.id}`,
|
||||||
|
cancel_url: cancelUrl,
|
||||||
|
|
||||||
|
metadata: {
|
||||||
|
order_id: order?.id?.toString(),
|
||||||
|
product_name: orderData?.productName ?? "",
|
||||||
|
},
|
||||||
|
customer_email: orderData?.email ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ sessionId: session.id, url: session.url }),
|
||||||
|
{
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("create-checkout-session error:", error);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: error.message ?? "Unknown error" }),
|
||||||
|
{
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
status: 400,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
290
supabase/functions/send-order-confirmation-email/index.js
Normal file
290
supabase/functions/send-order-confirmation-email/index.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
// Edge Function: send-order-confirmation-email
|
||||||
|
// Self-host Supabase (Docker)
|
||||||
|
|
||||||
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||||
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||||
|
import Stripe from "https://esm.sh/stripe@12.0.0";
|
||||||
|
|
||||||
|
const RESEND_API_KEY = Deno.env.get("RESEND_API_KEY");
|
||||||
|
const STRIPE_SECRET_KEY = Deno.env.get("STRIPE_SECRET_KEY") || "";
|
||||||
|
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") || "";
|
||||||
|
const SUPABASE_SERVICE_ROLE_KEY =
|
||||||
|
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || "";
|
||||||
|
const ADMIN_EMAIL = Deno.env.get("ADMIN_EMAIL") || "";
|
||||||
|
|
||||||
|
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||||
|
apiVersion: "2022-11-15",
|
||||||
|
httpClient: Stripe.createFetchHttpClient(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Headers":
|
||||||
|
"authorization, x-client-info, apikey, content-type",
|
||||||
|
};
|
||||||
|
|
||||||
|
serve(async (req) => {
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return new Response("ok", { headers: corsHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { orderData, sessionId } = await req.json();
|
||||||
|
|
||||||
|
// 1️⃣ Récupérer l'email vérifié depuis la session Stripe
|
||||||
|
let customerEmail = null;
|
||||||
|
|
||||||
|
if (sessionId && STRIPE_SECRET_KEY) {
|
||||||
|
try {
|
||||||
|
const session = await stripe.checkout.sessions.retrieve(sessionId);
|
||||||
|
|
||||||
|
// Priorité : customer_details.email puis customer_email
|
||||||
|
customerEmail =
|
||||||
|
session?.customer_details?.email || session?.customer_email;
|
||||||
|
} catch (stripeError) {
|
||||||
|
console.error("Error retrieving Stripe session:", stripeError);
|
||||||
|
// Fallback : email envoyé par le front
|
||||||
|
customerEmail = orderData?.email;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sans sessionId (tests par ex.)
|
||||||
|
customerEmail = orderData?.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customerEmail) {
|
||||||
|
console.warn("No email found in Stripe session or order data.");
|
||||||
|
// On continue quand même : la commande sera enregistrée mais sans email
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2️⃣ Client Supabase (SERVICE ROLE pour pouvoir écrire dans la table)
|
||||||
|
const supabaseClient = createClient(
|
||||||
|
SUPABASE_URL,
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY,
|
||||||
|
{
|
||||||
|
global: {
|
||||||
|
headers: {
|
||||||
|
Authorization: req.headers.get("Authorization") ?? "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3️⃣ Enregistrer la commande dans Supabase
|
||||||
|
const finalOrderData = {
|
||||||
|
...orderData,
|
||||||
|
customer_email: customerEmail,
|
||||||
|
session_id: sessionId,
|
||||||
|
status: "En attente de traitement",
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: order, error: orderError } = await supabaseClient
|
||||||
|
.from("orders")
|
||||||
|
.insert(finalOrderData)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (orderError) {
|
||||||
|
console.error("Error saving order to Supabase:", orderError);
|
||||||
|
// on continue quand même pour tenter les mails
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4️⃣ Email client (joli template violet)
|
||||||
|
let customerEmailError = null;
|
||||||
|
let customerEmailResult = null;
|
||||||
|
|
||||||
|
if (RESEND_API_KEY && customerEmail) {
|
||||||
|
const res = await fetch("https://api.resend.com/emails", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${RESEND_API_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
from: "Dites le en chanson <contact@dites-le-en-chanson.fr>",
|
||||||
|
to: [customerEmail],
|
||||||
|
subject: `Votre commande Dites le en chanson est confirmée ! (ID: ${order?.id?.slice(0, 8) ?? ""})`,
|
||||||
|
html: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Confirmation de votre commande - Dites le en chanson</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin:0; padding:0; background:#f4f4f9; color:#333; }
|
||||||
|
.container { max-width:600px; margin:20px auto; background:#fff; border-radius:8px; box-shadow:0 4px 15px rgba(0,0,0,.1); overflow:hidden; }
|
||||||
|
.header { background:linear-gradient(135deg,#6B46C1,#9F7AEA); color:#fff; padding:30px 20px; text-align:center; }
|
||||||
|
.header img { max-width:100px; margin-bottom:15px; }
|
||||||
|
.header h1 { margin:0; font-size:28px; font-weight:600; }
|
||||||
|
.content { padding:30px; line-height:1.6; }
|
||||||
|
.content h2 { color:#6B46C1; font-size:22px; margin-top:0; }
|
||||||
|
.content p { margin-bottom:15px; }
|
||||||
|
.order-details { background:#f9fafb; padding:20px; border-radius:6px; margin-bottom:20px; border:1px solid #e5e7eb; }
|
||||||
|
.order-details p { margin:5px 0; }
|
||||||
|
.order-details strong { color:#555; }
|
||||||
|
.delivery-info { background:#eef2ff; padding:15px; border-radius:6px; text-align:center; margin-bottom:25px; border-left:4px solid #6366f1; }
|
||||||
|
.delivery-info p { margin:0; color:#4338ca; }
|
||||||
|
.button { display:inline-block; background:#6B46C1; color:#fff; padding:12px 25px; text-decoration:none; border-radius:5px; font-size:1em; font-weight:500; }
|
||||||
|
.button:hover { background:#553C9A; }
|
||||||
|
.footer { text-align:center; padding:20px; font-size:.9em; color:#777; background:#f1f1f1; }
|
||||||
|
.footer a { color:#6B46C1; text-decoration:none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<img src="https://storage.googleapis.com/hostinger-horizons-assets-prod/72f15596-7338-40f3-8565-8548388d2677/4ac040560780878558644b6783d4f976.png" alt="Dites-le en chanson Logo" />
|
||||||
|
<h1>Merci pour votre commande !</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<h2>Bonjour ${orderData?.recipient_name ?? "cher client"},</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Nous sommes ravis de vous confirmer que votre commande a bien été enregistrée.
|
||||||
|
Notre équipe de créateurs est déjà prête à composer votre chanson personnalisée !
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="order-details">
|
||||||
|
<h3>Récapitulatif de votre commande (ID: ${order?.id?.slice(0,8) ?? ""}) :</h3>
|
||||||
|
|
||||||
|
<p><strong>Produit:</strong> ${orderData?.product_name ?? "Chanson personnalisée"}</p>
|
||||||
|
<p><strong>Prix Payé:</strong> ${orderData?.price ? orderData.price + " €" : "—"}</p>
|
||||||
|
<p><strong>Pour:</strong> ${orderData?.recipient_name ?? "—"}</p>
|
||||||
|
<p><strong>Langue:</strong> ${orderData?.language ?? "—"}</p>
|
||||||
|
<p><strong>Voix:</strong> ${orderData?.voice_gender ?? "—"}</p>
|
||||||
|
<p><strong>Style:</strong> ${orderData?.musical_style ?? "—"}</p>
|
||||||
|
<p><strong>Ambiance:</strong> ${orderData?.mood ?? "—"}</p>
|
||||||
|
|
||||||
|
${orderData?.anecdote1 ? `<p><strong>Anecdote 1:</strong> ${orderData.anecdote1}</p>` : ""}
|
||||||
|
${orderData?.anecdote2 ? `<p><strong>Anecdote 2:</strong> ${orderData.anecdote2}</p>` : ""}
|
||||||
|
${orderData?.anecdote3 ? `<p><strong>Anecdote 3:</strong> ${orderData.anecdote3}</p>` : ""}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="delivery-info">
|
||||||
|
<p><strong>Délai de création :</strong> Votre chanson unique sera prête et vous sera livrée par email dans les <strong>48 heures</strong>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Nous mettons tout notre cœur pour transformer vos histoires en mélodies inoubliables.</p>
|
||||||
|
<p>Si vous avez la moindre question, n'hésitez pas à nous contacter.</p>
|
||||||
|
|
||||||
|
<p style="text-align:center; margin-top:30px;color: white !important;">
|
||||||
|
<a href="https://dites-le-en-chanson.fr" class="button">Visiter notre site</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>© ${new Date().getFullYear()} <a href="https://dites-le-en-chanson.fr">Dites-le en chanson</a>. Tous droits réservés.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("Resend API Error (Customer):", data);
|
||||||
|
customerEmailError = data.message || "Unknown Resend Error";
|
||||||
|
} else {
|
||||||
|
customerEmailResult = data;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!RESEND_API_KEY) console.error("RESEND_API_KEY is missing");
|
||||||
|
if (!customerEmail) console.error("No customer email available to send confirmation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5️⃣ Email admin "Nouvelle Commande Reçue"
|
||||||
|
let adminEmailError = null;
|
||||||
|
let adminEmailResult = null;
|
||||||
|
|
||||||
|
if (RESEND_API_KEY && ADMIN_EMAIL) {
|
||||||
|
const adminRes = await fetch("https://api.resend.com/emails", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${RESEND_API_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
from: "Dites le en chanson <contact@dites-le-en-chanson.fr>",
|
||||||
|
to: [ADMIN_EMAIL],
|
||||||
|
subject: `Nouvelle commande reçue (ID: ${order?.id?.slice(0,8) ?? "N/A"})`,
|
||||||
|
html: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Nouvelle Commande Reçue</title>
|
||||||
|
</head>
|
||||||
|
<body style="font-family: Arial, sans-serif; color:#333;">
|
||||||
|
<h1>Nouvelle Commande Reçue !</h1>
|
||||||
|
<p>Une nouvelle commande a été passée sur votre site.</p>
|
||||||
|
|
||||||
|
<p><strong>ID de Commande :</strong> ${order?.id ?? "N/A"}</p>
|
||||||
|
<p><strong>ID de Session Stripe :</strong> ${sessionId ?? "N/A"}</p>
|
||||||
|
<p><strong>Email du Client :</strong> ${customerEmail ?? "N/A"}</p>
|
||||||
|
<p><strong>Statut :</strong> ${finalOrderData.status}</p>
|
||||||
|
|
||||||
|
<h2>Détails de la commande :</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Recipient Name:</strong> ${orderData?.recipient_name ?? "Non spécifié"}</li>
|
||||||
|
<li><strong>Song For Whom:</strong> ${orderData?.song_for_whom ?? "Non spécifié"}</li>
|
||||||
|
<li><strong>Occasion:</strong> ${orderData?.occasion ?? "Non spécifié"}</li>
|
||||||
|
<li><strong>Language:</strong> ${orderData?.language ?? "Non spécifié"}</li>
|
||||||
|
<li><strong>Anecdote1:</strong> ${orderData?.anecdote1 ?? "Non spécifié"}</li>
|
||||||
|
<li><strong>Anecdote2:</strong> ${orderData?.anecdote2 ?? "Non spécifié"}</li>
|
||||||
|
<li><strong>Anecdote3:</strong> ${orderData?.anecdote3 ?? "Non spécifié"}</li>
|
||||||
|
<li><strong>Voice Gender:</strong> ${orderData?.voice_gender ?? "Non spécifié"}</li>
|
||||||
|
<li><strong>Musical Style:</strong> ${orderData?.musical_style ?? "Non spécifié"}</li>
|
||||||
|
<li><strong>Mood:</strong> ${orderData?.mood ?? "Non spécifié"}</li>
|
||||||
|
<li><strong>Price:</strong> ${orderData?.price ?? "Non spécifié"}</li>
|
||||||
|
<li><strong>Product Name:</strong> ${orderData?.product_name ?? "Non spécifié"}</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const adminData = await adminRes.json();
|
||||||
|
|
||||||
|
if (!adminRes.ok) {
|
||||||
|
console.error("Resend API Error (Admin):", adminData);
|
||||||
|
adminEmailError = adminData.message || "Unknown Resend Error";
|
||||||
|
} else {
|
||||||
|
adminEmailResult = adminData;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!ADMIN_EMAIL) console.error("ADMIN_EMAIL is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
orderId: order?.id || "N/A",
|
||||||
|
customerEmail,
|
||||||
|
emailSent: !!customerEmailResult,
|
||||||
|
customerEmailError,
|
||||||
|
adminEmailSent: !!adminEmailResult,
|
||||||
|
adminEmailError,
|
||||||
|
dbError: orderError ? orderError.message : null,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Function Critical Error:", error);
|
||||||
|
return new Response(JSON.stringify({ error: error.message }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user