Added visualisation

Added return and cancel URL's
This commit is contained in:
2025-09-28 02:29:07 +02:00
parent bc3b86b705
commit a866801535
22 changed files with 2133 additions and 553 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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_]' }],
},
},
])

View File

@@ -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
View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"]
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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"
} }

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -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); }
}

View File

@@ -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,23 +19,55 @@ 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">
<Routes> <Container mt="5">
<Route path="/" element={<Payment />} />
<Route path="/payment" element={<Payment />} /> <Grid>
<Route path="/success" element={<Success />} /> <Box
<Route path="/cancel" element={<Cancel />} /> p="4"
<Route path="*" element={ borderWidth="1px"
<div className="container mt-5 text-center"> borderColor="border.disabled"
<h2>Page Not Found</h2> color="fg.disabled"
<p>The requested page could not be found.</p> >
<a href="/payment" className="btn btn-primary">Go to Payment</a> <Grid
</div> templateColumns={{ base: "1fr", md: "1fr auto" }}
} /> alignItems="center"
</Routes> 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>
<Route path="/" element={<Payment />} />
<Route path="/payment" element={<Payment />} />
<Route path="/success" element={<Success />} />
<Route path="/cancel" element={<Cancel />} />
<Route path="*" element={
<div className="container mt-5 text-center">
<h2>Page Not Found</h2>
<p>The requested page could not be found.</p>
<a href="/payment" className="btn btn-primary">Go to Payment</a>
</div>
} />
</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>

View File

@@ -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>
); );
}; };

View File

@@ -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
onClick={() => window.location.reload()} variant="outline"
disabled={loading} onClick={() => window.location.reload()}
> disabled={loading}
<i className="fas fa-redo me-2"></i> >
Refresh PayPal <LuRepeat /> Reload PayPal
</button> </Button>
</div> </HStack>
</Center>
)} )}
</div> </div>
); );

View 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}
/>
)
})

View 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>
)
}

View 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>
)
}

View 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>
)
})

View File

@@ -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>
<App /> <Provider>
<App />
</Provider>
</StrictMode>, </StrictMode>,
) )

View File

@@ -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
onClick={handleRetryPayment} variant="outline"
> onClick={handleRetryPayment}
<i className="fas fa-redo me-2"></i> >
Try Payment Again <LuRepeat /> 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>
); );
}; };

View File

@@ -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"; // 11th19th
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 />
<small> <Alert.Content>
<strong>Order ID:</strong> {orderData.wc_order_id}<br/> <Alert.Title>Debug Info</Alert.Title>
<strong>Total:</strong> ${orderData.total}<br/> <Alert.Description>
<strong>PayPal Order:</strong> {paypalOrderId || 'Not created yet'}<br/> <small>
<strong>Backend:</strong> {backendStatus ? ' Connected' : ' Disconnected'} <strong>Order ID:</strong> {orderData.wc_order_id}<br/>
</small> <strong>Total:</strong> ${orderData.total}<br/>
</div> <strong>PayPal Order:</strong> {paypalOrderId || 'Not created yet'}<br/>
<strong>Backend:</strong> {backendStatus ? ' Connected' : ' Disconnected'}
</small>
</Alert.Description>
</Alert.Content>
</Alert.Root>
)} )}
</div> </div>
</div> </div>

View File

@@ -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">
<Alert.Root status="success" title="Payment Successful!" mb="5">
<Alert.Indicator />
<Alert.Title>Payment Successful!</Alert.Title>
<Alert.Description>
<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>
<DataList.Root orientation="horizontal">
{transactionId && (
<DataList.Item>
<DataList.ItemLabel>Transaction ID:</DataList.ItemLabel>
<DataList.ItemValue>{transactionId}</DataList.ItemValue>
</DataList.Item>
)}
{orderId && (
<DataList.Item>
<DataList.ItemLabel>Order ID:</DataList.ItemLabel>
<DataList.ItemValue>{orderId}</DataList.ItemValue>
</DataList.Item>
)}
{paypalOrderId && (
<DataList.Item>
<DataList.ItemLabel>PayPal Order ID:</DataList.ItemLabel>
<DataList.ItemValue>{paypalOrderId}</DataList.ItemValue>
</DataList.Item>
)}
{orderStatus && orderStatus.success && (
<>
<DataList.Item>
<DataList.ItemLabel>PayPal Order ID:</DataList.ItemLabel>
<DataList.ItemValue>{paypalOrderId}</DataList.ItemValue>
</DataList.Item>
<DataList.Item>
<DataList.ItemLabel><strong>Total:</strong></DataList.ItemLabel>
<DataList.ItemValue>{formatCurrency(orderStatus.total, orderStatus.currency)}</DataList.ItemValue>
</DataList.Item>
{/*<small>*/}
{/* <strong>Order Status:</strong>*/}
{/* <span className={`ms-2 badge bg-${*/}
{/* orderStatus.status === 'completed' ? 'success' :*/}
{/* orderStatus.status === 'processing' ? 'warning' : 'secondary'*/}
{/* }`}>*/}
{/* {orderStatus.status}*/}
{/* </span>*/}
{/*</small>*/}
</>
)}
</DataList.Root>
<div className="payment-card"> <div className="payment-card">
<div className="payment-body text-center"> <div className="payment-body text-center">
<div className="payment-status-icon status-success"> <Center mt="5" mb="5">
<i className="fas fa-check-circle"></i>
</div>
<h2 className="text-success mb-3">Payment Successful!</h2>
<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 && (
<div className="col-md-6 mb-3">
<strong>Transaction ID:</strong>
<br />
<code>{transactionId}</code>
</div>
)}
{orderId && (
<div className="col-md-6 mb-3">
<strong>Order ID:</strong>
<br />
<span className="badge bg-primary">{orderId}</span>
</div>
)}
{paypalOrderId && (
<div className="col-12 mb-3">
<small className="text-muted">
<strong>PayPal Order ID:</strong> {paypalOrderId}
</small>
</div>
)}
</div>
</div>
{orderStatus && orderStatus.success && (
<div className="mt-4 p-3 bg-light rounded">
<h6>Order Details</h6>
<div className="row text-start">
<div className="col-6">
<small>
<strong>Status:</strong>
<span className={`ms-2 badge bg-${
orderStatus.status === 'completed' ? 'success' :
orderStatus.status === 'processing' ? 'warning' : 'secondary'
}`}>
{orderStatus.status}
</span>
</small>
</div>
<div className="col-6">
<small>
<strong>Total:</strong> {formatCurrency(orderStatus.total, orderStatus.currency)}
</small>
</div>
</div>
</div>
)}
<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">
<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>

View File

@@ -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: {