MEGA POG STRIPE WORKS
This commit is contained in:
25
webui/package-lock.json
generated
25
webui/package-lock.json
generated
@@ -15,6 +15,8 @@
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/x-data-grid": "^8.5.2",
|
||||
"@mui/x-date-pickers": "^8.5.2",
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.3.1",
|
||||
"axios": "^1.9.0",
|
||||
"lucide-react": "^0.516.0",
|
||||
"next": "15.3.3",
|
||||
@@ -1334,6 +1336,29 @@
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/react-stripe-js": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.7.0.tgz",
|
||||
"integrity": "sha512-PYls/2S9l0FF+2n0wHaEJsEU8x7CmBagiH7zYOsxbBlLIHEsqUIQ4MlIAbV9Zg6xwT8jlYdlRIyBTHmO3yM7kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@stripe/stripe-js": ">=1.44.1 <8.0.0",
|
||||
"react": ">=16.8.0 <20.0.0",
|
||||
"react-dom": ">=16.8.0 <20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/stripe-js": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.3.1.tgz",
|
||||
"integrity": "sha512-pTzb864TQWDRQBPLgSPFRoyjSDUqpCkbEgTzpsjiTjGz1Z5SxZNXJek28w1s6Dyry4CyW4/Izj5jHE/J9hCJYQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/x-data-grid": "^8.5.2",
|
||||
"@mui/x-date-pickers": "^8.5.2",
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.3.1",
|
||||
"axios": "^1.9.0",
|
||||
"lucide-react": "^0.516.0",
|
||||
"next": "15.3.3",
|
||||
|
||||
103
webui/src/app/components/PaymentForm.js
Normal file
103
webui/src/app/components/PaymentForm.js
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
useStripe,
|
||||
useElements,
|
||||
PaymentElement,
|
||||
AddressElement,
|
||||
} from '@stripe/react-stripe-js';
|
||||
|
||||
export default function PaymentForm({ onSuccess, orderId }) {
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!stripe || !elements) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setMessage('');
|
||||
|
||||
const { error } = await stripe.confirmPayment({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: `${window.location.origin}/payment-success`,
|
||||
},
|
||||
redirect: 'if_required',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
if (error.type === 'card_error' || error.type === 'validation_error') {
|
||||
setMessage(error.message || 'An error occurred');
|
||||
} else {
|
||||
setMessage('An unexpected error occurred.');
|
||||
}
|
||||
} else {
|
||||
setMessage('Payment successful! 🎉');
|
||||
setIsSuccess(true);
|
||||
setTimeout(() => {
|
||||
onSuccess();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const paymentElementOptions = {
|
||||
layout: 'tabs',
|
||||
};
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
<div className="success-container">
|
||||
<div className="success-message">
|
||||
<h2>✅ Payment Successful!</h2>
|
||||
<p>Thank you for your purchase!</p>
|
||||
<p><strong>Order ID:</strong> {orderId}</p>
|
||||
<p>You will receive a confirmation email shortly.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="payment-form">
|
||||
<div className="payment-section">
|
||||
<h3>Billing Information</h3>
|
||||
<AddressElement options={{ mode: 'billing' }} />
|
||||
</div>
|
||||
|
||||
<div className="payment-section">
|
||||
<h3>Payment Information</h3>
|
||||
<PaymentElement options={paymentElementOptions} />
|
||||
</div>
|
||||
|
||||
<button
|
||||
disabled={isLoading || !stripe || !elements}
|
||||
className="pay-button"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="spinner">
|
||||
<div className="spinner-border"></div>
|
||||
Processing...
|
||||
</div>
|
||||
) : (
|
||||
'Pay Now'
|
||||
)}
|
||||
</button>
|
||||
|
||||
{message && (
|
||||
<div className={`message ${isSuccess ? 'success' : 'error'}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
246
webui/src/app/globals.css
Normal file
246
webui/src/app/globals.css
Normal file
@@ -0,0 +1,246 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
|
||||
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
header p {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.products h2 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.product-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.product-card h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #0570de;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.select-btn {
|
||||
background: #0570de;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.select-btn:hover:not(:disabled) {
|
||||
background: #0458b3;
|
||||
}
|
||||
|
||||
.select-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.checkout {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.order-summary h2 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.order-details p {
|
||||
margin: 8px 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
/* Payment Form Styles */
|
||||
.payment-form {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.payment-section {
|
||||
margin-bottom: 30px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.payment-section h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.pay-button {
|
||||
width: 100%;
|
||||
background: #0570de;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pay-button:hover:not(:disabled) {
|
||||
background: #0458b3;
|
||||
}
|
||||
|
||||
.pay-button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid transparent;
|
||||
border-top: 2px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.success-container {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
padding: 30px;
|
||||
border: 1px solid #c3e6cb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.success-message h2 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.success-message p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
@@ -1,26 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: {
|
||||
main: '#1976d2',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const metadata = {
|
||||
title: 'Stripe Payment Demo',
|
||||
description: 'Stripe payment integration demo with Next.js App Router',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className='antialiased'>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -1,192 +1,143 @@
|
||||
'use client';
|
||||
|
||||
import { useUser } from "@auth0/nextjs-auth0";
|
||||
import {useEffect, useState} from "react";
|
||||
import { useState } from 'react';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import PaymentForm from './components/PaymentForm';
|
||||
import './globals.css';
|
||||
|
||||
const stripePromise = loadStripe('pk_test_51RaxJBRrcXIyofFGYIfUxzWTLPBfr1A0f2VBjo0lOjHfTBtyVpJKBjVUJ972p5AytGl4LBrgQccwHkp6EYu4liln00vEAf2D4e');
|
||||
|
||||
const products = [
|
||||
{ id: '1', name: 'Premium Widget', price: 2999, description: 'High-quality widget for professionals' },
|
||||
{ id: '2', name: 'Standard Widget', price: 1999, description: 'Reliable widget for everyday use' },
|
||||
{ id: '3', name: 'Basic Widget', price: 999, description: 'Entry-level widget for beginners' }
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const { user, error, isLoading } = useUser();
|
||||
const [selectedProduct, setSelectedProduct] = useState(null);
|
||||
const [clientSecret, setClientSecret] = useState('');
|
||||
const [orderId, setOrderId] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAccessToken = async () => {
|
||||
if (user) {
|
||||
try {
|
||||
await fetch('/token');
|
||||
} catch (error) {
|
||||
console.error("Error fetching token");
|
||||
}
|
||||
const handleProductSelect = async (product) => {
|
||||
setLoading(true);
|
||||
setSelectedProduct(product);
|
||||
|
||||
const newOrderId = Math.floor(Math.random() * 10000).toString();
|
||||
setOrderId(newOrderId);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://impr.ink/api/stripe/create-payment-intent', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
amount: product.price,
|
||||
orderId: newOrderId
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.clientSecret) {
|
||||
setClientSecret(data.clientSecret);
|
||||
} else {
|
||||
try {
|
||||
await fetch('/untoken');
|
||||
} catch (e) {
|
||||
console.error('Error in /api/untoken:', e);
|
||||
}
|
||||
console.error('Error creating payment intent:', data.error);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAccessToken().then(r => console.log(r));
|
||||
}, [user]);
|
||||
const handlePaymentSuccess = () => {
|
||||
setSelectedProduct(null);
|
||||
setClientSecret('');
|
||||
setOrderId('');
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 flex items-center justify-center">
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 border-4 border-white/20 border-t-white rounded-full animate-spin"></div>
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="w-10 h-10 border-4 border-transparent border-t-purple-400 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const handleBackToProducts = () => {
|
||||
setSelectedProduct(null);
|
||||
setClientSecret('');
|
||||
setOrderId('');
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-900 via-pink-900 to-purple-900 flex items-center justify-center p-4">
|
||||
<div className="bg-white/10 backdrop-blur-xl rounded-2xl p-6 border border-white/20 shadow-2xl">
|
||||
<div className="text-white/80 mb-4">{error.message}</div>
|
||||
<div className="text-center">
|
||||
<a
|
||||
href="/auth/login"
|
||||
className="group relative inline-flex items-center gap-2 px-8 py-3 bg-gradient-to-r from-purple-500 to-blue-500 rounded-xl font-bold text-white shadow-2xl hover:shadow-purple-500/25 transition-all duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-purple-600 to-blue-600 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<span className="relative flex items-center gap-2">
|
||||
Sign In
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
onClick={() => checkValidity()}
|
||||
className="group relative px-6 py-3 bg-gradient-to-r from-red-500 to-pink-500 rounded-xl font-bold text-white shadow-2xl hover:shadow-red-500/25 transition-all duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-red-600 to-pink-600 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<span className="relative flex items-center gap-2">
|
||||
Check
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const appearance = {
|
||||
theme: 'stripe',
|
||||
variables: {
|
||||
colorPrimary: '#0570de',
|
||||
colorBackground: '#ffffff',
|
||||
colorText: '#30313d',
|
||||
colorDanger: '#df1b41',
|
||||
fontFamily: 'Ideal Sans, system-ui, sans-serif',
|
||||
spacingUnit: '2px',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
};
|
||||
|
||||
const options = {
|
||||
clientSecret,
|
||||
appearance,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 relative overflow-hidden">
|
||||
<div className="relative z-10 min-h-screen flex items-center justify-center p-4">
|
||||
{user ? (
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="text-center mb-6">
|
||||
<div
|
||||
className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full mb-3 shadow-2xl">
|
||||
{user.picture ? (
|
||||
<img
|
||||
src={user.picture}
|
||||
alt="Profile"
|
||||
className="w-full h-full rounded-full object-cover border-3 border-white/20"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-white text-xl font-bold">
|
||||
{user.name?.charAt(0) || user.email?.charAt(0) || '👤'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-2xl pb-1 font-bold bg-gradient-to-r from-white via-purple-200 to-blue-200 bg-clip-text text-transparent">
|
||||
Just testing :P
|
||||
</h1>
|
||||
</div>
|
||||
<div className="container">
|
||||
<header>
|
||||
<h1>🛍️ Stripe Payment Demo</h1>
|
||||
<p>Select a product to purchase</p>
|
||||
</header>
|
||||
|
||||
<div className="bg-white/10 backdrop-blur-xl rounded-2xl border border-white/20 shadow-2xl overflow-hidden mb-4">
|
||||
<div className="bg-gradient-to-r from-purple-500/20 to-blue-500/20 p-4 border-b border-white/10">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
Auth Details
|
||||
</h2>
|
||||
{!selectedProduct ? (
|
||||
<div className="products">
|
||||
<h2>Products</h2>
|
||||
<div className="product-grid">
|
||||
{products.map((product) => (
|
||||
<div key={product.id} className="product-card">
|
||||
<h3>{product.name}</h3>
|
||||
<p className="description">{product.description}</p>
|
||||
<p className="price">${(product.price / 100).toFixed(2)}</p>
|
||||
<button
|
||||
onClick={() => handleProductSelect(product)}
|
||||
disabled={loading}
|
||||
className="select-btn"
|
||||
>
|
||||
{loading ? 'Loading...' : 'Select'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
className="text-purple-300 text-xs font-semibold uppercase tracking-wider">Name</label>
|
||||
<div
|
||||
className="text-white text-base mt-1 p-2 bg-white/5 rounded-lg border border-white/10">
|
||||
{user.name || 'Not provided'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
className="text-purple-300 text-xs font-semibold uppercase tracking-wider">Email</label>
|
||||
<div
|
||||
className="text-white text-base mt-1 p-2 bg-white/5 rounded-lg border border-white/10">
|
||||
{user.email || 'Not provided'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
className="text-purple-300 text-xs font-semibold uppercase tracking-wider">User
|
||||
ID</label>
|
||||
<div
|
||||
className="text-white/80 text-xs mt-1 p-2 bg-white/5 rounded-lg border border-white/10 font-mono break-all">
|
||||
{user.sub || 'Not available'}
|
||||
</div>
|
||||
</div>
|
||||
{user.nickname && (
|
||||
<div>
|
||||
<label
|
||||
className="text-purple-300 text-xs font-semibold uppercase tracking-wider">Nickname</label>
|
||||
<div
|
||||
className="text-white text-base mt-1 p-2 bg-white/5 rounded-lg border border-white/10">
|
||||
{user.nickname}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="text-purple-300 text-xs font-semibold uppercase tracking-wider mb-2 block">
|
||||
Raw User Data
|
||||
</label>
|
||||
<div
|
||||
className="bg-black/30 rounded-lg p-3 border border-white/10 h-64 overflow-auto">
|
||||
<pre
|
||||
className="text-green-300 text-xs font-mono leading-tight whitespace-pre-wrap">
|
||||
{JSON.stringify(user, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<a
|
||||
onClick={() => checkValidity()}
|
||||
className="group relative px-6 py-3 bg-gradient-to-r from-red-500 to-pink-500 rounded-xl font-bold text-white shadow-2xl hover:shadow-red-500/25 transition-all duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-red-600 to-pink-600 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<span className="relative flex items-center gap-2">
|
||||
Check
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/auth/logout"
|
||||
className="group relative px-6 py-3 bg-gradient-to-r from-red-500 to-pink-500 rounded-xl font-bold text-white shadow-2xl hover:shadow-red-500/25 transition-all duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-red-600 to-pink-600 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<span className="relative flex items-center gap-2">
|
||||
Sign Out
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="checkout">
|
||||
<div className="order-summary">
|
||||
<h2>Order Summary</h2>
|
||||
<div className="order-details">
|
||||
<p><strong>Product:</strong> {selectedProduct.name}</p>
|
||||
<p><strong>Order ID:</strong> {orderId}</p>
|
||||
<p><strong>Amount:</strong> ${(selectedProduct.price / 100).toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{clientSecret && (
|
||||
<Elements options={options} stripe={stripePromise}>
|
||||
<PaymentForm
|
||||
onSuccess={handlePaymentSuccess}
|
||||
orderId={orderId}
|
||||
/>
|
||||
</Elements>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleBackToProducts}
|
||||
className="back-btn"
|
||||
>
|
||||
← Back to Products
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user