diff --git a/backend/package.json b/backend/package.json
index 42bc4d2..a554651 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -14,6 +14,7 @@
"express": "^4.18.2",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.2",
+ "stripe": "^12.0.0",
"nodemailer": "^6.9.1",
"pg": "^8.10.0",
"pg-hstore": "^2.3.4",
diff --git a/backend/src/config.js b/backend/src/config.js
index 80d0efd..b675058 100644
--- a/backend/src/config.js
+++ b/backend/src/config.js
@@ -27,6 +27,14 @@ const config = {
reply: process.env.EMAIL_REPLY || 'noreply@2many.ca'
},
+ // Payment configuration
+ payment: {
+ stripeEnabled: process.env.STRIPE_ENABLED === 'true',
+ stripePublicKey: process.env.STRIPE_PUBLIC_KEY || '',
+ stripeSecretKey: process.env.STRIPE_SECRET_KEY || '',
+ stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET || ''
+ },
+
// Site configuration (domain and protocol based on environment)
site: {
domain: process.env.ENVIRONMENT === 'prod' ? 'rocks.2many.ca' : 'localhost:3000',
@@ -35,4 +43,52 @@ const config = {
}
};
+// This function will be called after loading settings from DB
+// to override config values with DB values
+config.updateFromDatabase = (settings) => {
+ if (!settings) return;
+
+ // Update email settings if they exist in DB
+ const emailSettings = settings.filter(s => s.category === 'email');
+ if (emailSettings.length > 0) {
+ const smtpHost = emailSettings.find(s => s.key === 'smtp_host');
+ const smtpPort = emailSettings.find(s => s.key === 'smtp_port');
+ const smtpUser = emailSettings.find(s => s.key === 'smtp_user');
+ const smtpPass = emailSettings.find(s => s.key === 'smtp_password');
+ const smtpReply = emailSettings.find(s => s.key === 'smtp_from_email');
+
+ if (smtpHost && smtpHost.value) config.email.host = smtpHost.value;
+ if (smtpPort && smtpPort.value) config.email.port = parseInt(smtpPort.value, 10);
+ if (smtpUser && smtpUser.value) config.email.user = smtpUser.value;
+ if (smtpPass && smtpPass.value) config.email.pass = smtpPass.value;
+ if (smtpReply && smtpReply.value) config.email.reply = smtpReply.value;
+ }
+
+ // Update payment settings if they exist in DB
+ const paymentSettings = settings.filter(s => s.category === 'payment');
+ if (paymentSettings.length > 0) {
+ const stripeEnabled = paymentSettings.find(s => s.key === 'stripe_enabled');
+ const stripePublic = paymentSettings.find(s => s.key === 'stripe_public_key');
+ const stripeSecret = paymentSettings.find(s => s.key === 'stripe_secret_key');
+ const stripeWebhook = paymentSettings.find(s => s.key === 'stripe_webhook_secret');
+
+ if (stripeEnabled && stripeEnabled.value) config.payment.stripeEnabled = stripeEnabled.value === 'true';
+ if (stripePublic && stripePublic.value) config.payment.stripePublicKey = stripePublic.value;
+ if (stripeSecret && stripeSecret.value) config.payment.stripeSecretKey = stripeSecret.value;
+ if (stripeWebhook && stripeWebhook.value) config.payment.stripeWebhookSecret = stripeWebhook.value;
+ }
+
+ // Update site settings if they exist in DB
+ const siteSettings = settings.filter(s => s.category === 'site');
+ if (siteSettings.length > 0) {
+ const siteDomain = siteSettings.find(s => s.key === 'site_domain');
+ const siteProtocol = siteSettings.find(s => s.key === 'site_protocol');
+ const siteApiDomain = siteSettings.find(s => s.key === 'site_api_domain');
+
+ if (siteDomain && siteDomain.value) config.site.domain = siteDomain.value;
+ if (siteProtocol && siteProtocol.value) config.site.protocol = siteProtocol.value;
+ if (siteApiDomain && siteApiDomain.value) config.site.apiDomain = siteApiDomain.value;
+ }
+};
+
module.exports = config;
\ No newline at end of file
diff --git a/backend/src/index.js b/backend/src/index.js
index 5e61b44..9346889 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -8,9 +8,11 @@ const { query, pool } = require('./db')
const authMiddleware = require('./middleware/auth');
const adminAuthMiddleware = require('./middleware/adminAuth');
const settingsAdminRoutes = require('./routes/settingsAdmin');
+const SystemSettings = require('./models/SystemSettings');
const fs = require('fs');
// routes
+const stripePaymentRoutes = require('./routes/stripePayment');
const productRoutes = require('./routes/products');
const authRoutes = require('./routes/auth');
const cartRoutes = require('./routes/cart');
@@ -76,9 +78,24 @@ const upload = multer({
});
pool.connect()
- .then(() => console.log('Connected to PostgreSQL database'))
+ .then(async () => {
+ console.log('Connected to PostgreSQL database');
+
+ // Load settings from database
+ try {
+ const settings = await SystemSettings.getAllSettings(pool, query);
+ if (settings) {
+ // Update config with database settings
+ config.updateFromDatabase(settings);
+ console.log('Loaded settings from database');
+ }
+ } catch (error) {
+ console.error('Error loading settings from database:', error);
+ }
+ })
.catch(err => console.error('Database connection error:', err));
+
// Handle SSL proxy headers
app.use((req, res, next) => {
// Trust X-Forwarded-Proto header from Cloudflare
@@ -220,7 +237,10 @@ app.delete('/api/image/product/:filename', adminAuthMiddleware(pool, query), (re
}
});
+
+
// Use routes
+app.use('/api/payment', stripePaymentRoutes(pool, query, authMiddleware(pool, query)));
app.use('/api/admin/settings', settingsAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/products', productRoutes(pool, query));
app.use('/api/auth', authRoutes(pool, query));
diff --git a/backend/src/routes/cart.js b/backend/src/routes/cart.js
index 49dfafb..552bffb 100644
--- a/backend/src/routes/cart.js
+++ b/backend/src/routes/cart.js
@@ -393,16 +393,17 @@ module.exports = (pool, query, authMiddleware) => {
}
});
- // Checkout (create order from cart)
router.post('/checkout', async (req, res, next) => {
try {
const { userId, shippingAddress } = req.body;
+
if (req.user.id !== userId) {
return res.status(403).json({
error: true,
message: 'You can only checkout your own cart'
});
}
+
// Get cart
const cartResult = await query(
'SELECT * FROM carts WHERE user_id = $1',
@@ -420,7 +421,16 @@ module.exports = (pool, query, authMiddleware) => {
// Get cart items
const cartItemsResult = await query(
- `SELECT ci.*, p.price
+ `SELECT ci.*, p.price, p.name, p.description,
+ (
+ SELECT json_build_object(
+ 'path', pi.image_path,
+ 'isPrimary', pi.is_primary
+ )
+ FROM product_images pi
+ WHERE pi.product_id = p.id AND pi.is_primary = true
+ LIMIT 1
+ ) AS primary_image
FROM cart_items ci
JOIN products p ON ci.product_id = p.id
WHERE ci.cart_id = $1`,
@@ -448,8 +458,8 @@ module.exports = (pool, query, authMiddleware) => {
// Create order
const orderId = uuidv4();
await client.query(
- 'INSERT INTO orders (id, user_id, status, total_amount, shipping_address) VALUES ($1, $2, $3, $4, $5)',
- [orderId, userId, 'pending', total, shippingAddress]
+ 'INSERT INTO orders (id, user_id, status, total_amount, shipping_address, payment_completed) VALUES ($1, $2, $3, $4, $5, $6)',
+ [orderId, userId, 'pending', total, shippingAddress, false]
);
// Create order items
@@ -458,25 +468,103 @@ module.exports = (pool, query, authMiddleware) => {
'INSERT INTO order_items (id, order_id, product_id, quantity, price_at_purchase) VALUES ($1, $2, $3, $4, $5)',
[uuidv4(), orderId, item.product_id, item.quantity, item.price]
);
-
- // Update product stock
- await client.query(
- 'UPDATE products SET stock_quantity = stock_quantity - $1 WHERE id = $2',
- [item.quantity, item.product_id]
- );
}
- // Clear cart
- await client.query(
- 'DELETE FROM cart_items WHERE cart_id = $1',
- [cartId]
- );
-
await client.query('COMMIT');
+ // Send back cart items for Stripe checkout
res.status(201).json({
success: true,
- message: 'Order created successfully',
+ message: 'Order created successfully, ready for payment',
+ orderId,
+ cartItems: cartItemsResult.rows,
+ total
+ });
+
+ // Note: We don't clear the cart here now - we'll do that after successful payment
+ } catch (error) {
+ await client.query('ROLLBACK');
+ throw error;
+ } finally {
+ client.release();
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ // New endpoint: Complete checkout after successful payment
+ router.post('/complete-checkout', async (req, res, next) => {
+ try {
+ const { userId, orderId, sessionId } = req.body;
+
+ if (req.user.id !== userId) {
+ return res.status(403).json({
+ error: true,
+ message: 'You can only complete your own checkout'
+ });
+ }
+
+ // Verify the order exists and belongs to the user
+ const orderResult = await query(
+ 'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
+ [orderId, userId]
+ );
+
+ if (orderResult.rows.length === 0) {
+ return res.status(404).json({
+ error: true,
+ message: 'Order not found'
+ });
+ }
+
+ // Begin transaction
+ const client = await pool.connect();
+
+ try {
+ await client.query('BEGIN');
+
+ // Update order status and payment info
+ await client.query(
+ 'UPDATE orders SET status = $1, payment_completed = true, payment_id = $2 WHERE id = $3',
+ ['processing', sessionId, orderId]
+ );
+
+ // Get cart
+ const cartResult = await client.query(
+ 'SELECT * FROM carts WHERE user_id = $1',
+ [userId]
+ );
+
+ if (cartResult.rows.length > 0) {
+ const cartId = cartResult.rows[0].id;
+
+ // Get cart items to update product stock
+ const cartItemsResult = await client.query(
+ 'SELECT * FROM cart_items WHERE cart_id = $1',
+ [cartId]
+ );
+
+ // Update product stock
+ for (const item of cartItemsResult.rows) {
+ await client.query(
+ 'UPDATE products SET stock_quantity = stock_quantity - $1 WHERE id = $2',
+ [item.quantity, item.product_id]
+ );
+ }
+
+ // Clear cart
+ await client.query(
+ 'DELETE FROM cart_items WHERE cart_id = $1',
+ [cartId]
+ );
+ }
+
+ await client.query('COMMIT');
+
+ res.status(200).json({
+ success: true,
+ message: 'Order completed successfully',
orderId
});
} catch (error) {
diff --git a/backend/src/routes/stripePayment.js b/backend/src/routes/stripePayment.js
new file mode 100644
index 0000000..ecc9f60
--- /dev/null
+++ b/backend/src/routes/stripePayment.js
@@ -0,0 +1,198 @@
+const express = require('express');
+const router = express.Router();
+const stripe = require('stripe');
+const config = require('../config');
+
+module.exports = (pool, query, authMiddleware) => {
+ // Apply authentication middleware to all routes
+ router.use(authMiddleware);
+
+ // Initialize Stripe with the secret key from config
+ const stripeClient = stripe(config.payment?.stripeSecretKey);
+
+ // Create checkout session
+ router.post('/create-checkout-session', async (req, res, next) => {
+ try {
+ const { cartItems, shippingAddress, userId, orderId } = req.body;
+
+ if (!cartItems || cartItems.length === 0) {
+ return res.status(400).json({
+ error: true,
+ message: 'Cart items are required'
+ });
+ }
+
+ // Verify user has access to this cart
+ if (req.user.id !== userId) {
+ return res.status(403).json({
+ error: true,
+ message: 'You can only checkout your own cart'
+ });
+ }
+
+ // Format line items for Stripe
+ const lineItems = cartItems.map(item => {
+ return {
+ price_data: {
+ currency: 'usd',
+ product_data: {
+ name: item.name,
+ description: item.description ? item.description.substring(0, 500) : undefined,
+ images: item.primary_image ? [
+ `${config.site.protocol}://${config.site.apiDomain}${item.primary_image.path}`
+ ] : undefined,
+ },
+ unit_amount: Math.round(parseFloat(item.price) * 100), // Convert to cents
+ },
+ quantity: item.quantity,
+ };
+ });
+
+ // Create metadata with shipping info and order reference
+ const metadata = {
+ order_id: orderId,
+ user_id: userId,
+ shipping_address: JSON.stringify(shippingAddress),
+ };
+
+ // Create Stripe checkout session
+ const session = await stripeClient.checkout.sessions.create({
+ payment_method_types: ['card'],
+ line_items: lineItems,
+ mode: 'payment',
+ success_url: `${config.site.protocol}://${config.site.domain}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
+ cancel_url: `${config.site.protocol}://${config.site.domain}/checkout/cancel`,
+ metadata: metadata,
+ shipping_address_collection: {
+ allowed_countries: ['US', 'CA'],
+ },
+ shipping_options: [
+ {
+ shipping_rate_data: {
+ type: 'fixed_amount',
+ fixed_amount: {
+ amount: 0,
+ currency: 'usd',
+ },
+ display_name: 'Free shipping',
+ delivery_estimate: {
+ minimum: {
+ unit: 'business_day',
+ value: 5,
+ },
+ maximum: {
+ unit: 'business_day',
+ value: 7,
+ },
+ },
+ },
+ },
+ ],
+ });
+
+ // Return the session ID to the client
+ res.json({
+ success: true,
+ sessionId: session.id,
+ url: session.url
+ });
+ } catch (error) {
+ console.error('Stripe checkout error:', error);
+ next(error);
+ }
+ });
+
+ // Verify payment status
+ router.get('/session-status/:sessionId', async (req, res, next) => {
+ try {
+ const { sessionId } = req.params;
+
+ // Retrieve the session from Stripe
+ const session = await stripeClient.checkout.sessions.retrieve(sessionId);
+
+ res.json({
+ success: true,
+ status: session.payment_status,
+ metadata: session.metadata
+ });
+ } catch (error) {
+ console.error('Error checking session status:', error);
+ next(error);
+ }
+ });
+
+ // Webhook to handle events from Stripe
+ router.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
+ // This needs to be called with raw body data
+ const payload = req.body;
+ const sig = req.headers['stripe-signature'];
+
+ let event;
+
+ try {
+ // Verify the webhook signature
+ const webhookSecret = config.payment?.stripeWebhookSecret;
+ if (!webhookSecret) {
+ throw new Error('Stripe webhook secret is not configured');
+ }
+
+ event = stripeClient.webhooks.constructEvent(payload, sig, webhookSecret);
+
+ // Handle the event
+ switch (event.type) {
+ case 'checkout.session.completed':
+ const session = event.data.object;
+
+ // Check if payment was successful
+ if (session.payment_status === 'paid') {
+ // Get metadata
+ const { order_id, user_id } = session.metadata;
+
+ if (order_id) {
+ // Update order status in database
+ await query(
+ 'UPDATE orders SET status = $1, payment_completed = true, payment_id = $2 WHERE id = $3',
+ ['processing', session.id, order_id]
+ );
+
+ console.log(`Payment completed for order ${order_id}`);
+ }
+ }
+ break;
+
+ case 'payment_intent.payment_failed':
+ const paymentIntent = event.data.object;
+ console.log(`Payment failed: ${paymentIntent.last_payment_error?.message}`);
+
+ // Handle failed payment
+ if (paymentIntent.metadata?.order_id) {
+ await query(
+ 'UPDATE orders SET status = $1, payment_notes = $2 WHERE id = $3',
+ ['payment_failed', 'Payment attempt failed', paymentIntent.metadata.order_id]
+ );
+ }
+ break;
+
+ default:
+ console.log(`Unhandled event type ${event.type}`);
+ }
+
+ // Return a 200 success response
+ res.status(200).send();
+ } catch (err) {
+ console.error(`Webhook Error: ${err.message}`);
+ return res.status(400).send(`Webhook Error: ${err.message}`);
+ }
+ });
+ router.get('/config', async (req, res, next) => {
+ try {
+ res.json({
+ stripePublicKey: config.payment?.stripePublicKey || '',
+ stripeEnabled: config.payment?.stripeEnabled || false
+ });
+ } catch (error) {
+ next(error);
+ }
+ });
+ return router;
+};
\ No newline at end of file
diff --git a/db/init/09-system-settings.sql b/db/init/09-system-settings.sql
index 9730e73..fe6e2cc 100644
--- a/db/init/09-system-settings.sql
+++ b/db/init/09-system-settings.sql
@@ -34,8 +34,6 @@ VALUES
('site_environment', NULL, 'site'),
-- Payment Settings
- ('stripe_public_key', NULL, 'payment'),
- ('stripe_secret_key', NULL, 'payment'),
('currency', 'CAD', 'payment'),
('tax_rate', '0', 'payment'),
diff --git a/db/init/10-payment.sql b/db/init/10-payment.sql
new file mode 100644
index 0000000..747c02c
--- /dev/null
+++ b/db/init/10-payment.sql
@@ -0,0 +1,22 @@
+-- Add payment related columns to the orders table
+ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_completed BOOLEAN DEFAULT FALSE;
+ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_id VARCHAR(255);
+ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_method VARCHAR(50);
+ALTER TABLE orders ADD COLUMN IF NOT EXISTS payment_notes TEXT;
+
+-- Add Stripe settings if they don't exist
+INSERT INTO system_settings (key, value, category)
+VALUES ('stripe_public_key', '', 'payment')
+ON CONFLICT (key) DO NOTHING;
+
+INSERT INTO system_settings (key, value, category)
+VALUES ('stripe_secret_key', '', 'payment')
+ON CONFLICT (key) DO NOTHING;
+
+INSERT INTO system_settings (key, value, category)
+VALUES ('stripe_webhook_secret', '', 'payment')
+ON CONFLICT (key) DO NOTHING;
+
+INSERT INTO system_settings (key, value, category)
+VALUES ('stripe_enabled', 'false', 'payment')
+ON CONFLICT (key) DO NOTHING;
\ No newline at end of file
diff --git a/fileStructure.txt b/fileStructure.txt
index b9272c4..d058992 100644
--- a/fileStructure.txt
+++ b/fileStructure.txt
@@ -1,96 +1,117 @@
-backend/
-├── public/
-├── node_modules/
-├── src/
-│ ├── routes/
-│ │ ├── auth.js
-│ │ ├── products.js
-│ │ ├── productAdminImages.js
-│ │ ├── images.js
-│ │ ├── productAdmin.js
-│ │ └── cart.js
-│ ├── middleware/
-│ │ ├── upload.js
-│ │ ├── auth.js
-│ │ └── adminAuth.js
-│ ├── db/
-│ │ └── index.js
-│ ├── index.js
-│ └── config.js
-│ └── Dockerfile
-├── package.json
-├── .env
-├── README.md
-└── .gitignore
-db/
-├── init/
-│ ├── 05-admin-role.sql
-│ ├── 02-seed.sql
-│ ├── 04-product-images.sql
-│ ├── 03-api-key.sql
-│ ├── 01-schema.sql
-frontend/
-├── node_modules/
-├── public/
-├── src/
-│ ├── pages/
-│ │ ├── Admin/
-│ │ │ ├── ProductEditPage.jsx
-│ │ │ ├── DashboardPage.jsx
-│ │ │ └── ProductsPage.jsx
-│ │ ├── ProductsPage.jsx
-│ │ ├── HomePage.jsx
-│ │ ├── CartPage.jsx
-│ │ ├── ProductDetailPage.jsx
-│ │ ├── VerifyPage.jsx
-│ │ ├── CheckoutPage.jsx
-│ │ ├── RegisterPage.jsx
-│ │ ├── NotFoundPage.jsx
-│ │ └── LoginPage.jsx
-│ ├── utils/
-│ │ └── imageUtils.js
-│ ├── components/
-│ │ ├── ImageUploader.jsx
-│ │ ├── ProductImage.jsx
-│ │ ├── Footer.jsx
-│ │ ├── ProtectedRoute.jsx
-│ │ └── Notifications.jsx
-│ ├── services/
-│ │ ├── imageService.js
-│ │ ├── productService.js
-│ │ ├── cartService.js
-│ │ ├── authService.js
-│ │ └── api.js
-│ ├── hooks/
-│ │ ├── apiHooks.js
-│ │ └── reduxHooks.js
-│ ├── layouts/
-│ │ ├── MainLayout.jsx
-│ │ ├── AuthLayout.jsx
-│ │ └── AdminLayout.jsx
-│ ├── theme/
-│ │ ├── index.js
-│ │ └── ThemeProvider.jsx
-│ ├── features/
-│ │ ├── ui/
-│ │ │ └── uiSlice.js
-│ │ ├── cart/
-│ │ │ └── cartSlice.js
-│ │ ├── auth/
-│ │ │ └── authSlice.js
-│ │ └── store/
-│ │ └── index.js
-│ ├── assets/
-│ ├── App.jsx
-│ ├── main.jsx
-│ └── config.js
-├── package.json
-├── package-lock.json
-├── vite.config.js
-├── Dockerfile
-├── nginx.conf
-├── index.html
-├── README.md
-├── .env
-├── setup-frontend.sh
-└── docker-compose.yml
\ No newline at end of file
+Rocks/
+├── .git/ # Git repository
+├── .env # Environment configuration
+├── .gitignore # Git ignore file
+├── README.md # Project documentation
+├── Dockerfile # Main Dockerfile
+├── docker-compose.yml # Docker Compose configuration
+├── nginx.conf # Nginx configuration
+├── setup-frontend.sh # Frontend setup script
+├── package.json # Project dependencies
+├── index.html # Main HTML entry point
+├── frontend/
+│ ├── node_modules/ # Node.js dependencies
+│ ├── public/ # Static public assets
+│ └── src/
+│ ├── assets/ # Static assets
+│ ├── components/
+│ │ ├── EmailDialog.jsx # Email dialog component
+│ │ ├── Footer.jsx # Footer component
+│ │ ├── ImageUploader.jsx # Image upload component
+│ │ ├── Notifications.jsx # Notifications component
+│ │ ├── ProductImage.jsx # Product image component
+│ │ └── ProtectedRoute.jsx # Auth route protection
+│ ├── features/
+│ │ ├── ui/
+│ │ │ └── uiSlice.js # UI state management
+│ │ ├── cart/
+│ │ │ └── cartSlice.js # Cart state management
+│ │ ├── auth/
+│ │ │ └── authSlice.js # Auth state management
+│ │ └── store/
+│ │ └── index.js # Redux store configuration
+│ ├── hooks/
+│ │ ├── reduxHooks.js # Redux related hooks
+│ │ ├── apiHooks.js # API related hooks
+│ │ └── settingsAdminHooks.js # Admin settings hooks
+│ ├── layouts/
+│ │ ├── AdminLayout.jsx # Admin area layout
+│ │ ├── MainLayout.jsx # Main site layout
+│ │ └── AuthLayout.jsx # Authentication layout
+│ ├── pages/
+│ │ ├── Admin/
+│ │ │ ├── DashboardPage.jsx # Admin dashboard
+│ │ │ ├── ProductsPage.jsx # Products management
+│ │ │ ├── ProductEditPage.jsx # Product editing
+│ │ │ ├── OrdersPage.jsx # Orders management
+│ │ │ ├── CategoriesPage.jsx # Categories management
+│ │ │ ├── CustomersPage.jsx # Customer management
+│ │ │ └── SettingsPage.jsx # Site settings
+│ │ ├── HomePage.jsx # Home page
+│ │ ├── ProductsPage.jsx # Products listing
+│ │ ├── ProductDetailPage.jsx # Product details
+│ │ ├── CartPage.jsx # Shopping cart
+│ │ ├── CheckoutPage.jsx # Checkout process
+│ │ ├── LoginPage.jsx # Login page
+│ │ ├── RegisterPage.jsx # Registration page
+│ │ ├── VerifyPage.jsx # Email verification
+│ │ └── NotFoundPage.jsx # 404 page
+│ ├── services/
+│ │ ├── api.js # API client
+│ │ ├── authService.js # Authentication service
+│ │ ├── cartService.js # Cart management service
+│ │ ├── productService.js # Products service
+│ │ ├── settingsAdminService.js # Settings service
+│ │ ├── adminService.js # Admin service
+│ │ ├── categoryAdminService.js # Category service
+│ │ └── imageService.js # Image handling service
+│ ├── theme/
+│ │ ├── index.js # Theme configuration
+│ │ └── ThemeProvider.jsx # Theme provider component
+│ ├── utils/
+│ │ └── imageUtils.js # Image handling utilities
+│ ├── App.jsx # Main application component
+│ ├── main.jsx # Application entry point
+│ ├── config.js # Frontend configuration
+│ └── vite.config.js # Vite bundler configuration
+├── backend/
+│ ├── node_modules/ # Node.js dependencies
+│ ├── public/
+│ │ └── uploads/
+│ │ └── products/ # Product images storage
+│ ├── src/
+│ │ ├── routes/
+│ │ │ ├── auth.js # Authentication routes
+│ │ │ ├── userAdmin.js # User administration
+│ │ │ ├── products.js # Product routes
+│ │ │ ├── productAdmin.js # Product administration
+│ │ │ ├── cart.js # Shopping cart routes
+│ │ │ ├── settingsAdmin.js # Settings administration
+│ │ │ ├── images.js # Image handling routes
+│ │ │ ├── categoryAdmin.js # Category administration
+│ │ │ └── orderAdmin.js # Order administration
+│ │ ├── middleware/
+│ │ │ ├── auth.js # Authentication middleware
+│ │ │ ├── adminAuth.js # Admin authentication
+│ │ │ └── upload.js # File upload middleware
+│ │ ├── models/
+│ │ │ └── SystemSettings.js # System settings model
+│ │ ├── db/
+│ │ │ └── index.js # Database setup
+│ │ ├── config.js # Backend configuration
+│ │ └── index.js # Server entry point
+│ ├── .env # Backend environment variables
+│ ├── Dockerfile # Backend Dockerfile
+│ └── package.json # Backend dependencies
+└── db/
+ ├── init/
+ │ ├── 01-schema.sql # Main database schema
+ │ ├── 02-seed.sql # Initial seed data
+ │ ├── 03-api-key.sql # API key setup
+ │ ├── 04-product-images.sql # Product images schema
+ │ ├── 05-admin-role.sql # Admin role definition
+ │ ├── 06-product-categories.sql # Product categories
+ │ ├── 07-user-keys.sql # User API keys
+ │ ├── 08-create-email.sql # Email templates
+ │ └── 09-system-settings.sql # System settings
+ └── test/ # Test database scripts
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 3d5e0ad..f716c70 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -14,6 +14,8 @@
"@mui/icons-material": "^5.14.19",
"@mui/material": "^5.14.19",
"@reduxjs/toolkit": "^2.0.1",
+ "@stripe/react-stripe-js": "^2.4.0",
+ "@stripe/stripe-js": "^2.2.0",
"@tanstack/react-query": "^5.12.2",
"@tanstack/react-query-devtools": "^5.12.2",
"axios": "^1.6.2",
@@ -1841,6 +1843,24 @@
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
},
+ "node_modules/@stripe/react-stripe-js": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.4.0.tgz",
+ "integrity": "sha512-1jVQEL3OuhuzNlf4OdfqovHt+MkWh8Uh8xpLxx/xUFUDdF+7/kDOrGKy+xJO3WLCfZUL7NAy+/ypwXbbYZi0tg==",
+ "dependencies": {
+ "prop-types": "^15.7.2"
+ },
+ "peerDependencies": {
+ "@stripe/stripe-js": "^1.44.1 || ^2.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@stripe/stripe-js": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.3.0.tgz",
+ "integrity": "sha512-iTwzjw1ORUR+1pH21+C/M05w+Jh5hAuE4QUei7Gnku65N7QpEaHtyVszYMYDBs6iNyLrD1tfQTSrjD6NkOA/ww=="
+ },
"node_modules/@tanstack/query-core": {
"version": "5.74.4",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.74.4.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 031594b..fc40a51 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,6 +16,8 @@
"@mui/icons-material": "^5.14.19",
"@mui/material": "^5.14.19",
"@reduxjs/toolkit": "^2.0.1",
+ "@stripe/react-stripe-js": "^2.4.0",
+ "@stripe/stripe-js": "^2.2.0",
"@tanstack/react-query": "^5.12.2",
"@tanstack/react-query-devtools": "^5.12.2",
"axios": "^1.6.2",
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index c82ca31..d8ffacd 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -3,6 +3,7 @@ import { Suspense, lazy } from 'react';
import { CircularProgress, Box } from '@mui/material';
import Notifications from './components/Notifications';
import ProtectedRoute from './components/ProtectedRoute';
+import { StripeProvider } from './context/StripeContext';
// Layouts
import MainLayout from './layouts/MainLayout';
@@ -15,6 +16,8 @@ const ProductsPage = lazy(() => import('./pages/ProductsPage'));
const ProductDetailPage = lazy(() => import('./pages/ProductDetailPage'));
const CartPage = lazy(() => import('./pages/CartPage'));
const CheckoutPage = lazy(() => import('./pages/CheckoutPage'));
+const PaymentSuccessPage = lazy(() => import('./pages/PaymentSuccessPage'));
+const PaymentCancelPage = lazy(() => import('./pages/PaymentCancelPage'));
const LoginPage = lazy(() => import('./pages/LoginPage'));
const RegisterPage = lazy(() => import('./pages/RegisterPage'));
const VerifyPage = lazy(() => import('./pages/VerifyPage'));
@@ -26,6 +29,7 @@ const AdminCustomersPage = lazy(() => import('./pages/Admin/CustomersPage'));
const AdminOrdersPage = lazy(() => import('./pages/Admin/OrdersPage'));
const AdminSettingsPage = lazy(() => import('./pages/Admin/SettingsPage'));
const NotFoundPage = lazy(() => import('./pages/NotFoundPage'));
+
// Loading component for suspense fallback
const LoadingComponent = () => (
@@ -35,55 +39,68 @@ const LoadingComponent = () => (
function App() {
return (
- }>
-
-
- {/* Main routes with MainLayout */}
- }>
- } />
- } />
- } />
-
-
+
+ }>
+
+
+ {/* Main routes with MainLayout */}
+ }>
+ } />
+ } />
+ } />
+
+
+
+ } />
+
+
+
+ } />
+ {/* Payment success and cancel routes */}
+
+
+
+ } />
+
+
+
+ } />
+
+
+ {/* Auth routes with AuthLayout */}
+ }>
+ } />
+ } />
+
+
+ {/* Verification route - standalone page */}
+ } />
+
+ {/* Admin routes with AdminLayout - protected for admins only */}
+
+
- } />
-
-
-
- } />
-
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
- {/* Auth routes with AuthLayout */}
- }>
- } />
- } />
-
-
- {/* Verification route - standalone page */}
- } />
-
- {/* Admin routes with AdminLayout - protected for admins only */}
-
-
-
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
- {/* Catch-all route for 404s */}
- } />
-
-
+ {/* Catch-all route for 404s */}
+ } />
+
+
+
);
}
diff --git a/frontend/src/components/StripePaymentForm.jsx b/frontend/src/components/StripePaymentForm.jsx
new file mode 100644
index 0000000..4f3e284
--- /dev/null
+++ b/frontend/src/components/StripePaymentForm.jsx
@@ -0,0 +1,98 @@
+import React, { useState } from 'react';
+import {
+ PaymentElement,
+ useStripe as useStripeJs,
+ useElements
+} from '@stripe/react-stripe-js';
+import { Box, Button, CircularProgress, Alert, Typography } from '@mui/material';
+import { useStripe } from '../context/StripeContext';
+
+const StripePaymentForm = ({ orderId, onSuccess, onError }) => {
+ const stripe = useStripeJs();
+ const elements = useElements();
+ const { completeOrder } = useStripe();
+ const [isLoading, setIsLoading] = useState(false);
+ const [message, setMessage] = useState(null);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!stripe || !elements) {
+ // Stripe.js hasn't loaded yet
+ return;
+ }
+
+ setIsLoading(true);
+ setMessage(null);
+
+ try {
+ // Submit the form
+ const { error, paymentIntent } = await stripe.confirmPayment({
+ elements,
+ confirmParams: {
+ return_url: `${window.location.origin}/checkout/confirmation?order_id=${orderId}`,
+ },
+ redirect: 'if_required',
+ });
+
+ if (error) {
+ setMessage(error.message || 'An unexpected error occurred');
+ onError(error.message);
+ } else if (paymentIntent && paymentIntent.status === 'succeeded') {
+ // Call our backend to update the order status
+ await completeOrder(orderId, paymentIntent.id);
+ setMessage('Payment successful!');
+ onSuccess(paymentIntent);
+ } else {
+ setMessage('Payment processing. Please wait for confirmation.');
+ }
+ } catch (err) {
+ console.error('Payment error:', err);
+ setMessage(err.message || 'An error occurred during payment processing');
+ onError(err.message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ {message && (
+
+ {message}
+
+ )}
+
+
+ Enter your payment details
+
+
+
+
+
+
+
+
+ );
+};
+
+export default StripePaymentForm;
\ No newline at end of file
diff --git a/frontend/src/context/StripeContext.jsx b/frontend/src/context/StripeContext.jsx
new file mode 100644
index 0000000..3318aef
--- /dev/null
+++ b/frontend/src/context/StripeContext.jsx
@@ -0,0 +1,151 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { loadStripe } from '@stripe/stripe-js';
+import { Elements } from '@stripe/react-stripe-js';
+import axios from 'axios';
+import config from '../config';
+
+// Create the context
+const StripeContext = createContext();
+
+export const StripeProvider = ({ children }) => {
+ const [stripePromise, setStripePromise] = useState(null);
+ const [clientSecret, setClientSecret] = useState('');
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ // Try to load Stripe public key from environment
+ let publicKey = import.meta.env.VITE_STRIPE_PUBLIC_KEY;
+
+ // If not found, fetch from API
+ if (!publicKey) {
+ // Fetch Stripe public key from backend
+ axios.get('/api/payment/config')
+ .then(response => {
+ if (response.data.stripePublicKey) {
+ loadStripeInstance(response.data.stripePublicKey);
+ } else {
+ setError('Stripe public key not found');
+ setIsLoading(false);
+ }
+ })
+ .catch(err => {
+ console.error('Error fetching Stripe config:', err);
+ setError('Failed to load payment configuration');
+ setIsLoading(false);
+ });
+ } else {
+ loadStripeInstance(publicKey);
+ }
+ }, []);
+
+ const loadStripeInstance = (publicKey) => {
+ try {
+ const stripe = loadStripe(publicKey);
+ setStripePromise(stripe);
+ setIsLoading(false);
+ } catch (err) {
+ console.error('Error loading Stripe:', err);
+ setError('Failed to initialize payment system');
+ setIsLoading(false);
+ }
+ };
+
+ // Create a checkout session
+ const createCheckoutSession = async (cartItems, orderId, shippingAddress, userId) => {
+ try {
+ const response = await axios.post('/api/payment/create-checkout-session', {
+ cartItems,
+ orderId,
+ shippingAddress,
+ userId
+ });
+
+ return response.data;
+ } catch (err) {
+ console.error('Error creating checkout session:', err);
+ throw new Error(err.response?.data?.message || 'Failed to create checkout session');
+ }
+ };
+
+ // Check session status
+ const checkSessionStatus = async (sessionId) => {
+ try {
+ const response = await axios.get(`/api/payment/session-status/${sessionId}`);
+ return response.data;
+ } catch (err) {
+ console.error('Error checking session status:', err);
+ throw new Error(err.response?.data?.message || 'Failed to check payment status');
+ }
+ };
+
+ // Complete the order after successful payment
+ const completeOrder = async (orderId, sessionId, userId) => {
+ try {
+ const response = await axios.post('/api/cart/complete-checkout', {
+ orderId,
+ sessionId,
+ userId
+ });
+ return response.data;
+ } catch (err) {
+ console.error('Error completing order:', err);
+ throw new Error(err.response?.data?.message || 'Failed to complete order');
+ }
+ };
+
+ const value = {
+ stripePromise,
+ clientSecret,
+ isLoading,
+ error,
+ createCheckoutSession,
+ checkSessionStatus,
+ completeOrder
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useStripe = () => {
+ const context = useContext(StripeContext);
+ if (!context) {
+ throw new Error('useStripe must be used within a StripeProvider');
+ }
+ return context;
+};
+
+// Wrapper component for Elements
+export const StripeElementsProvider = ({ children, clientSecret }) => {
+ const { stripePromise } = useStripe();
+
+ const options = {
+ clientSecret,
+ appearance: {
+ theme: 'stripe',
+ variables: {
+ colorPrimary: '#6200ea',
+ colorBackground: '#ffffff',
+ colorText: '#30313d',
+ colorDanger: '#df1b41',
+ fontFamily: 'Roboto, sans-serif',
+ spacingUnit: '4px',
+ borderRadius: '4px'
+ }
+ }
+ };
+
+ if (!clientSecret) {
+ return children;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/pages/Admin/SettingsPage.jsx b/frontend/src/pages/Admin/SettingsPage.jsx
index 5587463..db37bb6 100644
--- a/frontend/src/pages/Admin/SettingsPage.jsx
+++ b/frontend/src/pages/Admin/SettingsPage.jsx
@@ -258,78 +258,175 @@ const AdminSettingsPage = () => {
+ {category === 'payment' &&
+
+
+ }
+ label={formData['stripe_enabled'] === 'true' ? 'Stripe is Enabled' : 'Stripe is Disabled'}
+ />
+
+
+
+
+
+
+
+
+ togglePasswordVisibility('stripe_secret_key')}
+ edge="end"
+ >
+ {showPasswords['stripe_secret_key'] ? : }
+
+
+ )
+ }}
+ />
+
+
+
+
+ togglePasswordVisibility('stripe_webhook_secret')}
+ edge="end"
+ >
+ {showPasswords['stripe_webhook_secret'] ? : }
+
+
+ )
+ }}
+ />
+
+
+
+
+
+ To set up Stripe integration:
+
+
+ - Create a Stripe account at stripe.com
+ - Obtain your API keys from the Stripe Dashboard
+ - Set up a webhook endpoint at:
{`${window.location.protocol}//${window.location.host}/api/payment/webhook`}
+ - Copy the webhook signing secret here
+
+
+
+ }
- {settingsData[category].map((setting) => (
-
-
-
-
-
- {setting.key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
+ {settingsData[category].map((setting) => {
+ if(setting.key.toUpperCase().includes("STRIPE"))
+ return null
+
+ return (
+
+
+
+
+
+ {setting.key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
+
+
+ handleOpenDeleteDialog(setting)}
+ >
+
+
+
+
+
+ {setting.key.includes('password') || setting.key.includes('secret') || setting.key.includes('key') ? (
+
+ togglePasswordVisibility(setting.key)}
+ edge="end"
+ >
+ {showPasswords[setting.key] ? : }
+
+
+ )
+ }}
+ />
+ ) : setting.key.includes('enabled') || setting.key.includes('active') ? (
+
+ }
+ label={formData[setting.key] === 'true' ? 'Enabled' : 'Disabled'}
+ />
+ ) : (
+
+ )}
+
+
+ Last updated: {new Date(setting.updated_at).toLocaleString()}
-
- handleOpenDeleteDialog(setting)}
- >
-
-
-
-
-
- {setting.key.includes('password') || setting.key.includes('secret') || setting.key.includes('key') ? (
-
- togglePasswordVisibility(setting.key)}
- edge="end"
- >
- {showPasswords[setting.key] ? : }
-
-
- )
- }}
- />
- ) : setting.key.includes('enabled') || setting.key.includes('active') ? (
-
- }
- label={formData[setting.key] === 'true' ? 'Enabled' : 'Disabled'}
- />
- ) : (
-
- )}
-
-
- Last updated: {new Date(setting.updated_at).toLocaleString()}
-
-
-
-
- ))}
+
+
+
+ )
+ }
+ )}
))}
diff --git a/frontend/src/pages/CheckoutPage.jsx b/frontend/src/pages/CheckoutPage.jsx
index a3ddbf3..999b80e 100644
--- a/frontend/src/pages/CheckoutPage.jsx
+++ b/frontend/src/pages/CheckoutPage.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
@@ -21,6 +21,8 @@ import {
import { useNavigate, Link as RouterLink } from 'react-router-dom';
import { useAuth, useCart } from '../hooks/reduxHooks';
import { useCheckout } from '../hooks/apiHooks';
+import { useStripe, StripeElementsProvider } from '../context/StripeContext';
+import StripePaymentForm from '../components/StripePaymentForm';
// Checkout steps
const steps = ['Shipping Address', 'Review Order', 'Payment', 'Confirmation'];
@@ -30,9 +32,14 @@ const CheckoutPage = () => {
const { user } = useAuth();
const { items, total, itemCount } = useCart();
const checkout = useCheckout();
+ const { createCheckoutSession, isLoading: isStripeLoading } = useStripe();
// State for checkout steps
const [activeStep, setActiveStep] = useState(0);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [error, setError] = useState(null);
+ const [orderId, setOrderId] = useState(null);
+ const [checkoutUrl, setCheckoutUrl] = useState(null);
// State for form data
const [formData, setFormData] = useState({
@@ -107,29 +114,61 @@ const CheckoutPage = () => {
};
// Handle place order
- const handlePlaceOrder = () => {
+ const handlePlaceOrder = async () => {
if (!user || !items || items.length === 0) {
return;
}
- // Format shipping address for API
- const shippingAddress = `${formData.firstName} ${formData.lastName}
+ setIsProcessing(true);
+ setError(null);
+
+ try {
+ // Format shipping address
+ const shippingAddress = `${formData.firstName} ${formData.lastName}
${formData.address}
${formData.city}, ${formData.state} ${formData.zipCode}
${formData.country}
${formData.email}`;
-
- checkout.mutate({
- userId: user,
- shippingAddress
- }, {
- onSuccess: () => {
- // Move to confirmation step
- setActiveStep(3);
+
+ // Call the checkout API to create the order
+ const orderResponse = await checkout.mutateAsync({
+ userId: user,
+ shippingAddress
+ });
+
+ // Store the order ID for later use
+ setOrderId(orderResponse.orderId);
+
+ // Proceed to payment step
+ setActiveStep(2);
+
+ // Create a Stripe checkout session
+ const session = await createCheckoutSession(
+ orderResponse.cartItems,
+ orderResponse.orderId,
+ shippingAddress,
+ user
+ );
+
+ // Redirect to Stripe Checkout
+ if (session.url) {
+ setCheckoutUrl(session.url);
}
- });
+ } catch (error) {
+ console.error('Checkout error:', error);
+ setError(error.message || 'An error occurred during checkout');
+ } finally {
+ setIsProcessing(false);
+ }
};
+ // Redirect to Stripe checkout when the URL is available
+ useEffect(() => {
+ if (checkoutUrl) {
+ window.location.href = checkoutUrl;
+ }
+ }, [checkoutUrl]);
+
// If no items in cart, redirect to cart page
if (!items || items.length === 0) {
return (
@@ -330,24 +369,26 @@ ${formData.email}`;
);
case 2:
- // Placeholder for payment (in a real app, this would have a payment form)
return (
-
- This is a demo application. No actual payment will be processed.
-
+ {isStripeLoading || isProcessing ? (
+
+
+
+ Preparing secure payment...
+
+
+ ) : (
+
+ You will be redirected to our secure payment processor.
+
+ )}
-
- Payment Method
-
-
-
- For this demo, we'll simulate a successful payment.
-
-
-
- Total to pay: ${total.toFixed(2)}
-
+ {error && (
+
+ {error}
+
+ )}
);
case 3:
@@ -362,7 +403,7 @@ ${formData.email}`;
- Your order number is: #{Math.floor(100000 + Math.random() * 900000)}
+ Your order number is: #{orderId?.substring(0, 8) || Math.floor(100000 + Math.random() * 900000)}
@@ -402,28 +443,28 @@ ${formData.email}`;
{getStepContent(activeStep)}
- {activeStep !== 0 && activeStep !== 3 && (
+ {activeStep !== 0 && activeStep !== 3 && !isProcessing && (
)}
- {activeStep !== 3 ? (
+ {activeStep !== 2 && activeStep !== 3 ? (
- ) : (
+ ) : activeStep === 3 ? (
- )}
+ ) : null}
diff --git a/frontend/src/pages/PaymentCancelPage.jsx b/frontend/src/pages/PaymentCancelPage.jsx
new file mode 100644
index 0000000..5c41770
--- /dev/null
+++ b/frontend/src/pages/PaymentCancelPage.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import {
+ Box,
+ Typography,
+ Paper,
+ Button,
+ Alert
+} from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+import CancelIcon from '@mui/icons-material/Cancel';
+
+const PaymentCancelPage = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+
+ Payment Cancelled
+
+
+
+ Your payment was cancelled and you have not been charged.
+
+
+
+ You can try again or choose a different payment method.
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PaymentCancelPage;
\ No newline at end of file
diff --git a/frontend/src/pages/PaymentSuccessPage.jsx b/frontend/src/pages/PaymentSuccessPage.jsx
new file mode 100644
index 0000000..c424712
--- /dev/null
+++ b/frontend/src/pages/PaymentSuccessPage.jsx
@@ -0,0 +1,147 @@
+import React, { useEffect, useState } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import {
+ Box,
+ Typography,
+ Paper,
+ Button,
+ CircularProgress,
+ Alert,
+ Divider,
+ CheckCircleOutline as CheckCircleIcon
+} from '@mui/material';
+import { useStripe } from '../context/StripeContext';
+import { useAuth } from '../hooks/reduxHooks';
+
+const PaymentSuccessPage = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { checkSessionStatus, completeOrder } = useStripe();
+ const { user } = useAuth();
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [orderDetails, setOrderDetails] = useState(null);
+
+ useEffect(() => {
+ const processPayment = async () => {
+ try {
+ // Get the session ID from the URL
+ const params = new URLSearchParams(location.search);
+ const sessionId = params.get('session_id');
+
+ if (!sessionId) {
+ setError('No session ID found. Please contact customer support.');
+ setIsLoading(false);
+ return;
+ }
+
+ // Check the session status
+ const sessionData = await checkSessionStatus(sessionId);
+
+ if (sessionData.status !== 'paid') {
+ setError('Payment not completed. Please try again or contact customer support.');
+ setIsLoading(false);
+ return;
+ }
+
+ // Get metadata from session
+ const { metadata } = sessionData;
+
+ if (!metadata || !metadata.order_id) {
+ setError('Missing order information. Please contact customer support.');
+ setIsLoading(false);
+ return;
+ }
+
+ // Complete the order in our system
+ const orderResult = await completeOrder(
+ metadata.order_id,
+ sessionId,
+ user
+ );
+
+ setOrderDetails({
+ orderId: metadata.order_id,
+ sessionId
+ });
+
+ setIsLoading(false);
+ } catch (err) {
+ console.error('Error processing payment success:', err);
+ setError(err.message || 'An error occurred while processing your payment.');
+ setIsLoading(false);
+ }
+ };
+
+ processPayment();
+ }, [location.search, checkSessionStatus, completeOrder, user]);
+
+ if (isLoading) {
+ return (
+
+
+
+ Processing your payment...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ {error}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Payment Successful!
+
+
+
+ Thank you for your purchase. Your order has been completed and a confirmation email will be sent shortly.
+
+
+
+
+
+
+ Order Reference: {orderDetails?.orderId.substring(0, 8)}...
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PaymentSuccessPage;
\ No newline at end of file