File History: src/main/resources/templates/payment.html

← View file content

File Content at Commit f0438c2

1 <!DOCTYPE html>
2 <html lang="en" xmlns:th="http://www.thymeleaf.org">
3 <head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Payment - Payment Link Service</title>
7 <link rel="stylesheet" th:href="@{/css/styles.css}">
8 <script src="https://js.stripe.com/v3/"></script>
9 <style>
10 * {
11 box-sizing: border-box;
12 }
13 html, body {
14 margin: 0;
15 padding: 0;
16 width: 100%;
17 height: 100%;
18 overflow-x: hidden;
19 }
20 header {
21 display: none;
22 }
23 main {
24 padding: 0 !important;
25 margin: 0;
26 width: 100%;
27 min-height: 100vh;
28 display: flex;
29 align-items: center;
30 justify-content: center;
31 overflow-y: auto;
32 overflow-x: hidden;
33 }
34 .payment-container {
35 max-width: 500px;
36 width: 100%;
37 margin: 0 auto;
38 background: transparent;
39 padding: 0;
40 border-radius: 0;
41 box-shadow: none;
42 overflow: visible;
43 }
44 .payment-status {
45 padding: 0.75rem 1rem;
46 border-radius: 4px;
47 margin-bottom: 2rem;
48 text-align: center;
49 font-weight: 500;
50 font-size: 0.9rem;
51 }
52 .payment-container h2 {
53 font-size: 1.25rem;
54 margin-bottom: 1.5rem;
55 font-weight: 500;
56 text-align: left;
57 text-transform: uppercase;
58 letter-spacing: 0.1em;
59 font-size: 0.85rem;
60 color: var(--text-color);
61 }
62 .product-summary {
63 background: transparent;
64 padding: 0;
65 border-radius: 0;
66 margin-bottom: 2rem;
67 }
68 .summary-row {
69 display: flex;
70 justify-content: space-between;
71 margin-bottom: 1rem;
72 font-size: 0.9rem;
73 gap: 1rem;
74 padding-bottom: 1rem;
75 border-bottom: 1px solid var(--border-color);
76 }
77 .summary-row:last-of-type {
78 border-bottom: none;
79 padding-bottom: 0;
80 }
81 .summary-row.total {
82 font-size: 1.1rem;
83 font-weight: 600;
84 margin-top: 1rem;
85 padding-top: 1rem;
86 border-top: 2px solid var(--border-color);
87 border-bottom: none;
88 }
89 .summary-row span:last-child {
90 text-align: right;
91 word-break: break-word;
92 }
93 .stripe-form {
94 margin-top: 1.5rem;
95 }
96 .stripe-form label {
97 display: block;
98 font-size: 0.9rem;
99 font-weight: 600;
100 margin-bottom: 0.5rem;
101 color: var(--text-color);
102 }
103 #card-element {
104 padding: 0.875rem;
105 border: 1px solid var(--border-color);
106 border-radius: 4px;
107 margin-bottom: 1rem;
108 background: var(--card-bg);
109 box-shadow: none;
110 }
111 #card-element:focus-within {
112 border-color: var(--text-color);
113 box-shadow: none;
114 }
115 #card-errors {
116 color: #ef4444;
117 margin-bottom: 0.75rem;
118 min-height: 1.25rem;
119 font-size: 0.85rem;
120 }
121 .payment-button {
122 width: 100%;
123 padding: 1rem;
124 font-size: 0.95rem;
125 font-weight: 500;
126 border-radius: 4px;
127 transition: all 0.2s;
128 background: var(--text-color);
129 color: white;
130 border: none;
131 cursor: pointer;
132 }
133 .payment-button:hover {
134 opacity: 0.9;
135 }
136 .payment-button:disabled {
137 opacity: 0.6;
138 cursor: not-allowed;
139 }
140 .payment-info {
141 font-size: 0.85rem;
142 color: var(--text-light);
143 text-align: center;
144 margin-top: 2rem;
145 padding-top: 1.5rem;
146 border-top: 1px solid var(--border-color);
147 }
148 .payment-info code {
149 background: #f5f5f5;
150 padding: 0.25rem 0.5rem;
151 border-radius: 4px;
152 font-size: 0.8rem;
153 color: var(--text-color);
154 }
155 .payment-info p {
156 margin-bottom: 0.5rem;
157 }
158 @media (max-width: 768px) {
159 main {
160 padding: 1rem;
161 align-items: flex-start;
162 }
163 .payment-container {
164 max-width: 100%;
165 padding: 1.25rem;
166 margin: 0;
167 }
168 .payment-container h2 {
169 font-size: 1.1rem;
170 }
171 .product-summary {
172 padding: 1rem;
173 }
174 .summary-row {
175 font-size: 0.85rem;
176 }
177 .summary-row.total {
178 font-size: 1rem;
179 }
180 .payment-button {
181 padding: 0.75rem;
182 font-size: 0.95rem;
183 }
184 }
185 @media (max-width: 480px) {
186 main {
187 padding: 0.5rem;
188 }
189 .payment-container {
190 padding: 1rem;
191 }
192 .product-summary {
193 padding: 0.75rem;
194 }
195 .summary-row {
196 font-size: 0.8rem;
197 }
198 }
199 </style>
200 </head>
201 <body>
202 <header>
203 <nav class="navbar container">
204 <a th:href="@{/}" class="navbar-brand">
205 <h1>Webshop</h1>
206 </a>
207 </nav>
208 </header>
209
210 <main>
211 <div class="payment-container">
212 <div id="loading" class="loading">Loading payment information...</div>
213 <div id="error" class="error" style="display: none;"></div>
214 <div id="paymentContent" style="display: none;"></div>
215 </div>
216 </main>
217
218 <script th:src="@{/js/csrf.js}"></script>
219 <script>
220 const linkId = window.location.pathname.split('/').pop();
221 const API_BASE = '/api';
222 let stripe, elements, cardElement, clientSecret;
223
224 // Get Stripe publishable key from server
225 let stripePublishableKey = '';
226
227 document.addEventListener('DOMContentLoaded', async () => {
228 await loadPaymentLink();
229 });
230
231 async function loadPaymentLink() {
232 const loading = document.getElementById('loading');
233 const error = document.getElementById('error');
234 const content = document.getElementById('paymentContent');
235
236 try {
237 const response = await fetch(`${API_BASE}/payment-links/${linkId}`);
238 const data = await response.json();
239
240 if (!data.success) {
241 throw new Error(data.error || 'Payment link not found');
242 }
243
244 const { paymentLink, products } = data;
245
246 // If already paid, show confirmation
247 if (paymentLink.status === 'paid') {
248 loading.style.display = 'none';
249 content.style.display = 'block';
250 content.innerHTML = createPaidContent(paymentLink, products);
251 return;
252 }
253
254 // Initialize Stripe payment
255 await initializeStripe();
256
257 loading.style.display = 'none';
258 content.style.display = 'block';
259 content.innerHTML = createPaymentContent(paymentLink, products);
260
261 // Setup Stripe Elements
262 setupStripeElements();
263 } catch (err) {
264 loading.style.display = 'none';
265 error.style.display = 'block';
266 error.textContent = `Error: ${err.message}`;
267 }
268 }
269
270 async function initializeStripe() {
271 try {
272 // Get Stripe publishable key and client secret from server
273 const configResponse = await fetch('/api/config/stripe-key');
274 const configData = await configResponse.json();
275
276 if (configData.success && configData.publishableKey) {
277 stripePublishableKey = configData.publishableKey;
278 } else {
279 throw new Error('Stripe not configured. Please set STRIPE_PUBLISHABLE_KEY.');
280 }
281
282 const response = await fetchWithCsrf(`${API_BASE}/payment-links/${linkId}/process`, {
283 method: 'POST'
284 });
285 const data = await response.json();
286
287 if (!data.success) {
288 throw new Error(data.error || 'Failed to initialize payment');
289 }
290
291 clientSecret = data.clientSecret;
292
293 // Initialize Stripe
294 stripe = Stripe(stripePublishableKey);
295 elements = stripe.elements();
296 } catch (error) {
297 console.error('Stripe initialization error:', error);
298 throw error;
299 }
300 }
301
302 function setupStripeElements() {
303 // Create card element
304 cardElement = elements.create('card', {
305 style: {
306 base: {
307 fontSize: '16px',
308 color: '#424770',
309 '::placeholder': {
310 color: '#aab7c4',
311 },
312 },
313 invalid: {
314 color: '#9e2146',
315 },
316 },
317 });
318
319 cardElement.mount('#card-element');
320
321 // Handle real-time validation errors
322 cardElement.on('change', ({error}) => {
323 const displayError = document.getElementById('card-errors');
324 if (error) {
325 displayError.textContent = error.message;
326 } else {
327 displayError.textContent = '';
328 }
329 });
330 }
331
332 async function handlePaymentSubmit(event) {
333 event.preventDefault();
334
335 const submitButton = document.getElementById('submit-payment');
336 submitButton.disabled = true;
337 submitButton.textContent = 'Processing...';
338
339 try {
340 const {error, paymentIntent} = await stripe.confirmCardPayment(clientSecret, {
341 payment_method: {
342 card: cardElement,
343 }
344 });
345
346 if (error) {
347 throw new Error(error.message);
348 }
349
350 // Payment succeeded - complete the order
351 const completeResponse = await fetchWithCsrf(`${API_BASE}/payment-links/${linkId}/complete`, {
352 method: 'POST',
353 headers: {
354 'Content-Type': 'application/json'
355 },
356 body: JSON.stringify({
357 paymentIntentId: paymentIntent.id
358 })
359 });
360
361 const completeData = await completeResponse.json();
362
363 if (!completeData.success) {
364 throw new Error(completeData.error || 'Failed to complete order');
365 }
366
367 // Show success and redirect to order confirmation
368 window.location.href = `/order/${completeData.order.orderId}`;
369 } catch (err) {
370 showNotification(`Payment failed: ${err.message}`, 'error');
371 submitButton.disabled = false;
372 submitButton.textContent = 'Pay Now';
373 }
374 }
375
376 function createPaymentContent(paymentLink, products) {
377 const totals = paymentLink.totals || {
378 subtotal: paymentLink.subtotal || 0,
379 shipping: paymentLink.shippingInfo?.cost || 0,
380 tax: paymentLink.shippingInfo?.tax || 0,
381 total: paymentLink.total || paymentLink.amount || 0
382 };
383
384 let productsHtml = '';
385 if (products && products.length > 0) {
386 productsHtml = products.map(p => `
387 <div class="summary-row">
388 <span>${escapeHtml(p.name)} ${p.quantity ? `x${p.quantity}` : ''}</span>
389 <span>${paymentLink.currency} ${(((p.price || 0) * (p.quantity || 1)) / 100).toFixed(2)}</span>
390 </div>
391 `).join('');
392 }
393
394 return `
395 <div class="payment-status status-pending">
396 Status: Pending Payment
397 </div>
398
399 <h2>Order Summary</h2>
400
401 <div class="product-summary">
402 ${productsHtml}
403 <div class="summary-row">
404 <span><strong>Subtotal:</strong></span>
405 <span>${paymentLink.currency} ${(totals.subtotal / 100).toFixed(2)}</span>
406 </div>
407 ${totals.shipping > 0 ? `
408 <div class="summary-row">
409 <span><strong>Shipping:</strong></span>
410 <span>${paymentLink.currency} ${(totals.shipping / 100).toFixed(2)}</span>
411 </div>
412 ` : ''}
413 ${totals.tax > 0 ? `
414 <div class="summary-row">
415 <span><strong>Tax:</strong></span>
416 <span>${paymentLink.currency} ${(totals.tax / 100).toFixed(2)}</span>
417 </div>
418 ` : ''}
419 <div class="summary-row total">
420 <span>Total:</span>
421 <span>${paymentLink.currency} ${(totals.total / 100).toFixed(2)}</span>
422 </div>
423 </div>
424
425 <form id="payment-form" class="stripe-form" onsubmit="handlePaymentSubmit(event)">
426 <label for="card-element">Card Details</label>
427 <div id="card-element"></div>
428 <div id="card-errors" role="alert"></div>
429 <button type="submit" id="submit-payment" class="btn payment-button">
430 Pay ${paymentLink.currency} ${(totals.total / 100).toFixed(2)}
431 </button>
432 </form>
433
434 <div class="payment-info">
435 <p>Payment Link ID: <code>${paymentLink.linkId}</code></p>
436 <p>Created: ${new Date(paymentLink.createdAt).toLocaleString()}</p>
437 </div>
438 `;
439 }
440
441 function createPaidContent(paymentLink, products) {
442 return `
443 <div class="success-icon" style="color: #28a745; font-size: 3rem; margin-bottom: 0.5rem; line-height: 1;">✓</div>
444 <h2>Payment Successful</h2>
445 <p style="color: #666; margin-bottom: 1.5rem; font-size: 0.9rem;">Your payment has been successfully processed.</p>
446
447 <div class="product-summary">
448 ${paymentLink.orderId ? `
449 <div class="summary-row">
450 <span><strong>Order ID:</strong></span>
451 <span>${paymentLink.orderId}</span>
452 </div>
453 ` : ''}
454 </div>
455
456 <div style="text-align: center; margin-top: 1.5rem;">
457 ${paymentLink.orderId ? `
458 <a href="/order/${paymentLink.orderId}" class="btn">
459 View Order Details
460 </a>
461 ` : ''}
462 </div>
463 `;
464 }
465
466 function escapeHtml(text) {
467 const div = document.createElement('div');
468 div.textContent = text;
469 return div.innerHTML;
470 }
471 </script>
472
473 <!-- Notification Container -->
474 <div id="notificationContainer" class="notification-container"></div>
475
476 <script th:src="@{/js/notifications.js}"></script>
477 </body>
478 </html>
479

Commits

Commit Author Date Message File SHA Actions
f0438c2 <f69e50@finnacloud.com> 1766443042 +0300 12/22/2025, 10:37:22 PM increment once more 123cb5d Hide
188fc92 <f69e50@finnacloud.com> 1766442998 +0300 12/22/2025, 10:36:38 PM increment 123cb5d View
4617f76 <f69e50@finnacloud.com> 1766442953 +0300 12/22/2025, 10:35:53 PM rename branch from main to master oops 123cb5d View
e6d1548 <f69e50@finnacloud.com> 1766442769 +0300 12/22/2025, 10:32:49 PM add initial test workflow file 123cb5d View
9c24ca4 <f69e50@finnacloud.com> 1766442705 +0300 12/22/2025, 10:31:45 PM add CI configuration and test script for Jenkins build 123cb5d View
690c1f6 <f69e50@finnacloud.com> 1766368110 +0300 12/22/2025, 1:48:30 AM initialize backend structure with controllers, DTOs, and configuration files 123cb5d View