This commit is contained in:
Vassshhh
2025-07-29 15:32:25 +07:00
parent 659e25dd74
commit ad5e3c7a1c
2 changed files with 325 additions and 68 deletions

View File

@@ -1,72 +1,272 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import styles from './Checkout.module.css';
const Checkout = () => {
import { QRCodeCanvas } from 'qrcode.react';
function parseJwt(token) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
} catch (e) {
return null;
}
}
const Checkout = ({ socketId, transactionSuccess }) => {
const [products, setProducts] = useState([]);
const [itemIds, setItemIds] = useState(null);
const [token, setToken] = useState(null);
const [qrisData, setQrisData] = useState(null);
const [value, setValue] = useState(null);
const [loadingPay, setLoadingPay] = useState(false);
const [redirect_uri, setRedirect_Uri] = useState('');
const [redirect_failed, setRedirect_Failed] = useState('');
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const tokenParam = urlParams.get('token');
const itemsIdString = urlParams.get('itemsId');
setRedirect_Uri(urlParams.get('redirect_uri'));
setRedirect_Failed(urlParams.get('redirect_failed'));
setToken(tokenParam);
if (!itemsIdString) {
window.location.href = redirect_failed;
return;
}
try {
const parsedIds = JSON.parse(itemsIdString);
if (!Array.isArray(parsedIds) || parsedIds.length === 0) {
window.location.href = redirect_failed;
return;
}
setItemIds(parsedIds);
} catch (e) {
console.error('Invalid itemsId format', e);
window.location.href = redirect_failed;
}
}, []);
// Fetch products
useEffect(() => {
if (itemIds && Array.isArray(itemIds) && itemIds.length > 0) {
fetch('https://bot.kediritechnopark.com/webhook/store-dev/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ itemsId: itemIds }),
})
.then((res) => res.json())
.then((data) => {
setProducts(data);
})
.catch((err) => {
console.error('Error fetching products:', err);
});
}
}, [itemIds]);
const handleRemove = (id) => {
const updatedItemIds = itemIds.filter((itemId) => itemId !== id);
const updatedProducts = products.filter((product) => product.id !== id);
if (updatedItemIds.length === 0) {
window.location.href = redirect_failed;
return;
}
setItemIds(updatedItemIds);
setProducts(updatedProducts);
const url = new URL(window.location);
url.searchParams.set('itemsId', JSON.stringify(updatedItemIds));
window.history.replaceState(null, '', url.toString());
};
const handlePay = async () => {
if (!itemIds || !token) {
alert('Token atau itemsId tidak ditemukan.');
return;
}
setLoadingPay(true);
try {
const params = new URLSearchParams();
itemIds.forEach((id) => params.append('itemsId', id));
params.append('socketId', socketId);
// Jika butuh socketId bisa tambahkan di sini, misal: params.append('socketId', socketId);
const response = await fetch('https://bot.kediritechnopark.com/webhook/store-dev/pay', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${token}`,
},
body: params.toString(),
});
const result = await response.json();
if (response.ok && result?.qris_dynamic) {
setQrisData(result.qris_dynamic);
setValue(result.total_price);
} else {
alert(`Gagal mendapatkan QRIS: ${result?.error || 'Unknown error'}`);
}
} catch (error) {
console.error('Network error:', error);
alert('Terjadi kesalahan jaringan.');
} finally {
setLoadingPay(false);
}
};
useEffect(() => {
if (transactionSuccess) {
const timer = setTimeout(() => {
window.location.href = redirect_uri;
}, 10000); // 10 detik = 10000 ms
// Bersihkan timer kalau komponen unmount atau transactionSuccess berubah
return () => clearTimeout(timer);
}
}, [transactionSuccess]);
return (
<div style={{
<div
style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '1rem',
boxSizing: 'border-box',
}}>
}}
>
<div className={styles.checkoutCard}>
{/* Product List */}
<div className={styles.cartSection}>
{!qrisData &&
<>
<h2 className={styles.cartTitle}>Your Cart</h2>
<ul className={styles.cartList}>
<li className={styles.cartItem}>
{products.map((item, index) => (
<li key={index} className={styles.cartItem}>
<div className={styles.itemDetails}>
<img
src="https://via.placeholder.com/60"
alt="Product 1"
src={item.image || 'https://via.placeholder.com/60'}
alt={item.name}
className={styles.productImage}
/>
<div>
<p className={styles.itemText}>Pure Kit</p>
<p className={styles.itemPrice}>$65.00</p>
<p className={styles.itemText}>{item.name}</p>
<p className={styles.itemPrice}>Rp{item.price?.toLocaleString('id-ID')}</p>
{item.duration?.hours && (
<p style={{ fontSize: '0.8rem', color: '#777' }}>
Durasi: {item.duration.hours} jam
</p>
)}
</div>
</div>
<button className={styles.removeBtn} aria-label="Remove Pure Kit">&times;</button>
</li>
<li className={styles.cartItem}>
<div className={styles.itemDetails}>
<img
src="https://via.placeholder.com/60"
alt="Product 2"
className={styles.productImage}
/>
<div>
<p className={styles.itemText}>Energy Drink</p>
<p className={styles.itemPrice}>$25.00</p>
</div>
</div>
<button className={styles.removeBtn} aria-label="Remove Energy Drink">&times;</button>
<button
className={styles.removeBtn}
onClick={() => handleRemove(item.id)}
aria-label={`Remove ${item.name}`}
>
&times;
</button>
</li>
))}
</ul>
</>
}
{qrisData && (
<>
<p>Silahkan scan QRIS ini</p>
<div style={{ marginTop: '2rem', textAlign: 'center', position: 'relative', display: 'inline-block' }}>
<QRCodeCanvas value={qrisData} size={256} />
{transactionSuccess && (
<div className={styles.CheckmarkOverlay}>
<svg
className={styles.Checkmark}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 52 52"
>
<circle
className={styles.CheckmarkCircle}
cx="26"
cy="26"
r="25"
fill="none"
stroke="#4BB543"
strokeWidth="4"
/>
<path
className={styles.CheckmarkCheck}
fill="none"
stroke="#4BB543"
strokeWidth="4"
d="M14 27l7 7 16-16"
/>
</svg>
</div>
)}
{!transactionSuccess && (
<>
<h2>Rp{value?.toLocaleString('id-ID')}</h2>
</>
)}
</div>
</>
)}
</div>
{/* Checkout form */}
<div className={styles.checkoutSection}>
<div>
<h2 className={styles.checkoutTitle}>Note / Request</h2>
<h2 className={styles.checkoutTitle}>
Rp{qrisData ? value : products.reduce((acc, item) => acc + (item.price || 0), 0).toLocaleString('id-ID')}
</h2>
<div style={{ marginBottom: '1.5rem' }}>
<input
type="text"
placeholder="Optional notes for seller"
placeholder={parseJwt(token)?.username || 'User'}
className={styles.inputNote}
readOnly
/>
</div>
<button className={styles.paymentBtn}>Complete Payment</button>
<button
className={styles.paymentBtn}
onClick={handlePay}
disabled={loadingPay || qrisData !== null}
>
{loadingPay ? 'Processing...' : qrisData ? 'Payment QRIS Generated' : 'Complete Payment'}
</button>
</div>
{/* Footer */}
<div className={styles.footerText}>
Powered by <span className={styles.footerHighlight}>Stripe</span> {' '}
<a href="#" className={styles.footerLink}>Terms</a> {' '}
<a href="#" className={styles.footerLink}>Privacy Policy</a>
Powered by <span className={styles.footerHighlight}>KEDIRITECHNOPARK</span> {' '}
</div>
</div>
</div>

View File

@@ -181,3 +181,60 @@
max-width: 100%;
}
}
.CheckmarkOverlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.5);
width: 130px;
height: 130px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
pointer-events: none;
z-index: 10;
}
.CheckmarkSvg {
width: 100px;
height: 100px;
display: block;
}
.CheckmarkCircle {
fill: none;
stroke: #4BB543;
stroke-width: 4;
stroke-dasharray: 157; /* 2πr where r = 25 */
stroke-dashoffset: 157;
transform: rotate(-90deg);
transform-origin: center;
animation: CircleFill 1s ease forwards;
}
.CheckmarkCheck {
fill: none;
stroke: #4BB543;
stroke-width: 4;
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: DrawCheck 0.5s ease forwards;
animation-delay: 1s;
}
/* Circle fills in a clockwise motion */
@keyframes CircleFill {
to {
stroke-dashoffset: 0;
}
}
/* Checkmark is drawn after circle is full */
@keyframes DrawCheck {
to {
stroke-dashoffset: 0;
}
}