ok
This commit is contained in:
279
src/Checkout.js
279
src/Checkout.js
@@ -18,6 +18,13 @@ function parseJwt(token) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimeLeft(ms) {
|
||||
const totalSeconds = Math.max(Math.floor(ms / 1000), 0);
|
||||
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
|
||||
const seconds = String(totalSeconds % 60).padStart(2, '0');
|
||||
return `${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
const Checkout = ({ socketId, transactionSuccess }) => {
|
||||
const [products, setProducts] = useState([]);
|
||||
const [itemIds, setItemIds] = useState(null);
|
||||
@@ -30,80 +37,32 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
||||
|
||||
const [redirect_uri, setRedirect_Uri] = useState('');
|
||||
const [redirect_failed, setRedirect_Failed] = useState('');
|
||||
|
||||
const [paymentMethod, setPaymentMethod] = useState('QRIS');
|
||||
|
||||
const [payTimeout, setPayTimeout] = useState(null);
|
||||
const [timeLeft, setTimeLeft] = useState(null);
|
||||
|
||||
const [activeAccordion, setActiveAccordion] = useState('QRIS');
|
||||
|
||||
let grandTotal = 0;
|
||||
let tax = 0;
|
||||
|
||||
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 (!socketId) return;
|
||||
|
||||
if (!itemsIdString) {
|
||||
window.location.href = urlParams.get('redirect_failed') || '/';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedIds = JSON.parse(itemsIdString);
|
||||
if (!Array.isArray(parsedIds) || parsedIds.length === 0) {
|
||||
window.location.href = urlParams.get('redirect_failed') || '/';
|
||||
return;
|
||||
}
|
||||
setItemIds(parsedIds);
|
||||
} catch (e) {
|
||||
console.error('Invalid itemsId format', e);
|
||||
window.location.href = urlParams.get('redirect_failed') || '/';
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch products
|
||||
useEffect(() => {
|
||||
if (itemIds && Array.isArray(itemIds) && itemIds.length > 0) {
|
||||
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ itemsId: itemIds, noParents: true }),
|
||||
})
|
||||
.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());
|
||||
};
|
||||
let urlParams = new URLSearchParams(window.location.search);
|
||||
let tokenParam = urlParams.get('token');
|
||||
let itemsIdString = urlParams.get('itemsId');
|
||||
|
||||
const handlePay = async () => {
|
||||
if (!itemIds || !token) {
|
||||
alert('Token atau itemsId tidak ditemukan.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingPay(true);
|
||||
|
||||
try {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const newName = urlParams.get('new_name');
|
||||
const setName = urlParams.get('set_name');
|
||||
|
||||
const params = new URLSearchParams();
|
||||
itemIds.forEach((id) => params.append('itemsId', id));
|
||||
itemsIdString.forEach((id) => params.append('itemsId', id));
|
||||
params.append('socketId', socketId);
|
||||
params.append('paymentMethod', paymentMethod);
|
||||
if (newName) params.append('newName', newName);
|
||||
@@ -113,22 +72,27 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
Authorization: `Bearer ${tokenParam}`,
|
||||
},
|
||||
body: params.toString(),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
if (paymentMethod === 'QRIS' && result?.qris_dynamic) {
|
||||
setQrisData(result.qris_dynamic);
|
||||
setValue(result.total_price);
|
||||
} else if (paymentMethod === 'Bank Transfer' && result?.bank_account) {
|
||||
setTransferData(result);
|
||||
setValue(result.total_price);
|
||||
} else {
|
||||
alert(`Gagal memproses pembayaran: ${result?.error || 'Unknown error'}`);
|
||||
if (result.pay_timeout && result.time_now) {
|
||||
const timeout = new Date(result.pay_timeout).getTime();
|
||||
const now = new Date(result.time_now).getTime();
|
||||
setPayTimeout(timeout);
|
||||
setTimeLeft(timeout - now);
|
||||
}
|
||||
|
||||
setQrisData(result.qris_dynamic || null);
|
||||
setTransferData(result);
|
||||
|
||||
grandTotal = result.total_price;
|
||||
tax = 0;
|
||||
|
||||
} else {
|
||||
alert(`Request gagal: ${result?.error || 'Unknown error'}`);
|
||||
}
|
||||
@@ -140,6 +104,33 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
||||
}
|
||||
};
|
||||
|
||||
itemsIdString = JSON.parse(urlParams.get('itemsId'));
|
||||
setRedirect_Uri(urlParams.get('redirect_uri') || '');
|
||||
setRedirect_Failed(urlParams.get('redirect_failed') || '');
|
||||
setToken(tokenParam);
|
||||
|
||||
if (!itemsIdString || !Array.isArray(itemsIdString) || itemsIdString.length === 0) {
|
||||
window.location.href = urlParams.get('redirect_failed') || '/';
|
||||
return;
|
||||
}
|
||||
|
||||
setItemIds(itemsIdString);
|
||||
handlePay();
|
||||
}, [socketId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (itemIds?.length > 0) {
|
||||
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ itemsId: itemIds, noParents: true }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => setProducts(data))
|
||||
.catch((err) => console.error('Error fetching products:', err));
|
||||
}
|
||||
}, [itemIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (transactionSuccess) {
|
||||
const timer = setTimeout(() => {
|
||||
@@ -149,17 +140,25 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
||||
}
|
||||
}, [transactionSuccess, redirect_uri]);
|
||||
|
||||
const subtotal = products.reduce((acc, item) => acc + (item.price || 0), 0);
|
||||
const shipping = 0;
|
||||
const tax = 0;
|
||||
const grandTotal = subtotal + shipping + tax;
|
||||
useEffect(() => {
|
||||
if (!payTimeout) return;
|
||||
const interval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
const remaining = payTimeout - now;
|
||||
setTimeLeft(remaining);
|
||||
if (remaining <= 0) {
|
||||
clearInterval(interval);
|
||||
alert('Waktu pembayaran habis.');
|
||||
window.location.href = redirect_failed;
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [payTimeout]);
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.checkoutCard}>
|
||||
<div className={styles.cartSection}>
|
||||
{!qrisData && !transferData ? (
|
||||
<>
|
||||
<div className={styles.invHeader}>
|
||||
<div>
|
||||
<h2 className={styles.brand}>
|
||||
@@ -181,27 +180,16 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Quantity</th>
|
||||
<th className={styles.textRight}>Subtotal</th>
|
||||
<th></th>
|
||||
<th className={styles.textRight}>{grandTotal}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{products.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td>{item.name}</td>
|
||||
<td>{item.qty ?? 1}</td>
|
||||
<td className={styles.textRight}>
|
||||
Rp{(item.price || 0).toLocaleString('id-ID')}
|
||||
</td>
|
||||
<td className={styles.textRight}>
|
||||
<button
|
||||
className={styles.removeBtn}
|
||||
onClick={() => handleRemove(item.id)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -210,11 +198,7 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
||||
<div className={styles.summary}>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>SUBTOTAL</span>
|
||||
<span>Rp{subtotal.toLocaleString('id-ID')}</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>SHIPPING</span>
|
||||
<span>Rp{shipping.toLocaleString('id-ID')}</span>
|
||||
<span>Rp{grandTotal.toLocaleString('id-ID')}</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>TAX</span>
|
||||
@@ -225,12 +209,32 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
||||
<span>Rp{grandTotal.toLocaleString('id-ID')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : paymentMethod === 'QRIS' ? (
|
||||
</div>
|
||||
|
||||
<div className={styles.checkoutSection}>
|
||||
{(qrisData || transferData) && (
|
||||
<div className="mt-4">
|
||||
{qrisData && (
|
||||
<div className={styles.accordion}>
|
||||
<div
|
||||
className={styles.accordionHeader}
|
||||
onClick={() =>
|
||||
setActiveAccordion(activeAccordion === 'QRIS' ? '' : 'QRIS')
|
||||
}
|
||||
>
|
||||
QRIS Payment
|
||||
</div>
|
||||
{activeAccordion === 'QRIS' && (
|
||||
<div className={styles.accordionBody}>
|
||||
<QRCodeCanvas value={qrisData} size={200} />
|
||||
{!transactionSuccess && (
|
||||
<>
|
||||
<p className={styles.qrTitle}>Silakan scan QRIS ini</p>
|
||||
<div className={styles.qrBox}>
|
||||
<QRCodeCanvas value={qrisData} size={256} />
|
||||
<h5 className="mt-3">Rp{value?.toLocaleString('id-ID')}</h5>
|
||||
{timeLeft !== null && (
|
||||
<p>Waktu tersisa: <strong>{formatTimeLeft(timeLeft)}</strong></p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{transactionSuccess && (
|
||||
<div className={styles.CheckmarkOverlay}>
|
||||
<svg className={styles.Checkmark} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
|
||||
@@ -239,67 +243,36 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{!transactionSuccess && <h2>Rp{value?.toLocaleString('id-ID')}</h2>}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.transferBox}>
|
||||
<h3 className={styles.transferTitle}>Bank Transfer Information</h3>
|
||||
<div className={styles.transferRow}><span>Bank</span><span>{transferData?.bank_name}</span></div>
|
||||
<div className={styles.transferRow}><span>Account No</span><span>{transferData?.bank_account}</span></div>
|
||||
<div className={styles.transferRow}><span>Account Name</span><span>{transferData?.account_name}</span></div>
|
||||
<div className={styles.transferRow}><span>Total</span><span>Rp{value?.toLocaleString('id-ID')}</span></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.checkoutSection}>
|
||||
<div>
|
||||
<h2 className={styles.checkoutTitle}>
|
||||
Rp{(qrisData || transferData ? value : grandTotal).toLocaleString('id-ID')}
|
||||
</h2>
|
||||
|
||||
<div className={styles.paymentInfo}>
|
||||
<p className={styles.paymentHeading}>PAYMENT INFORMATION</p>
|
||||
<label className={styles.radioLabel}>
|
||||
<input
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="Bank Transfer"
|
||||
checked={paymentMethod === 'Bank Transfer'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
/>
|
||||
Bank Transfer
|
||||
</label>
|
||||
<label className={styles.radioLabel}>
|
||||
<input
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="QRIS"
|
||||
checked={paymentMethod === 'QRIS'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
/>
|
||||
QRIS
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputGroup}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={parseJwt(token)?.username || 'User'}
|
||||
className={styles.inputNote}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={styles.paymentBtn}
|
||||
onClick={handlePay}
|
||||
disabled={loadingPay || qrisData !== null || transferData !== null}
|
||||
{transferData && (
|
||||
<div className={styles.accordion}>
|
||||
<div
|
||||
className={styles.accordionHeader}
|
||||
onClick={() =>
|
||||
setActiveAccordion(activeAccordion === 'Bank' ? '' : 'Bank')
|
||||
}
|
||||
>
|
||||
{loadingPay ? 'Processing...' : (qrisData || transferData) ? 'Payment Created' : 'PAY'}
|
||||
</button>
|
||||
Bank Transfer
|
||||
</div>
|
||||
{activeAccordion === 'Bank' && (
|
||||
<div className={styles.accordionBody}>
|
||||
<div><strong>Bank:</strong> {transferData?.bank_name}</div>
|
||||
<div><strong>Account No:</strong> {transferData?.bank_account}</div>
|
||||
<div><strong>Account Name:</strong> {transferData?.account_name}</div>
|
||||
<div><strong>Total:</strong> Rp{value?.toLocaleString('id-ID')}</div>
|
||||
{timeLeft !== null && (
|
||||
<div><strong>Waktu Tersisa:</strong> {formatTimeLeft(timeLeft)}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.footerText}>
|
||||
Powered by <span className={styles.footerHighlight}>KEDIRITECHNOPARK</span>
|
||||
|
||||
@@ -46,18 +46,20 @@
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.brand {
|
||||
margin-top: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
color: #2563eb;
|
||||
}
|
||||
.brandLight { font-weight: 500; }
|
||||
.greeting { margin-top: .5rem; font-size: .875rem; color: #374151; }
|
||||
.greeting { text-align: left; margin-top: .5rem; font-size: .875rem; color: #374151; }
|
||||
.orderInfo { text-align: right; font-size: .875rem; color: #6b7280; }
|
||||
.invoiceLabel { font-size: 1rem; color: #4b5563; }
|
||||
.orderMeta { margin-top: .125rem; }
|
||||
|
||||
/* Table */
|
||||
.table {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: .5rem;
|
||||
@@ -67,7 +69,6 @@
|
||||
.table th, .table td { padding: .75rem 0; }
|
||||
.table th {
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
text-align: left;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -210,3 +211,87 @@
|
||||
@media (max-width: 767px) {
|
||||
.checkoutCard { flex-direction: column; }
|
||||
}
|
||||
|
||||
.selectPayment {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ccc;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.selectPaymentModern {
|
||||
width: 100%;
|
||||
padding: 0.65rem 1rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #d1d5db;
|
||||
background-color: #f9fafb;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg fill='gray' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/></svg>");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 1rem center;
|
||||
background-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.selectPaymentModern:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.transferBox {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
background: #f9fafb;
|
||||
text-align: left;
|
||||
}
|
||||
.transferBox span {
|
||||
text-align: right;
|
||||
}
|
||||
.transferTitle {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
color: #111827;
|
||||
}
|
||||
.transferRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px dashed #d1d5db;
|
||||
font-size: 0.95rem;
|
||||
color: #374151;
|
||||
}
|
||||
.transferRow:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.accordion {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordionHeader {
|
||||
background-color: #f5f5f5;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.accordionHeader:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.accordionBody {
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user