MEGA POG STRIPE WORKS

This commit is contained in:
lumijiez
2025-06-21 23:19:16 +03:00
parent d9dfafe07a
commit cae87ee1b7
15 changed files with 779 additions and 325 deletions

View File

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

View File

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

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

View File

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

View File

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