ok
This commit is contained in:
336
src/Checkout.js
336
src/Checkout.js
@@ -1,77 +1,277 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styles from './Checkout.module.css';
|
||||
|
||||
const Checkout = () => {
|
||||
return (
|
||||
<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}>
|
||||
<h2 className={styles.cartTitle}>Your Cart</h2>
|
||||
<ul className={styles.cartList}>
|
||||
<li className={styles.cartItem}>
|
||||
<div className={styles.itemDetails}>
|
||||
<img
|
||||
src="https://via.placeholder.com/60"
|
||||
alt="Product 1"
|
||||
className={styles.productImage}
|
||||
/>
|
||||
<div>
|
||||
<p className={styles.itemText}>Pure Kit</p>
|
||||
<p className={styles.itemPrice}>$65.00</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className={styles.removeBtn} aria-label="Remove Pure Kit">×</button>
|
||||
</li>
|
||||
import { QRCodeCanvas } from 'qrcode.react';
|
||||
|
||||
<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">×</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
{/* Checkout form */}
|
||||
<div className={styles.checkoutSection}>
|
||||
<div>
|
||||
<h2 className={styles.checkoutTitle}>Note / Request</h2>
|
||||
<div style={{ marginBottom: '1.5rem' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Optional notes for seller"
|
||||
className={styles.inputNote}
|
||||
/>
|
||||
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={{
|
||||
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}>
|
||||
{products.map((item, index) => (
|
||||
<li key={index} className={styles.cartItem}>
|
||||
<div className={styles.itemDetails}>
|
||||
<img
|
||||
src={item.image || 'https://via.placeholder.com/60'}
|
||||
alt={item.name}
|
||||
className={styles.productImage}
|
||||
/>
|
||||
<div>
|
||||
<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}
|
||||
onClick={() => handleRemove(item.id)}
|
||||
aria-label={`Remove ${item.name}`}
|
||||
>
|
||||
×
|
||||
</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}>
|
||||
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={parseJwt(token)?.username || 'User'}
|
||||
className={styles.inputNote}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<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}>KEDIRITECHNOPARK</span> •{' '}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className={styles.paymentBtn}>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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkout;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user