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) {
|
||||
|
||||
// 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(
|
||||
'wc_order_id' => $order->get_id(),
|
||||
'total' => $order->get_total(),
|
||||
'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(),
|
||||
'return_url' => $this->get_return_url($order),
|
||||
'cancel_url' => wc_get_checkout_url(),
|
||||
'_wpnonce' => wp_create_nonce('cpg_payment_' . $order->get_id()),
|
||||
);
|
||||
|
||||
// Add nonce for security
|
||||
|
||||
@@ -254,7 +254,23 @@ router.get('/order-status/:wc_order_id', async (req, res) => {
|
||||
total: wcOrder.order.total,
|
||||
currency: wcOrder.order.currency,
|
||||
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) {
|
||||
|
||||
@@ -35,7 +35,6 @@ class WooCommerceService {
|
||||
async getOrder(orderId) {
|
||||
try {
|
||||
const response = await this.client.get(`/orders/${orderId}`);
|
||||
|
||||
console.log('WooCommerce Order Retrieved:', {
|
||||
id: response.data.id,
|
||||
status: response.data.status,
|
||||
|
||||
@@ -1,29 +1,9 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
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_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
{ files: ["**/*.{js,mjs,cjs,jsx}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
|
||||
pluginReact.configs.flat.recommended,
|
||||
]);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>my-app</title>
|
||||
<title>ALUXPAY - Payment Gateway</title>
|
||||
</head>
|
||||
<body>
|
||||
<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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^3.27.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@paypal/react-paypal-js": "^8.9.1",
|
||||
"axios": "^1.12.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.9.2",
|
||||
"react-scripts": "^5.0.1"
|
||||
},
|
||||
@@ -23,10 +27,12 @@
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.3",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.4.0",
|
||||
"vite": "^7.1.7"
|
||||
"vite": "^7.1.7",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"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 {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 { PayPalScriptProvider } from '@paypal/react-paypal-js';
|
||||
import Payment from './pages/Payment.jsx';
|
||||
import Success from './pages/Success.jsx';
|
||||
import Cancel from './pages/Cancel.jsx';
|
||||
import './App.css';
|
||||
import logo from '/affordableluxurywatches-logo.svg';
|
||||
|
||||
// PayPal configuration
|
||||
const paypalOptions = {
|
||||
@@ -14,10 +19,36 @@ const paypalOptions = {
|
||||
};
|
||||
|
||||
function App() {
|
||||
const { toggleColorMode, colorMode } = useColorMode()
|
||||
return (
|
||||
<PayPalScriptProvider options={paypalOptions}>
|
||||
<Router>
|
||||
<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>
|
||||
<Route path="/" element={<Payment />} />
|
||||
<Route path="/payment" element={<Payment />} />
|
||||
@@ -31,6 +62,12 @@ function App() {
|
||||
</div>
|
||||
} />
|
||||
</Routes>
|
||||
<Center mt="5">
|
||||
<Text textStyle="xs">{import.meta.env.VITE_REACT_APP_STORE_NAME}</Text>
|
||||
</Center>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Container>
|
||||
</div>
|
||||
</Router>
|
||||
</PayPalScriptProvider>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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') => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
@@ -21,48 +22,83 @@ const OrderSummary = ({ orderData, loading }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<div className="order-summary">
|
||||
<h5 className="mb-3">
|
||||
<i className="fas fa-receipt me-2"></i>
|
||||
Order Summary
|
||||
</h5>
|
||||
<Box maxW="sm" borderWidth="1px" mb={5}>
|
||||
<Box p="4" spaceY="2">
|
||||
<Heading as="h2" fontSize="xl" fontWeight="bold" size="lg">Order Summary</Heading>
|
||||
<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">
|
||||
<span>Order #</span>
|
||||
<span className="fw-bold">{orderData.wc_order_id}</span>
|
||||
</div>
|
||||
{/*{orderData.customer_email && (
|
||||
<DataList.Item>
|
||||
<DataList.ItemLabel>E-mail</DataList.ItemLabel>
|
||||
<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">
|
||||
<span>Description</span>
|
||||
<span>{orderData.description}</span>
|
||||
</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>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
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 [{ isPending }] = usePayPalScriptReducer();
|
||||
@@ -42,22 +44,20 @@ const PayPalButton = ({ orderData, onCreateOrder, onCaptureOrder, loading }) =>
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="text-center py-4">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading PayPal...</span>
|
||||
</div>
|
||||
<p className="mt-2 text-muted">Loading PayPal...</p>
|
||||
</div>
|
||||
<VStack colorPalette="teal">
|
||||
<Spinner size="xl" />
|
||||
<Text>Loading PayPal...</Text>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="paypal-button-wrapper">
|
||||
{paypalError && (
|
||||
<div className="alert alert-danger mb-3">
|
||||
<i className="fas fa-exclamation-triangle me-2"></i>
|
||||
{paypalError}
|
||||
</div>
|
||||
<Alert.Root mb="5" status="warning">
|
||||
<Alert.Indicator />
|
||||
<Alert.Title>{paypalError}</Alert.Title>
|
||||
</Alert.Root>
|
||||
)}
|
||||
|
||||
<PayPalButtons
|
||||
@@ -76,24 +76,25 @@ const PayPalButton = ({ orderData, onCreateOrder, onCaptureOrder, loading }) =>
|
||||
forceReRender={[orderData.total, orderData.currency]}
|
||||
/>
|
||||
|
||||
<div className="mt-3 text-center">
|
||||
<small className="text-muted d-block">
|
||||
<Center>
|
||||
<Text textStyle="xs">
|
||||
Don't have a PayPal account? You can still pay with your credit or debit card.
|
||||
</small>
|
||||
</div>
|
||||
</Text>
|
||||
</Center>
|
||||
|
||||
{/* Fallback button for when PayPal doesn't load */}
|
||||
{!isPending && (
|
||||
<div className="mt-3">
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-sm w-100"
|
||||
<Center>
|
||||
<HStack mt="5" mb="5">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.location.reload()}
|
||||
disabled={loading}
|
||||
>
|
||||
<i className="fas fa-redo me-2"></i>
|
||||
Refresh PayPal
|
||||
</button>
|
||||
</div>
|
||||
<LuRepeat /> Reload PayPal
|
||||
</Button>
|
||||
</HStack>
|
||||
</Center>
|
||||
)}
|
||||
</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 { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
@@ -5,6 +6,8 @@ import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<Provider>
|
||||
<App />
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import React from 'react';
|
||||
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 [searchParams] = useSearchParams();
|
||||
@@ -8,6 +21,8 @@ const Cancel = () => {
|
||||
const orderId = searchParams.get('order_id');
|
||||
const paypalToken = searchParams.get('token');
|
||||
|
||||
const cancelUrl = searchParams.get('cancel_url');
|
||||
|
||||
const handleRetryPayment = () => {
|
||||
// Build the payment URL with original parameters
|
||||
const paymentUrl = new URL('/payment', window.location.origin);
|
||||
@@ -21,44 +36,27 @@ const Cancel = () => {
|
||||
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 (
|
||||
<div className="payment-container">
|
||||
<div className="payment-card">
|
||||
<div className="payment-body text-center">
|
||||
<div className={`payment-status-icon ${getIconClass()}`}>
|
||||
<i className={getIconName()}></i>
|
||||
</div>
|
||||
<Alert.Root status="error" title="Payment Cancelled">
|
||||
<Alert.Indicator />
|
||||
<Alert.Title>Payment Cancelled</Alert.Title>
|
||||
<Alert.Description>
|
||||
<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()}`}>
|
||||
{getTitle()}
|
||||
</h2>
|
||||
{/*<div className={`payment-status-icon ${getIconClass()}`}>*/}
|
||||
{/* <i className={getIconName()}></i>*/}
|
||||
{/*</div>*/}
|
||||
|
||||
<p className="lead mb-4">
|
||||
{getErrorMessage()}
|
||||
</p>
|
||||
{/*<h2 className={`mb-3 ${getTitleClass()}`}>*/}
|
||||
{/* {getTitle()}*/}
|
||||
{/*</h2>*/}
|
||||
|
||||
{/*<p className="lead mb-4">*/}
|
||||
{/* {getErrorMessage()}*/}
|
||||
{/*</p>*/}
|
||||
|
||||
{orderId && (
|
||||
<div className="alert alert-info">
|
||||
@@ -79,45 +77,58 @@ const Cancel = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4">
|
||||
<button
|
||||
className="btn btn-primary me-3"
|
||||
<Center mt="5" mb="5" gap="5">
|
||||
<HStack>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRetryPayment}
|
||||
>
|
||||
<i className="fas fa-redo me-2"></i>
|
||||
Try Payment Again
|
||||
</button>
|
||||
|
||||
<LuRepeat /> Try Payment Again
|
||||
</Button>
|
||||
</HStack>
|
||||
<a
|
||||
href={`${import.meta.env.VITE_REACT_APP_WOOCOMMERCE_URL || '#'}`}
|
||||
className="btn btn-outline-secondary"
|
||||
href={cancelUrl}
|
||||
>
|
||||
<i className="fas fa-arrow-left me-2"></i>
|
||||
Back to Store
|
||||
<HStack>
|
||||
<Button
|
||||
variant="outline"
|
||||
>
|
||||
<LuMoveLeft /> Back to the Store
|
||||
</Button>
|
||||
</HStack>
|
||||
</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">
|
||||
<div className="alert alert-light border">
|
||||
<h6 className="mb-3">Need Help?</h6>
|
||||
<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>
|
||||
<Center>
|
||||
<Text textStyle="xs">Contact Support: {import.meta.env.VITE_REACT_APP_SUPPORT_EMAIL}</Text>
|
||||
</Center>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
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 OrderSummary from '../components/OrderSummary.jsx';
|
||||
import PayPalButton from '../components/PayPalButton.jsx';
|
||||
@@ -13,9 +27,14 @@ const Payment = () => {
|
||||
total: searchParams.get('total') || '25.99',
|
||||
currency: searchParams.get('currency') || 'USD',
|
||||
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 [error, setError] = useState('');
|
||||
const [paypalOrderId, setPaypalOrderId] = useState('');
|
||||
@@ -49,6 +68,25 @@ const Payment = () => {
|
||||
}
|
||||
}, [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
|
||||
const createPayPalOrder = async () => {
|
||||
try {
|
||||
@@ -96,27 +134,141 @@ const Payment = () => {
|
||||
console.log('Payment captured:', data.transaction_id);
|
||||
|
||||
// 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) {
|
||||
console.error('Capture payment error:', error);
|
||||
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 {
|
||||
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 (
|
||||
<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-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">
|
||||
{error && (
|
||||
@@ -127,47 +279,38 @@ const Payment = () => {
|
||||
)}
|
||||
|
||||
{backendStatus && (
|
||||
<div className="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i className="fas fa-check-circle me-2"></i>
|
||||
Payment system connected
|
||||
<button type="button" className="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<Alert.Root status="info" mb="5">
|
||||
<Alert.Indicator />
|
||||
<Alert.Content>
|
||||
<Alert.Title>Payment system connected</Alert.Title>
|
||||
<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 */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<div className="mt-4 p-3 bg-light border rounded">
|
||||
<h6>Debug Info:</h6>
|
||||
{import.meta.env.VITE_NODE_ENV === 'development' && (
|
||||
<Alert.Root status="info" colorPalette="teal">
|
||||
<Alert.Indicator />
|
||||
<Alert.Content>
|
||||
<Alert.Title>Debug Info</Alert.Title>
|
||||
<Alert.Description>
|
||||
<small>
|
||||
<strong>Order ID:</strong> {orderData.wc_order_id}<br/>
|
||||
<strong>Total:</strong> ${orderData.total}<br/>
|
||||
<strong>PayPal Order:</strong> {paypalOrderId || 'Not created yet'}<br/>
|
||||
<strong>Backend:</strong> {backendStatus ? '✅ Connected' : '❌ Disconnected'}
|
||||
</small>
|
||||
</div>
|
||||
</Alert.Description>
|
||||
</Alert.Content>
|
||||
</Alert.Root>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,44 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 [searchParams] = useSearchParams();
|
||||
const [orderStatus, setOrderStatus] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [countdown, setCountdown] = useState(10);
|
||||
|
||||
const transactionId = searchParams.get('transaction_id');
|
||||
const orderId = searchParams.get('order_id');
|
||||
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(() => {
|
||||
// Simulate checking order status
|
||||
@@ -38,112 +68,84 @@ const Success = () => {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="payment-container">
|
||||
<div className="payment-card">
|
||||
<div className="payment-body text-center">
|
||||
<div className="loading-spinner">
|
||||
<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>
|
||||
<VStack colorPalette="teal">
|
||||
<Spinner size="xl" />
|
||||
<Text>Processing and confirming your payment...</Text>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="payment-container">
|
||||
<div className="payment-card">
|
||||
<div className="payment-body text-center">
|
||||
<div className="payment-status-icon status-success">
|
||||
<i className="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<DataList.Root orientation="horizontal">
|
||||
{transactionId && (
|
||||
<div className="col-md-6 mb-3">
|
||||
<strong>Transaction ID:</strong>
|
||||
<br />
|
||||
<code>{transactionId}</code>
|
||||
</div>
|
||||
<DataList.Item>
|
||||
<DataList.ItemLabel>Transaction ID:</DataList.ItemLabel>
|
||||
<DataList.ItemValue>{transactionId}</DataList.ItemValue>
|
||||
</DataList.Item>
|
||||
)}
|
||||
{orderId && (
|
||||
<div className="col-md-6 mb-3">
|
||||
<strong>Order ID:</strong>
|
||||
<br />
|
||||
<span className="badge bg-primary">{orderId}</span>
|
||||
</div>
|
||||
<DataList.Item>
|
||||
<DataList.ItemLabel>Order ID:</DataList.ItemLabel>
|
||||
<DataList.ItemValue>{orderId}</DataList.ItemValue>
|
||||
</DataList.Item>
|
||||
)}
|
||||
{paypalOrderId && (
|
||||
<div className="col-12 mb-3">
|
||||
<small className="text-muted">
|
||||
<strong>PayPal Order ID:</strong> {paypalOrderId}
|
||||
</small>
|
||||
</div>
|
||||
<DataList.Item>
|
||||
<DataList.ItemLabel>PayPal Order ID:</DataList.ItemLabel>
|
||||
<DataList.ItemValue>{paypalOrderId}</DataList.ItemValue>
|
||||
</DataList.Item>
|
||||
)}
|
||||
</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>
|
||||
<>
|
||||
<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="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
|
||||
href={`${import.meta.env.VITE_REACT_APP_WOOCOMMERCE_URL || '#'}`}
|
||||
className="btn btn-primary me-3"
|
||||
href={returnUrl}
|
||||
>
|
||||
<i className="fas fa-arrow-left me-2"></i>
|
||||
Back to Store
|
||||
<HStack>
|
||||
<Button
|
||||
variant="outline"
|
||||
>
|
||||
<LuMoveLeft /> Back to the Store
|
||||
</Button>
|
||||
</HStack>
|
||||
</a>
|
||||
<button
|
||||
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>
|
||||
</Center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [react(), tsconfigPaths()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
|
||||
Reference in New Issue
Block a user