Added visualisation
Added return and cancel URL's
This commit is contained in:
@@ -137,14 +137,22 @@ class WC_Aluxpay_Payment_Gateway extends WC_Payment_Gateway {
|
|||||||
*/
|
*/
|
||||||
private function get_payment_redirect_url($order) {
|
private function get_payment_redirect_url($order) {
|
||||||
|
|
||||||
|
// Build description without special characters OR encode it
|
||||||
|
$description = sprintf(
|
||||||
|
__('Order %s from %s', 'aluxpay-payment-gateway'),
|
||||||
|
$order->get_id(),
|
||||||
|
get_bloginfo('name')
|
||||||
|
);
|
||||||
|
|
||||||
$params = array(
|
$params = array(
|
||||||
'wc_order_id' => $order->get_id(),
|
'wc_order_id' => $order->get_id(),
|
||||||
'total' => $order->get_total(),
|
'total' => $order->get_total(),
|
||||||
'currency' => $order->get_currency(),
|
'currency' => $order->get_currency(),
|
||||||
'description' => sprintf(__('Order #%s from %s', 'aluxpay-payment-gateway'), $order->get_id(), get_bloginfo('name')),
|
'description' => $description,
|
||||||
'customer_email' => $order->get_billing_email(),
|
'customer_email' => $order->get_billing_email(),
|
||||||
'return_url' => $this->get_return_url($order),
|
'return_url' => $this->get_return_url($order),
|
||||||
'cancel_url' => wc_get_checkout_url(),
|
'cancel_url' => wc_get_checkout_url(),
|
||||||
|
'_wpnonce' => wp_create_nonce('cpg_payment_' . $order->get_id()),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add nonce for security
|
// Add nonce for security
|
||||||
|
|||||||
@@ -254,7 +254,23 @@ router.get('/order-status/:wc_order_id', async (req, res) => {
|
|||||||
total: wcOrder.order.total,
|
total: wcOrder.order.total,
|
||||||
currency: wcOrder.order.currency,
|
currency: wcOrder.order.currency,
|
||||||
payment_method: wcOrder.order.payment_method,
|
payment_method: wcOrder.order.payment_method,
|
||||||
payment_method_title: wcOrder.order.payment_method_title
|
payment_method_title: wcOrder.order.payment_method_title,
|
||||||
|
line_items: wcOrder.order.line_items.map(item => ({
|
||||||
|
name: item.name,
|
||||||
|
quantity: item.quantity,
|
||||||
|
price: item.price,
|
||||||
|
total: item.total,
|
||||||
|
image: item.image?.src || null
|
||||||
|
})),
|
||||||
|
shipping: {
|
||||||
|
total: wcOrder.order.shipping_total,
|
||||||
|
method: wcOrder.order.shipping_lines[0]?.method_title
|
||||||
|
},
|
||||||
|
billing: {
|
||||||
|
first_name: wcOrder.order.billing.first_name,
|
||||||
|
last_name: wcOrder.order.billing.last_name,
|
||||||
|
email: wcOrder.order.billing.email
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class WooCommerceService {
|
|||||||
async getOrder(orderId) {
|
async getOrder(orderId) {
|
||||||
try {
|
try {
|
||||||
const response = await this.client.get(`/orders/${orderId}`);
|
const response = await this.client.get(`/orders/${orderId}`);
|
||||||
|
|
||||||
console.log('WooCommerce Order Retrieved:', {
|
console.log('WooCommerce Order Retrieved:', {
|
||||||
id: response.data.id,
|
id: response.data.id,
|
||||||
status: response.data.status,
|
status: response.data.status,
|
||||||
|
|||||||
@@ -1,29 +1,9 @@
|
|||||||
import js from '@eslint/js'
|
import js from "@eslint/js";
|
||||||
import globals from 'globals'
|
import globals from "globals";
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import pluginReact from "eslint-plugin-react";
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import { defineConfig } from "eslint/config";
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores(['dist']),
|
{ files: ["**/*.{js,mjs,cjs,jsx}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
|
||||||
{
|
pluginReact.configs.flat.recommended,
|
||||||
files: ['**/*.{js,jsx}'],
|
]);
|
||||||
extends: [
|
|
||||||
js.configs.recommended,
|
|
||||||
reactHooks.configs['recommended-latest'],
|
|
||||||
reactRefresh.configs.vite,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
ecmaFeatures: { jsx: true },
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>my-app</title>
|
<title>ALUXPAY - Payment Gateway</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
11
frontend/jsconfig.json
Normal file
11
frontend/jsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1543
frontend/package-lock.json
generated
1543
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,14 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@chakra-ui/react": "^3.27.0",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
"@paypal/react-paypal-js": "^8.9.1",
|
"@paypal/react-paypal-js": "^8.9.1",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^7.9.2",
|
"react-router-dom": "^7.9.2",
|
||||||
"react-scripts": "^5.0.1"
|
"react-scripts": "^5.0.1"
|
||||||
},
|
},
|
||||||
@@ -23,10 +27,12 @@
|
|||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@vitejs/plugin-react": "^5.0.3",
|
"@vitejs/plugin-react": "^5.0.3",
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.36.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"vite": "^7.1.7"
|
"vite": "^7.1.7",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
"proxy": "http://localhost:5001"
|
"proxy": "http://localhost:5001"
|
||||||
}
|
}
|
||||||
|
|||||||
1
frontend/public/affordableluxurywatches-logo.svg
Normal file
1
frontend/public/affordableluxurywatches-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 33 KiB |
@@ -1,62 +0,0 @@
|
|||||||
.App {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-logo {
|
|
||||||
height: 40vmin;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.App-logo {
|
|
||||||
animation: App-logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-header {
|
|
||||||
background-color: #282c34;
|
|
||||||
padding: 20px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-link {
|
|
||||||
color: #61dafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom PayPal button styling */
|
|
||||||
.paypal-buttons {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading states */
|
|
||||||
.btn-loading {
|
|
||||||
position: relative;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-loading::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: 2px solid #ffffff;
|
|
||||||
border-radius: 50%;
|
|
||||||
border-top-color: transparent;
|
|
||||||
animation: spin 1s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
|
"use client"
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {Grid, GridItem, Button, Box, Container, Image, Flex, Center, SimpleGrid, Text} from "@chakra-ui/react"
|
||||||
|
import { LuMoon, LuSun } from "react-icons/lu";
|
||||||
|
import { useColorMode } from "@/components/ui/color-mode"
|
||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
import { PayPalScriptProvider } from '@paypal/react-paypal-js';
|
import { PayPalScriptProvider } from '@paypal/react-paypal-js';
|
||||||
import Payment from './pages/Payment.jsx';
|
import Payment from './pages/Payment.jsx';
|
||||||
import Success from './pages/Success.jsx';
|
import Success from './pages/Success.jsx';
|
||||||
import Cancel from './pages/Cancel.jsx';
|
import Cancel from './pages/Cancel.jsx';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
import logo from '/affordableluxurywatches-logo.svg';
|
||||||
|
|
||||||
// PayPal configuration
|
// PayPal configuration
|
||||||
const paypalOptions = {
|
const paypalOptions = {
|
||||||
@@ -14,10 +19,36 @@ const paypalOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { toggleColorMode, colorMode } = useColorMode()
|
||||||
return (
|
return (
|
||||||
<PayPalScriptProvider options={paypalOptions}>
|
<PayPalScriptProvider options={paypalOptions}>
|
||||||
<Router>
|
<Router>
|
||||||
<div className="App">
|
<div className="App">
|
||||||
|
<Container mt="5">
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Box
|
||||||
|
p="4"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="border.disabled"
|
||||||
|
color="fg.disabled"
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
templateColumns={{ base: "1fr", md: "1fr auto" }}
|
||||||
|
alignItems="center"
|
||||||
|
p={4}
|
||||||
|
gap={4}
|
||||||
|
>
|
||||||
|
<Image width="550px" src={logo} className="logo react" alt="React logo" />
|
||||||
|
<Center>
|
||||||
|
<Button variant="outline" onClick={toggleColorMode} >
|
||||||
|
{colorMode === "light" ? <><LuSun /> Toggle Light mode</> : <><LuMoon /> Toogle Dark mode</>}
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
</Grid>
|
||||||
|
<Flex direction="row" gap="4" justify="space-between" align="center" mb="10">
|
||||||
|
|
||||||
|
</Flex>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Payment />} />
|
<Route path="/" element={<Payment />} />
|
||||||
<Route path="/payment" element={<Payment />} />
|
<Route path="/payment" element={<Payment />} />
|
||||||
@@ -31,6 +62,12 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
} />
|
} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
<Center mt="5">
|
||||||
|
<Text textStyle="xs">{import.meta.env.VITE_REACT_APP_STORE_NAME}</Text>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
</PayPalScriptProvider>
|
</PayPalScriptProvider>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {Heading, Separator, DataList, Stack, Center, Box, Image, Badge, Text} from "@chakra-ui/react"
|
||||||
|
|
||||||
const OrderSummary = ({ orderData, loading }) => {
|
const OrderSummary = ({ orderData, loading, order }) => {
|
||||||
const formatCurrency = (amount, currency = 'USD') => {
|
const formatCurrency = (amount, currency = 'USD') => {
|
||||||
return new Intl.NumberFormat('en-US', {
|
return new Intl.NumberFormat('en-US', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
@@ -21,48 +22,83 @@ const OrderSummary = ({ orderData, loading }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Center>
|
||||||
<div className="order-summary">
|
<div className="order-summary">
|
||||||
<h5 className="mb-3">
|
<Box maxW="sm" borderWidth="1px" mb={5}>
|
||||||
<i className="fas fa-receipt me-2"></i>
|
<Box p="4" spaceY="2">
|
||||||
Order Summary
|
<Heading as="h2" fontSize="xl" fontWeight="bold" size="lg">Order Summary</Heading>
|
||||||
</h5>
|
<Box>
|
||||||
|
<DataList.Root orientation="horizontal" divideY="1px" maxW="md">
|
||||||
|
<DataList.Item pt="4">
|
||||||
|
<DataList.ItemLabel>Order #</DataList.ItemLabel>
|
||||||
|
<DataList.ItemValue>{orderData.wc_order_id}</DataList.ItemValue>
|
||||||
|
</DataList.Item>
|
||||||
|
{ order.billing !== undefined && <>
|
||||||
|
<DataList.Item pt="4">
|
||||||
|
<DataList.ItemLabel>First Name</DataList.ItemLabel>
|
||||||
|
<DataList.ItemValue>{order.billing.first_name}</DataList.ItemValue>
|
||||||
|
</DataList.Item>
|
||||||
|
<DataList.Item pt="4">
|
||||||
|
<DataList.ItemLabel>Last Name</DataList.ItemLabel>
|
||||||
|
<DataList.ItemValue>{order.billing.last_name}</DataList.ItemValue>
|
||||||
|
</DataList.Item>
|
||||||
|
<DataList.Item pt="4">
|
||||||
|
<DataList.ItemLabel>E-mail</DataList.ItemLabel>
|
||||||
|
<DataList.ItemValue>{order.billing.email}</DataList.ItemValue>
|
||||||
|
</DataList.Item>
|
||||||
|
|
||||||
<div className="order-item">
|
{/*{orderData.customer_email && (
|
||||||
<span>Order #</span>
|
<DataList.Item>
|
||||||
<span className="fw-bold">{orderData.wc_order_id}</span>
|
<DataList.ItemLabel>E-mail</DataList.ItemLabel>
|
||||||
</div>
|
<DataList.ItemValue>{orderData.customer_email}</DataList.ItemValue>
|
||||||
|
</DataList.Item>
|
||||||
|
)}*/}
|
||||||
|
<DataList.Item pt="4">
|
||||||
|
<DataList.ItemLabel>
|
||||||
|
<Badge colorPalette="teal" variant="solid">
|
||||||
|
{order.shipping.method}
|
||||||
|
</Badge>
|
||||||
|
</DataList.ItemLabel>
|
||||||
|
<DataList.ItemValue>{formatCurrency(order.shipping.total, orderData.currency)}</DataList.ItemValue>
|
||||||
|
</DataList.Item>
|
||||||
|
</> }
|
||||||
|
<DataList.Item pt="4" textStyle="xl">
|
||||||
|
<DataList.ItemLabel><strong>Total Amount:</strong></DataList.ItemLabel>
|
||||||
|
<DataList.ItemValue><strong>{formatCurrency(orderData.total, orderData.currency)}</strong></DataList.ItemValue>
|
||||||
|
</DataList.Item>
|
||||||
|
</DataList.Root>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box maxW="sm" borderWidth="1px" mb={5}>
|
||||||
|
<Box p="4" spaceY="2">
|
||||||
|
<Heading as="h4" fontSize="xl" fontWeight="bold" size="lg">Ordered Items</Heading>
|
||||||
|
<Box>
|
||||||
|
{order.line_items !== undefined && order.line_items.map((item, index) => (
|
||||||
|
<Box key={index} className="order-item">
|
||||||
|
<Stack direction="row" gap={5} mb={3}>
|
||||||
|
{item.image && <Image
|
||||||
|
width="75px"
|
||||||
|
src={item.image}
|
||||||
|
alt={item.name}
|
||||||
|
/>}
|
||||||
|
<Center>
|
||||||
|
<Heading size="sm" as="h6">{item.name}</Heading>
|
||||||
|
</Center>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" gap={5} mb={2}>
|
||||||
|
<Text w="75px" textStyle="sm">Qty: {item.quantity}</Text>
|
||||||
|
<Text textStyle="sm">${item.total}</Text>
|
||||||
|
</Stack>
|
||||||
|
<Separator />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
<div className="order-item">
|
</Box>
|
||||||
<span>Description</span>
|
</Box>
|
||||||
<span>{orderData.description}</span>
|
</Box>
|
||||||
</div>
|
|
||||||
|
|
||||||
{orderData.customer_email && (
|
|
||||||
<div className="order-item">
|
|
||||||
<span>Customer Email</span>
|
|
||||||
<span>{orderData.customer_email}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="order-item">
|
|
||||||
<span>Currency</span>
|
|
||||||
<span>{orderData.currency}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="order-item order-total">
|
|
||||||
<span>Total Amount</span>
|
|
||||||
<span className="text-primary">
|
|
||||||
{formatCurrency(orderData.total, orderData.currency)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 p-2 bg-info bg-opacity-10 rounded">
|
|
||||||
<small className="text-info">
|
|
||||||
<i className="fas fa-info-circle me-1"></i>
|
|
||||||
You will be redirected to PayPal to complete this payment securely
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Center>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { PayPalButtons, usePayPalScriptReducer } from '@paypal/react-paypal-js';
|
import { PayPalButtons, usePayPalScriptReducer } from '@paypal/react-paypal-js';
|
||||||
|
import {Button, HStack, Spinner, Text, VStack, Center, Alert} from "@chakra-ui/react";
|
||||||
|
import {LuRepeat} from "react-icons/lu";
|
||||||
|
|
||||||
const PayPalButton = ({ orderData, onCreateOrder, onCaptureOrder, loading }) => {
|
const PayPalButton = ({ orderData, onCreateOrder, onCaptureOrder, loading }) => {
|
||||||
const [{ isPending }] = usePayPalScriptReducer();
|
const [{ isPending }] = usePayPalScriptReducer();
|
||||||
@@ -42,22 +44,20 @@ const PayPalButton = ({ orderData, onCreateOrder, onCaptureOrder, loading }) =>
|
|||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-4">
|
<VStack colorPalette="teal">
|
||||||
<div className="spinner-border text-primary" role="status">
|
<Spinner size="xl" />
|
||||||
<span className="visually-hidden">Loading PayPal...</span>
|
<Text>Loading PayPal...</Text>
|
||||||
</div>
|
</VStack>
|
||||||
<p className="mt-2 text-muted">Loading PayPal...</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="paypal-button-wrapper">
|
<div className="paypal-button-wrapper">
|
||||||
{paypalError && (
|
{paypalError && (
|
||||||
<div className="alert alert-danger mb-3">
|
<Alert.Root mb="5" status="warning">
|
||||||
<i className="fas fa-exclamation-triangle me-2"></i>
|
<Alert.Indicator />
|
||||||
{paypalError}
|
<Alert.Title>{paypalError}</Alert.Title>
|
||||||
</div>
|
</Alert.Root>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PayPalButtons
|
<PayPalButtons
|
||||||
@@ -76,24 +76,25 @@ const PayPalButton = ({ orderData, onCreateOrder, onCaptureOrder, loading }) =>
|
|||||||
forceReRender={[orderData.total, orderData.currency]}
|
forceReRender={[orderData.total, orderData.currency]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-3 text-center">
|
<Center>
|
||||||
<small className="text-muted d-block">
|
<Text textStyle="xs">
|
||||||
Don't have a PayPal account? You can still pay with your credit or debit card.
|
Don't have a PayPal account? You can still pay with your credit or debit card.
|
||||||
</small>
|
</Text>
|
||||||
</div>
|
</Center>
|
||||||
|
|
||||||
{/* Fallback button for when PayPal doesn't load */}
|
{/* Fallback button for when PayPal doesn't load */}
|
||||||
{!isPending && (
|
{!isPending && (
|
||||||
<div className="mt-3">
|
<Center>
|
||||||
<button
|
<HStack mt="5" mb="5">
|
||||||
className="btn btn-outline-secondary btn-sm w-100"
|
<Button
|
||||||
|
variant="outline"
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<i className="fas fa-redo me-2"></i>
|
<LuRepeat /> Reload PayPal
|
||||||
Refresh PayPal
|
</Button>
|
||||||
</button>
|
</HStack>
|
||||||
</div>
|
</Center>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
90
frontend/src/components/ui/color-mode.jsx
Normal file
90
frontend/src/components/ui/color-mode.jsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { ClientOnly, IconButton, Skeleton, Span } from '@chakra-ui/react'
|
||||||
|
import { ThemeProvider, useTheme } from 'next-themes'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import { LuMoon, LuSun } from 'react-icons/lu'
|
||||||
|
|
||||||
|
export function ColorModeProvider(props) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider attribute='class' disableTransitionOnChange {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useColorMode() {
|
||||||
|
const { resolvedTheme, setTheme, forcedTheme } = useTheme()
|
||||||
|
const colorMode = forcedTheme || resolvedTheme
|
||||||
|
const toggleColorMode = () => {
|
||||||
|
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
colorMode: colorMode,
|
||||||
|
setColorMode: setTheme,
|
||||||
|
toggleColorMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useColorModeValue(light, dark) {
|
||||||
|
const { colorMode } = useColorMode()
|
||||||
|
return colorMode === 'dark' ? dark : light
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ColorModeIcon() {
|
||||||
|
const { colorMode } = useColorMode()
|
||||||
|
return colorMode === 'dark' ? <LuMoon /> : <LuSun />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ColorModeButton = React.forwardRef(
|
||||||
|
function ColorModeButton(props, ref) {
|
||||||
|
const { toggleColorMode } = useColorMode()
|
||||||
|
return (
|
||||||
|
<ClientOnly fallback={<Skeleton boxSize='9' />}>
|
||||||
|
<IconButton
|
||||||
|
onClick={toggleColorMode}
|
||||||
|
variant='ghost'
|
||||||
|
aria-label='Toggle color mode'
|
||||||
|
size='sm'
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
css={{
|
||||||
|
_icon: {
|
||||||
|
width: '5',
|
||||||
|
height: '5',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorModeIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ClientOnly>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const LightMode = React.forwardRef(function LightMode(props, ref) {
|
||||||
|
return (
|
||||||
|
<Span
|
||||||
|
color='fg'
|
||||||
|
display='contents'
|
||||||
|
className='chakra-theme light'
|
||||||
|
colorPalette='gray'
|
||||||
|
colorScheme='light'
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DarkMode = React.forwardRef(function DarkMode(props, ref) {
|
||||||
|
return (
|
||||||
|
<Span
|
||||||
|
color='fg'
|
||||||
|
display='contents'
|
||||||
|
className='chakra-theme dark'
|
||||||
|
colorPalette='gray'
|
||||||
|
colorScheme='dark'
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
12
frontend/src/components/ui/provider.jsx
Normal file
12
frontend/src/components/ui/provider.jsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { ChakraProvider, defaultSystem } from '@chakra-ui/react'
|
||||||
|
import { ColorModeProvider } from './color-mode'
|
||||||
|
|
||||||
|
export function Provider(props) {
|
||||||
|
return (
|
||||||
|
<ChakraProvider value={defaultSystem}>
|
||||||
|
<ColorModeProvider {...props} />
|
||||||
|
</ChakraProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
frontend/src/components/ui/toaster.jsx
Normal file
43
frontend/src/components/ui/toaster.jsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Toaster as ChakraToaster,
|
||||||
|
Portal,
|
||||||
|
Spinner,
|
||||||
|
Stack,
|
||||||
|
Toast,
|
||||||
|
createToaster,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
|
||||||
|
export const toaster = createToaster({
|
||||||
|
placement: 'bottom-end',
|
||||||
|
pauseOnPageIdle: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Toaster = () => {
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
|
||||||
|
{(toast) => (
|
||||||
|
<Toast.Root width={{ md: 'sm' }}>
|
||||||
|
{toast.type === 'loading' ? (
|
||||||
|
<Spinner size='sm' color='blue.solid' />
|
||||||
|
) : (
|
||||||
|
<Toast.Indicator />
|
||||||
|
)}
|
||||||
|
<Stack gap='1' flex='1' maxWidth='100%'>
|
||||||
|
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
||||||
|
{toast.description && (
|
||||||
|
<Toast.Description>{toast.description}</Toast.Description>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
{toast.action && (
|
||||||
|
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
|
||||||
|
)}
|
||||||
|
{toast.closable && <Toast.CloseTrigger />}
|
||||||
|
</Toast.Root>
|
||||||
|
)}
|
||||||
|
</ChakraToaster>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
frontend/src/components/ui/tooltip.jsx
Normal file
35
frontend/src/components/ui/tooltip.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export const Tooltip = React.forwardRef(function Tooltip(props, ref) {
|
||||||
|
const {
|
||||||
|
showArrow,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
portalled = true,
|
||||||
|
content,
|
||||||
|
contentProps,
|
||||||
|
portalRef,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
if (disabled) return children
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraTooltip.Root {...rest}>
|
||||||
|
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ChakraTooltip.Positioner>
|
||||||
|
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
||||||
|
{showArrow && (
|
||||||
|
<ChakraTooltip.Arrow>
|
||||||
|
<ChakraTooltip.ArrowTip />
|
||||||
|
</ChakraTooltip.Arrow>
|
||||||
|
)}
|
||||||
|
{content}
|
||||||
|
</ChakraTooltip.Content>
|
||||||
|
</ChakraTooltip.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</ChakraTooltip.Root>
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Provider } from "@/components/ui/provider"
|
||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
@@ -5,6 +6,8 @@ import App from './App.jsx'
|
|||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<Provider>
|
||||||
<App />
|
<App />
|
||||||
|
</Provider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Text,
|
||||||
|
Spinner,
|
||||||
|
Center,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Button,
|
||||||
|
Accordion,
|
||||||
|
Box,
|
||||||
|
Span
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import { LuRepeat, LuMoveLeft } from "react-icons/lu";
|
||||||
|
|
||||||
const Cancel = () => {
|
const Cancel = () => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@@ -8,6 +21,8 @@ const Cancel = () => {
|
|||||||
const orderId = searchParams.get('order_id');
|
const orderId = searchParams.get('order_id');
|
||||||
const paypalToken = searchParams.get('token');
|
const paypalToken = searchParams.get('token');
|
||||||
|
|
||||||
|
const cancelUrl = searchParams.get('cancel_url');
|
||||||
|
|
||||||
const handleRetryPayment = () => {
|
const handleRetryPayment = () => {
|
||||||
// Build the payment URL with original parameters
|
// Build the payment URL with original parameters
|
||||||
const paymentUrl = new URL('/payment', window.location.origin);
|
const paymentUrl = new URL('/payment', window.location.origin);
|
||||||
@@ -21,44 +36,27 @@ const Cancel = () => {
|
|||||||
window.location.href = paymentUrl.toString();
|
window.location.href = paymentUrl.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getErrorMessage = () => {
|
|
||||||
if (error) {
|
|
||||||
return decodeURIComponent(error);
|
|
||||||
}
|
|
||||||
return 'Your payment was cancelled. You can try again or choose a different payment method.';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getIconClass = () => {
|
|
||||||
return error ? 'status-error' : 'status-warning';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getIconName = () => {
|
|
||||||
return error ? 'fas fa-times-circle' : 'fas fa-exclamation-triangle';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTitle = () => {
|
|
||||||
return error ? 'Payment Failed' : 'Payment Cancelled';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTitleClass = () => {
|
|
||||||
return error ? 'text-danger' : 'text-warning';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="payment-container">
|
<div className="payment-container">
|
||||||
<div className="payment-card">
|
<Alert.Root status="error" title="Payment Cancelled">
|
||||||
<div className="payment-body text-center">
|
<Alert.Indicator />
|
||||||
<div className={`payment-status-icon ${getIconClass()}`}>
|
<Alert.Title>Payment Cancelled</Alert.Title>
|
||||||
<i className={getIconName()}></i>
|
<Alert.Description>
|
||||||
</div>
|
<Text>Your payment was cancelled. You can try again or choose a different payment method.</Text>
|
||||||
|
</Alert.Description>
|
||||||
|
</Alert.Root>
|
||||||
|
|
||||||
<h2 className={`mb-3 ${getTitleClass()}`}>
|
{/*<div className={`payment-status-icon ${getIconClass()}`}>*/}
|
||||||
{getTitle()}
|
{/* <i className={getIconName()}></i>*/}
|
||||||
</h2>
|
{/*</div>*/}
|
||||||
|
|
||||||
<p className="lead mb-4">
|
{/*<h2 className={`mb-3 ${getTitleClass()}`}>*/}
|
||||||
{getErrorMessage()}
|
{/* {getTitle()}*/}
|
||||||
</p>
|
{/*</h2>*/}
|
||||||
|
|
||||||
|
{/*<p className="lead mb-4">*/}
|
||||||
|
{/* {getErrorMessage()}*/}
|
||||||
|
{/*</p>*/}
|
||||||
|
|
||||||
{orderId && (
|
{orderId && (
|
||||||
<div className="alert alert-info">
|
<div className="alert alert-info">
|
||||||
@@ -79,45 +77,58 @@ const Cancel = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-4">
|
<Center mt="5" mb="5" gap="5">
|
||||||
<button
|
<HStack>
|
||||||
className="btn btn-primary me-3"
|
<Button
|
||||||
|
variant="outline"
|
||||||
onClick={handleRetryPayment}
|
onClick={handleRetryPayment}
|
||||||
>
|
>
|
||||||
<i className="fas fa-redo me-2"></i>
|
<LuRepeat /> Try Payment Again
|
||||||
Try Payment Again
|
</Button>
|
||||||
</button>
|
</HStack>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={`${import.meta.env.VITE_REACT_APP_WOOCOMMERCE_URL || '#'}`}
|
href={cancelUrl}
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
>
|
>
|
||||||
<i className="fas fa-arrow-left me-2"></i>
|
<HStack>
|
||||||
Back to Store
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<LuMoveLeft /> Back to the Store
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</Center>
|
||||||
|
<Accordion.Root multiple defaultValue={["a", "b"]}>
|
||||||
|
<Accordion.Item value="a">
|
||||||
|
<Accordion.ItemTrigger>
|
||||||
|
<Span flex="1">If you're experiencing issues with payment:</Span>
|
||||||
|
<Accordion.ItemIndicator />
|
||||||
|
</Accordion.ItemTrigger>
|
||||||
|
<Accordion.ItemContent>
|
||||||
|
<Accordion.ItemBody>
|
||||||
|
<Box ml="5" as="ul" listStyleType="circle">
|
||||||
|
<li>Check your internet connection</li>
|
||||||
|
<li>Ensure your PayPal account has sufficient funds</li>
|
||||||
|
<li>Try using a different payment method</li>
|
||||||
|
<li>Contact our support team if the problem persists</li>
|
||||||
|
</Box>
|
||||||
|
</Accordion.ItemBody>
|
||||||
|
</Accordion.ItemContent>
|
||||||
|
</Accordion.Item>
|
||||||
|
<Accordion.Item value="b">
|
||||||
|
<Accordion.ItemTrigger>
|
||||||
|
<Span flex="1">Need Help?</Span>
|
||||||
|
<Accordion.ItemIndicator />
|
||||||
|
</Accordion.ItemTrigger>
|
||||||
|
<Accordion.ItemContent>
|
||||||
|
<Accordion.ItemBody>Text</Accordion.ItemBody>
|
||||||
|
</Accordion.ItemContent>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion.Root>
|
||||||
|
|
||||||
<div className="mt-4">
|
<Center>
|
||||||
<div className="alert alert-light border">
|
<Text textStyle="xs">Contact Support: {import.meta.env.VITE_REACT_APP_SUPPORT_EMAIL}</Text>
|
||||||
<h6 className="mb-3">Need Help?</h6>
|
</Center>
|
||||||
<p className="mb-2">If you're experiencing issues with payment:</p>
|
|
||||||
<ul className="list-unstyled small text-start">
|
|
||||||
<li><i className="fas fa-check text-success me-2"></i>Check your internet connection</li>
|
|
||||||
<li><i className="fas fa-check text-success me-2"></i>Ensure your PayPal account has sufficient funds</li>
|
|
||||||
<li><i className="fas fa-check text-success me-2"></i>Try using a different payment method</li>
|
|
||||||
<li><i className="fas fa-check text-success me-2"></i>Contact our support team if the problem persists</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 pt-4 border-top">
|
|
||||||
<small className="text-muted">
|
|
||||||
<i className="fas fa-headset me-1"></i>
|
|
||||||
Contact support: support@yourstore.com | 1-800-XXX-XXXX
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,18 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Heading,
|
||||||
|
DataList,
|
||||||
|
Timeline,
|
||||||
|
Text,
|
||||||
|
Spinner,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
Center,
|
||||||
|
EmptyState,
|
||||||
|
VStack, HStack
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import { LuShoppingBasket, LuPackageCheck, LuLoaderCircle, LuMail } from "react-icons/lu";
|
||||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
import OrderSummary from '../components/OrderSummary.jsx';
|
import OrderSummary from '../components/OrderSummary.jsx';
|
||||||
import PayPalButton from '../components/PayPalButton.jsx';
|
import PayPalButton from '../components/PayPalButton.jsx';
|
||||||
@@ -13,9 +27,14 @@ const Payment = () => {
|
|||||||
total: searchParams.get('total') || '25.99',
|
total: searchParams.get('total') || '25.99',
|
||||||
currency: searchParams.get('currency') || 'USD',
|
currency: searchParams.get('currency') || 'USD',
|
||||||
description: searchParams.get('description') || 'Test Order',
|
description: searchParams.get('description') || 'Test Order',
|
||||||
customer_email: searchParams.get('customer_email') || 'customer@example.com'
|
customer_email: searchParams.get('customer_email') || 'customer@example.com',
|
||||||
|
return_url: searchParams.get('return_url'),
|
||||||
|
cancel_url: searchParams.get('cancel_url')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [orderItems, setOrderItems] = useState([]);
|
||||||
|
const [order, setOrder] = useState([]);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [paypalOrderId, setPaypalOrderId] = useState('');
|
const [paypalOrderId, setPaypalOrderId] = useState('');
|
||||||
@@ -49,6 +68,25 @@ const Payment = () => {
|
|||||||
}
|
}
|
||||||
}, [orderData]);
|
}, [orderData]);
|
||||||
|
|
||||||
|
// Fetch Order
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOrderDetails = async () => {
|
||||||
|
if (orderData.wc_order_id) {
|
||||||
|
try {
|
||||||
|
const data = await paymentAPI.getOrderStatus(orderData.wc_order_id);
|
||||||
|
if (data.success && data.line_items) {
|
||||||
|
setOrderItems(data.line_items);
|
||||||
|
setOrder(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch order items:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOrderDetails();
|
||||||
|
}, [orderData.wc_order_id]);
|
||||||
|
|
||||||
// Create PayPal order
|
// Create PayPal order
|
||||||
const createPayPalOrder = async () => {
|
const createPayPalOrder = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -96,27 +134,141 @@ const Payment = () => {
|
|||||||
console.log('Payment captured:', data.transaction_id);
|
console.log('Payment captured:', data.transaction_id);
|
||||||
|
|
||||||
// Redirect to success page with transaction details
|
// Redirect to success page with transaction details
|
||||||
navigate(`/success?transaction_id=${data.transaction_id}&order_id=${orderData.wc_order_id}`);
|
navigate(`/success?transaction_id=${data.transaction_id}&order_id=${orderData.wc_order_id}&return_url=${encodeURIComponent(orderData.return_url)}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Capture payment error:', error);
|
console.error('Capture payment error:', error);
|
||||||
setError(error.message);
|
setError(error.message);
|
||||||
navigate(`/cancel?error=${encodeURIComponent(error.message)}&order_id=${orderData.wc_order_id}`);
|
navigate(`/cancel?error=${encodeURIComponent(error.message)}&order_id=${orderData.wc_order_id}&cancel_url=${encodeURIComponent(orderData.cancel_url)}`);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getOrdinalSuffix(day) {
|
||||||
|
if (day > 3 && day < 21) return "th"; // 11th–19th
|
||||||
|
switch (day % 10) {
|
||||||
|
case 1: return "st";
|
||||||
|
case 2: return "nd";
|
||||||
|
case 3: return "rd";
|
||||||
|
default: return "th";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date) {
|
||||||
|
const day = date.getDate();
|
||||||
|
const month = date.toLocaleString('default', { month: 'long' });
|
||||||
|
const year = date.getFullYear();
|
||||||
|
|
||||||
|
return `${day}${getOrdinalSuffix(day)} ${month} ${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateAfterDays(days) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setDate(date.getDate() + days);
|
||||||
|
return formatDate(date);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="payment-container">
|
<div className="payment-container">
|
||||||
|
<Heading as="h1" fontSize="2xl" textAlign="center" mb="10">Secure Payment with PayPal</Heading>
|
||||||
|
{order.length === 0 ?
|
||||||
|
<Center>
|
||||||
|
<EmptyState.Root>
|
||||||
|
<EmptyState.Content>
|
||||||
|
<EmptyState.Indicator>
|
||||||
|
<LuShoppingBasket />
|
||||||
|
</EmptyState.Indicator>
|
||||||
|
<VStack textAlign="center">
|
||||||
|
<EmptyState.Title>Your cart is empty</EmptyState.Title>
|
||||||
|
<EmptyState.Description>
|
||||||
|
Explore our products and add items to your cart
|
||||||
|
</EmptyState.Description>
|
||||||
|
</VStack>
|
||||||
|
</EmptyState.Content>
|
||||||
|
</EmptyState.Root>
|
||||||
|
</Center> :
|
||||||
|
<Grid
|
||||||
|
templateColumns={{ base: "1fr", md: "repeat(3, 1fr)" }}
|
||||||
|
templateRows="repeat(1, 1fr)"
|
||||||
|
gap="6"
|
||||||
|
>
|
||||||
|
<GridItem colSpan={2}>
|
||||||
|
<Center>
|
||||||
|
<Text mb="10">
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
{!error && backendStatus && (
|
||||||
|
<Center mb="5">
|
||||||
|
<PayPalButton
|
||||||
|
orderData={orderData}
|
||||||
|
onCreateOrder={createPayPalOrder}
|
||||||
|
onCaptureOrder={capturePayPalOrder}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
<Center>
|
||||||
|
|
||||||
|
|
||||||
|
<Timeline.Root maxW="400px">
|
||||||
|
<Timeline.Item>
|
||||||
|
<Timeline.Connector>
|
||||||
|
<Timeline.Separator />
|
||||||
|
<Timeline.Indicator>
|
||||||
|
<LuShoppingBasket />
|
||||||
|
</Timeline.Indicator>
|
||||||
|
</Timeline.Connector>
|
||||||
|
<Timeline.Content>
|
||||||
|
<Timeline.Title>Order Confirmed</Timeline.Title>
|
||||||
|
<Timeline.Description>{formatDate(new Date())}</Timeline.Description>
|
||||||
|
</Timeline.Content>
|
||||||
|
</Timeline.Item>
|
||||||
|
|
||||||
|
<Timeline.Item>
|
||||||
|
<Timeline.Connector>
|
||||||
|
<Timeline.Separator />
|
||||||
|
<Timeline.Indicator>
|
||||||
|
<Spinner size="sm"/>
|
||||||
|
</Timeline.Indicator>
|
||||||
|
</Timeline.Connector>
|
||||||
|
<Timeline.Content>
|
||||||
|
<Timeline.Title textStyle="sm">Payment in progress</Timeline.Title>
|
||||||
|
<Timeline.Description></Timeline.Description>
|
||||||
|
</Timeline.Content>
|
||||||
|
</Timeline.Item>
|
||||||
|
|
||||||
|
<Timeline.Item>
|
||||||
|
<Timeline.Connector>
|
||||||
|
<Timeline.Separator />
|
||||||
|
<Timeline.Indicator>
|
||||||
|
<LuPackageCheck />
|
||||||
|
</Timeline.Indicator>
|
||||||
|
</Timeline.Connector>
|
||||||
|
<Timeline.Content>
|
||||||
|
<Timeline.Title textStyle="sm">Estimated Delivery Date</Timeline.Title>
|
||||||
|
<Timeline.Description>{getDateAfterDays(10)}</Timeline.Description>
|
||||||
|
<Text textStyle="sm">
|
||||||
|
We will shipp your product via <strong>FedEx</strong> and it should
|
||||||
|
arrive within 7-10 business days.
|
||||||
|
</Text>
|
||||||
|
</Timeline.Content>
|
||||||
|
</Timeline.Item>
|
||||||
|
</Timeline.Root>
|
||||||
|
</Center>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem rowSpan={2} colSpan={1}>
|
||||||
|
<OrderSummary
|
||||||
|
orderData={orderData}
|
||||||
|
loading={loading}
|
||||||
|
order={order}
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
<div className="payment-card">
|
<div className="payment-card">
|
||||||
<div className="payment-header">
|
|
||||||
<h2>
|
|
||||||
<i className="fas fa-credit-card me-2"></i>
|
|
||||||
Secure Payment
|
|
||||||
</h2>
|
|
||||||
<p className="mb-0">Complete your order securely with PayPal</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="payment-body">
|
<div className="payment-body">
|
||||||
{error && (
|
{error && (
|
||||||
@@ -127,47 +279,38 @@ const Payment = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{backendStatus && (
|
{backendStatus && (
|
||||||
<div className="alert alert-success alert-dismissible fade show" role="alert">
|
<Alert.Root status="info" mb="5">
|
||||||
<i className="fas fa-check-circle me-2"></i>
|
<Alert.Indicator />
|
||||||
Payment system connected
|
<Alert.Content>
|
||||||
<button type="button" className="btn-close" data-bs-dismiss="alert"></button>
|
<Alert.Title>Payment system connected</Alert.Title>
|
||||||
</div>
|
<Alert.Description>
|
||||||
|
<Text mb="2">We're available via email and usually respond within a few hours. So don't hesitate to reach out if you need assistance - we're here to help.</Text>
|
||||||
|
<HStack>
|
||||||
|
<LuMail />
|
||||||
|
<Text>{import.meta.env.VITE_REACT_APP_SUPPORT_EMAIL}</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
</Alert.Description>
|
||||||
|
</Alert.Content>
|
||||||
|
</Alert.Root>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<OrderSummary
|
|
||||||
orderData={orderData}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!error && backendStatus && (
|
|
||||||
<div className="paypal-button-container">
|
|
||||||
<PayPalButton
|
|
||||||
orderData={orderData}
|
|
||||||
onCreateOrder={createPayPalOrder}
|
|
||||||
onCaptureOrder={capturePayPalOrder}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4 text-center">
|
|
||||||
<small className="text-muted">
|
|
||||||
<i className="fas fa-lock me-1"></i>
|
|
||||||
Your payment is secured by PayPal's advanced encryption
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Debug info for development */}
|
{/* Debug info for development */}
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{import.meta.env.VITE_NODE_ENV === 'development' && (
|
||||||
<div className="mt-4 p-3 bg-light border rounded">
|
<Alert.Root status="info" colorPalette="teal">
|
||||||
<h6>Debug Info:</h6>
|
<Alert.Indicator />
|
||||||
|
<Alert.Content>
|
||||||
|
<Alert.Title>Debug Info</Alert.Title>
|
||||||
|
<Alert.Description>
|
||||||
<small>
|
<small>
|
||||||
<strong>Order ID:</strong> {orderData.wc_order_id}<br/>
|
<strong>Order ID:</strong> {orderData.wc_order_id}<br/>
|
||||||
<strong>Total:</strong> ${orderData.total}<br/>
|
<strong>Total:</strong> ${orderData.total}<br/>
|
||||||
<strong>PayPal Order:</strong> {paypalOrderId || 'Not created yet'}<br/>
|
<strong>PayPal Order:</strong> {paypalOrderId || 'Not created yet'}<br/>
|
||||||
<strong>Backend:</strong> {backendStatus ? '✅ Connected' : '❌ Disconnected'}
|
<strong>Backend:</strong> {backendStatus ? '✅ Connected' : '❌ Disconnected'}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</Alert.Description>
|
||||||
|
</Alert.Content>
|
||||||
|
</Alert.Root>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,44 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Text,
|
||||||
|
Spinner,
|
||||||
|
Center,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Button,
|
||||||
|
DataList
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import { LuMoveLeft } from "react-icons/lu";
|
||||||
|
|
||||||
const Success = () => {
|
const Success = () => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [orderStatus, setOrderStatus] = useState(null);
|
const [orderStatus, setOrderStatus] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [countdown, setCountdown] = useState(10);
|
||||||
|
|
||||||
const transactionId = searchParams.get('transaction_id');
|
const transactionId = searchParams.get('transaction_id');
|
||||||
const orderId = searchParams.get('order_id');
|
const orderId = searchParams.get('order_id');
|
||||||
const paypalOrderId = searchParams.get('token'); // PayPal adds this
|
const paypalOrderId = searchParams.get('token'); // PayPal adds this
|
||||||
|
const returnUrl = searchParams.get('return_url');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!returnUrl) return;
|
||||||
|
|
||||||
|
// Auto-redirect after X seconds
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCountdown(prev => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
window.location.href = decodeURIComponent(returnUrl);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [returnUrl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Simulate checking order status
|
// Simulate checking order status
|
||||||
@@ -38,112 +68,84 @@ const Success = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="payment-container">
|
<VStack colorPalette="teal">
|
||||||
<div className="payment-card">
|
<Spinner size="xl" />
|
||||||
<div className="payment-body text-center">
|
<Text>Processing and confirming your payment...</Text>
|
||||||
<div className="loading-spinner">
|
</VStack>
|
||||||
<div className="spinner-border text-success" role="status">
|
|
||||||
<span className="visually-hidden">Processing...</span>
|
|
||||||
</div>
|
|
||||||
<p className="mt-3">Confirming your payment...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="payment-container">
|
<div className="payment-container">
|
||||||
<div className="payment-card">
|
<Alert.Root status="success" title="Payment Successful!" mb="5">
|
||||||
<div className="payment-body text-center">
|
<Alert.Indicator />
|
||||||
<div className="payment-status-icon status-success">
|
<Alert.Title>Payment Successful!</Alert.Title>
|
||||||
<i className="fas fa-check-circle"></i>
|
<Alert.Description>
|
||||||
</div>
|
<Text>Thank you for your payment. Your transaction has been completed successfully.</Text>
|
||||||
|
<Text>A confirmation email has been sent to your email address with all the transaction details.</Text>
|
||||||
|
<Text>Redirecting you back to checkout in <strong>{countdown}</strong> seconds...</Text>
|
||||||
|
</Alert.Description>
|
||||||
|
</Alert.Root>
|
||||||
|
|
||||||
<h2 className="text-success mb-3">Payment Successful!</h2>
|
<DataList.Root orientation="horizontal">
|
||||||
<p className="lead mb-4">Thank you for your payment. Your transaction has been completed successfully.</p>
|
|
||||||
|
|
||||||
<div className="success-message">
|
|
||||||
<div className="row">
|
|
||||||
{transactionId && (
|
{transactionId && (
|
||||||
<div className="col-md-6 mb-3">
|
<DataList.Item>
|
||||||
<strong>Transaction ID:</strong>
|
<DataList.ItemLabel>Transaction ID:</DataList.ItemLabel>
|
||||||
<br />
|
<DataList.ItemValue>{transactionId}</DataList.ItemValue>
|
||||||
<code>{transactionId}</code>
|
</DataList.Item>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{orderId && (
|
{orderId && (
|
||||||
<div className="col-md-6 mb-3">
|
<DataList.Item>
|
||||||
<strong>Order ID:</strong>
|
<DataList.ItemLabel>Order ID:</DataList.ItemLabel>
|
||||||
<br />
|
<DataList.ItemValue>{orderId}</DataList.ItemValue>
|
||||||
<span className="badge bg-primary">{orderId}</span>
|
</DataList.Item>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{paypalOrderId && (
|
{paypalOrderId && (
|
||||||
<div className="col-12 mb-3">
|
<DataList.Item>
|
||||||
<small className="text-muted">
|
<DataList.ItemLabel>PayPal Order ID:</DataList.ItemLabel>
|
||||||
<strong>PayPal Order ID:</strong> {paypalOrderId}
|
<DataList.ItemValue>{paypalOrderId}</DataList.ItemValue>
|
||||||
</small>
|
</DataList.Item>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{orderStatus && orderStatus.success && (
|
{orderStatus && orderStatus.success && (
|
||||||
<div className="mt-4 p-3 bg-light rounded">
|
<>
|
||||||
<h6>Order Details</h6>
|
<DataList.Item>
|
||||||
<div className="row text-start">
|
<DataList.ItemLabel>PayPal Order ID:</DataList.ItemLabel>
|
||||||
<div className="col-6">
|
<DataList.ItemValue>{paypalOrderId}</DataList.ItemValue>
|
||||||
<small>
|
</DataList.Item>
|
||||||
<strong>Status:</strong>
|
<DataList.Item>
|
||||||
<span className={`ms-2 badge bg-${
|
<DataList.ItemLabel><strong>Total:</strong></DataList.ItemLabel>
|
||||||
orderStatus.status === 'completed' ? 'success' :
|
<DataList.ItemValue>{formatCurrency(orderStatus.total, orderStatus.currency)}</DataList.ItemValue>
|
||||||
orderStatus.status === 'processing' ? 'warning' : 'secondary'
|
</DataList.Item>
|
||||||
}`}>
|
{/*<small>*/}
|
||||||
{orderStatus.status}
|
{/* <strong>Order Status:</strong>*/}
|
||||||
</span>
|
{/* <span className={`ms-2 badge bg-${*/}
|
||||||
</small>
|
{/* orderStatus.status === 'completed' ? 'success' :*/}
|
||||||
</div>
|
{/* orderStatus.status === 'processing' ? 'warning' : 'secondary'*/}
|
||||||
<div className="col-6">
|
{/* }`}>*/}
|
||||||
<small>
|
{/* {orderStatus.status}*/}
|
||||||
<strong>Total:</strong> {formatCurrency(orderStatus.total, orderStatus.currency)}
|
{/* </span>*/}
|
||||||
</small>
|
{/*</small>*/}
|
||||||
</div>
|
</>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
</DataList.Root>
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="alert alert-info">
|
|
||||||
<i className="fas fa-envelope me-2"></i>
|
|
||||||
A confirmation email has been sent to your email address with all the transaction details.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
|
<div className="payment-card">
|
||||||
|
<div className="payment-body text-center">
|
||||||
|
<Center mt="5" mb="5">
|
||||||
<a
|
<a
|
||||||
href={`${import.meta.env.VITE_REACT_APP_WOOCOMMERCE_URL || '#'}`}
|
href={returnUrl}
|
||||||
className="btn btn-primary me-3"
|
|
||||||
>
|
>
|
||||||
<i className="fas fa-arrow-left me-2"></i>
|
<HStack>
|
||||||
Back to Store
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<LuMoveLeft /> Back to the Store
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
</a>
|
</a>
|
||||||
<button
|
</Center>
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={() => window.print()}
|
|
||||||
>
|
|
||||||
<i className="fas fa-print me-2"></i>
|
|
||||||
Print Receipt
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 pt-4 border-top">
|
|
||||||
<small className="text-muted">
|
|
||||||
<i className="fas fa-shield-alt me-1"></i>
|
|
||||||
This transaction was processed securely by PayPal
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths"
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react(), tsconfigPaths()],
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
Reference in New Issue
Block a user