PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.3
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.3
2.4.3 2.4.4 2.4.2 2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 All 60 releases
easy-invoice / templates / payment-section.php

payment-section.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.3, at templates/payment-section.php

1,085 lines 34.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Payment Section Template
4 *
5 * Displays payment options and processing UI
6 *
7 * @package EasyInvoice
8 * @since 1.0.0
9 */
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14 // Get invoice data
15 $invoice_id = get_the_ID();
16 // The access token the AJAX calls below present back to the payment endpoints:
17 // the ?ik= from an emailed link, or whatever another proof of access (a signed
18 // secure link) supplies through the filter.
19 /** This filter is documented in includes/Controllers/InvoiceController.php */
20 $ei_ps_access_token = (string) apply_filters('easy_invoice_presented_access_token', isset($_GET['ik']) ? sanitize_text_field(wp_unslash($_GET['ik'])) : '', 'invoice'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
21 $invoice = new \EasyInvoice\Models\Invoice(get_post($invoice_id));
22 // What is owed now: total less payments received and credit notes.
23 $total_amount = \EasyInvoice\Services\InvoiceBalance::due($invoice);
24 $currency_code = $invoice->getCurrencyCode() ?: 'USD';
25
26 // If currency is "global", use the global setting
27 if ($currency_code === 'global') {
28 $currency_code = get_option('easy_invoice_currency_code', 'USD');
29 }
30
31 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
32
33 // Get available payment gateways
34 $payment_controller = new \EasyInvoice\Controllers\PaymentController();
35 $available_gateways = $payment_controller->getAvailableGateways($invoice_id);
36
37 // Get nonce for AJAX security
38 $payment_nonce = wp_create_nonce('easy_invoice_payment');
39
40
41 ?>
42 <?php
43 if (!function_exists('easy_invoice_payment_icon')) {
44 /**
45 * An inline icon for a payment method. The public page loads no icon
46 * font, so Font Awesome class names rendered as empty boxes; a gateway
47 * may still hand over an image URL.
48 *
49 * @param string $gateway_id Gateway id.
50 * @param string $icon Icon the gateway declared (class name or URL).
51 * @return string HTML.
52 */
53 function easy_invoice_payment_icon(string $gateway_id, string $icon = ''): string {
54 if ('' !== $icon && preg_match('#^(https?:)?//|^/#', $icon)) {
55 return '<img src="' . esc_url($icon) . '" alt="" style="width:22px;height:22px;object-fit:contain;">';
56 }
57 $a = 'width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"';
58 $paths = [
59 'bank' => '<path d="M3 10h18M5 10v9M9 10v9M15 10v9M19 10v9M2 19h20M12 3l9 6H3l9-6z"/>',
60 'cheque' => '<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M7 10h6M7 14h3M15 14h2"/>',
61 'cash' => '<rect x="2" y="6" width="20" height="12" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M6 9h.01M18 15h.01"/>',
62 'card' => '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20M6 15h4"/>',
63 'wallet' => '<path d="M3 7a2 2 0 0 1 2-2h14v4"/><rect x="3" y="7" width="18" height="12" rx="2"/><path d="M16 13h.01"/>',
64 ];
65 $id = strtolower($gateway_id);
66 $key = 'card';
67 if (isset($paths[strtolower($icon)])) { $key = strtolower($icon); }
68 elseif (false !== strpos($id, 'bank') || false !== strpos($id, 'transfer') || false !== strpos($id, 'wire')) { $key = 'bank'; }
69 elseif (false !== strpos($id, 'cheque') || false !== strpos($id, 'check')) { $key = 'cheque'; }
70 elseif (false !== strpos($id, 'cash')) { $key = 'cash'; }
71 elseif (false !== strpos($id, 'paypal') || false !== strpos($id, 'paystack') || false !== strpos($id, 'mollie')) { $key = 'wallet'; }
72 return '<svg ' . $a . '>' . $paths[$key] . '</svg>';
73 }
74 }
75 ?>
76
77
78 <div class="easy-invoice-payment-sidebar">
79 <div class="payment-header">
80 <div class="header-content">
81 <h2><?php esc_html_e('Payment', 'easy-invoice'); ?></h2>
82 <div class="invoice-summary">
83 <span class="invoice-number"><?php echo esc_html($invoice->getNumber()); ?></span>
84 <span class="invoice-date"><?php echo esc_html(date_i18n('M j, Y', strtotime($invoice->getIssueDate()))); ?></span>
85 </div>
86 </div>
87
88 <div class="amount-display">
89 <div class="amount-label"><?php echo esc_html((float) $invoice->getTotal() - $total_amount > 0.005 ? __('Amount Due', 'easy-invoice') : __('Total Amount', 'easy-invoice')); ?></div>
90 <div class="amount-value">
91 <?php echo esc_html(easy_invoice_format_money($total_amount, $invoice)); ?>
92 </div>
93 </div>
94 </div>
95
96 <?php if (empty($available_gateways)): ?>
97 <div class="no-payment-methods">
98 <div class="empty-state">
99 <div class="empty-icon">
100 <?php echo easy_invoice_payment_icon('card'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
101 </div>
102 <h3><?php esc_html_e('No Payment Methods', 'easy-invoice'); ?></h3>
103 <p><?php esc_html_e('Payment methods are not configured for this invoice.', 'easy-invoice'); ?></p>
104 </div>
105 </div>
106 <?php else: ?>
107 <form id="easy-invoice-payment-form" class="payment-form">
108 <input type="hidden" name="invoice_id" value="<?php echo esc_attr($invoice_id); ?>">
109 <input type="hidden" name="payment_nonce" value="<?php echo esc_attr($payment_nonce); ?>">
110 <input type="hidden" name="payment_amount" id="payment_amount" value="<?php echo esc_attr($total_amount); ?>">
111 <input type="hidden" name="payment_note" id="payment_note" value="">
112 <input type="hidden" name="action" value="easy_invoice_process_payment">
113 <input type="hidden" name="is_partial_payment" id="is_partial_payment" value="0">
114 <input type="hidden" name="payment_method" id="payment_method" value="">
115
116 <?php
117 // Add partial payments form before payment gateways (only if enabled)
118 do_action('easy_invoice_payment_gateways_before', $invoice);
119
120 ?>
121
122 <div class="payment-methods">
123 <label class="section-label"><?php esc_html_e('Payment Method', 'easy-invoice'); ?></label>
124
125 <div class="method-options">
126 <?php foreach ($available_gateways as $gateway): ?>
127 <div class="method-option" data-gateway="<?php echo esc_attr($gateway['id']); ?>">
128 <input type="radio"
129 name="payment_method_radio"
130 id="payment_method_<?php echo esc_attr($gateway['id']); ?>"
131 value="<?php echo esc_attr($gateway['id']); ?>"
132 class="method-radio">
133
134 <label for="payment_method_<?php echo esc_attr($gateway['id']); ?>" class="method-label">
135 <div class="method-content">
136 <div class="method-icon">
137 <?php echo easy_invoice_payment_icon((string) $gateway['id'], (string) ($gateway['icon'] ?? '')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- inline SVG / escaped img. ?>
138 </div>
139
140 <div class="method-details">
141 <div class="method-name"><?php echo esc_html($gateway['title']); ?></div>
142 <?php if (!empty($gateway['description'])): ?>
143 <div class="method-description"><?php echo esc_html($gateway['description']); ?></div>
144 <?php endif; ?>
145 </div>
146
147 <div class="method-check">
148 <div class="checkmark"></div>
149 </div>
150 </div>
151 </label>
152
153 <div class="gateway-content" id="gateway-content-<?php echo esc_attr($gateway['id']); ?>" style="display: none;"></div>
154 </div>
155 <?php endforeach; ?>
156 </div>
157 </div>
158
159 <div class="message-area" id="payment-message-area"></div>
160
161 <div class="submit-section">
162 <button type="submit" id="pay-now-button" class="submit-button" disabled>
163 <span class="button-text"><?php esc_html_e('Pay', 'easy-invoice'); ?> <?php echo esc_html(easy_invoice_format_money($total_amount, $invoice)); ?></span>
164 <div class="button-loader hidden">
165 <div class="loader"></div>
166 <span><?php esc_html_e('Processing...', 'easy-invoice'); ?></span>
167 </div>
168 </button>
169
170 <div class="security-note">
171 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="vertical-align:-2px;margin-right:4px;"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
172 <span><?php esc_html_e('Your payment is secure and encrypted', 'easy-invoice'); ?></span>
173 </div>
174 </div>
175 </form>
176 <?php endif; ?>
177 </div>
178
179 <style>
180 /* Clean SaaS Payment Sidebar */
181 .easy-invoice-payment-sidebar {
182 background: #ffffff;
183 height: 100vh;
184 display: flex;
185 flex-direction: column;
186 border-left: 1px solid #e5e7eb;
187 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
188 box-shadow: -4px 0 12px rgba(0, 0, 0, 0.05);
189 }
190
191 /* Header */
192 .payment-header {
193 padding: 32px 24px 24px;
194 border-bottom: 1px solid #f3f4f6;
195 background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
196 }
197
198 .header-content h2 {
199 margin: 0 0 8px 0;
200 font-size: 24px;
201 font-weight: 600;
202 color: #111827;
203 line-height: 1.2;
204 }
205
206 .invoice-summary {
207 display: flex;
208 align-items: center;
209 gap: 12px;
210 font-size: 14px;
211 color: #6b7280;
212 }
213
214 .invoice-number {
215 font-weight: 500;
216 color: #374151;
217 }
218
219 .invoice-date {
220 color: #9ca3af;
221 }
222
223 .amount-display {
224 margin-top: 20px;
225 padding: 20px;
226 background: #ffffff;
227 border: 1px solid #e5e7eb;
228 border-radius: 12px;
229 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
230 }
231
232 .amount-label {
233 font-size: 12px;
234 font-weight: 500;
235 color: #6b7280;
236 text-transform: uppercase;
237 letter-spacing: 0.5px;
238 margin-bottom: 4px;
239 }
240
241 .amount-value {
242 font-size: 18px;
243 font-weight: 700;
244 color: #111827;
245 line-height: 1;
246 }
247
248 /* No Payment Methods */
249 .no-payment-methods {
250 flex: 1;
251 display: flex;
252 align-items: center;
253 justify-content: center;
254 padding: 40px 24px;
255 }
256
257 .empty-state {
258 text-align: center;
259 max-width: 280px;
260 }
261
262 .empty-icon {
263 width: 48px;
264 height: 48px;
265 background: #f3f4f6;
266 border-radius: 12px;
267 display: flex;
268 align-items: center;
269 justify-content: center;
270 margin: 0 auto 16px;
271 color: #9ca3af;
272 font-size: 20px;
273 }
274
275 .empty-state h3 {
276 margin: 0 0 8px 0;
277 font-size: 16px;
278 font-weight: 600;
279 color: #374151;
280 }
281
282 .empty-state p {
283 margin: 0;
284 font-size: 14px;
285 color: #6b7280;
286 line-height: 1.5;
287 }
288
289 /* Payment Form */
290 .payment-form {
291 flex: 1;
292 display: flex;
293 flex-direction: column;
294 padding: 24px;
295 overflow-y: auto;
296 }
297
298 /* Payment Methods */
299 .payment-methods {
300 margin-bottom: 24px;
301 }
302
303 .section-label {
304 display: block;
305 font-size: 14px;
306 font-weight: 600;
307 color: #374151;
308 margin-bottom: 16px;
309 }
310
311 .method-options {
312 display: flex;
313 flex-direction: column;
314 gap: 12px;
315 }
316
317 /* Gateway Content */
318 .gateway-content {
319 padding: 20px;
320 background: #ffffff;
321 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
322 border-top: 2px solid #10b981;
323 }
324
325 .gateway-content h4 {
326 margin: 0 0 16px 0;
327 font-size: 16px;
328 font-weight: 600;
329 color: #111827;
330 line-height: 1.4;
331 }
332
333 .gateway-content p {
334 margin: 0 0 12px 0;
335 font-size: 14px;
336 line-height: 1.5;
337 color: #374151;
338 }
339
340 .gateway-content .bank-details,
341 .gateway-content .payment-reference {
342 margin: 16px 0;
343 padding: 16px;
344 background: #f8fafc;
345 border-radius: 8px;
346 border: 1px solid #e5e7eb;
347 }
348
349 .gateway-content .payment-reference {
350 background: #f0f7ff;
351 border-left: 4px solid #0073aa;
352 }
353
354 .gateway-content .reference-note {
355 font-style: italic;
356 color: #6b7280;
357 margin-top: 8px;
358 font-size: 13px;
359 }
360
361 .gateway-content .ei-offline h4 { margin: 0 0 10px; font-size: 15px; font-weight: 600; color: #111827; }
362 .gateway-content .ei-offline__details { width: 100%; border-collapse: collapse; margin-bottom: 12px; font-size: 13px; }
363 .gateway-content .ei-offline__details th { text-align: left; padding: 6px 8px 6px 0; color: #6b7280; font-weight: 500; width: 40%; vertical-align: top; }
364 .gateway-content .ei-offline__details td { padding: 6px 0; color: #111827; font-weight: 600; word-break: break-word; }
365 .gateway-content .ei-offline__instructions { font-size: 13px; color: #374151; line-height: 1.5; margin-bottom: 10px; }
366 .gateway-content .ei-offline__instructions p { margin: 0 0 8px; }
367 .gateway-content .ei-offline__form { margin-top: 14px; padding-top: 14px; border-top: 1px solid #e5e7eb; }
368 .gateway-content .ei-offline__lead { font-size: 13px; color: #374151; margin: 0 0 10px; }
369 .gateway-content .ei-offline__form .form-group { margin-bottom: 12px; }
370 .gateway-content .ei-offline__form label { display: block; font-size: 13px; font-weight: 600; color: #111827; margin-bottom: 4px; }
371 .gateway-content .ei-offline__optional { font-weight: 400; color: #6b7280; }
372 .gateway-content .ei-offline__required { color: #dc2626; }
373 .gateway-content .ei-offline__form input[type="text"],
374 .gateway-content .ei-offline__form textarea { width: 100%; box-sizing: border-box; border: 1px solid #d1d5db; border-radius: 8px; padding: 9px 12px; font-size: 14px; background: #fff; }
375 .gateway-content .ei-offline__form input[type="file"] { display: block; width: 100%; font-size: 13px; color: #374151; }
376 .gateway-content .ei-offline__form .reference-note { margin: 4px 0 0; font-size: 12px; color: #6b7280; }
377 .gateway-content .payment-proof-form {
378 margin-top: 24px;
379 padding-top: 20px;
380 border-top: 1px solid #e5e7eb;
381 }
382
383
384
385 /* Gateway-specific instruction styles */
386 .gateway-content .bank-transfer-instructions,
387 .gateway-content .cheque-payment-instructions,
388 .gateway-content .cash-payment-instructions {
389 margin: 0;
390 padding: 0;
391 background: transparent;
392 border: none;
393 border-radius: 0;
394 border-left: none;
395 }
396
397 .gateway-content .bank-details-table {
398 width: 100%;
399 border-collapse: collapse;
400 margin-bottom: 16px;
401 }
402
403 .gateway-content .bank-details-table th,
404 .gateway-content .bank-details-table td {
405 padding: 8px 12px;
406 border-bottom: 1px solid #e5e7eb;
407 text-align: left;
408 }
409
410 .gateway-content .bank-details-table th {
411 width: 40%;
412 font-weight: 600;
413 color: #374151;
414 }
415
416 .gateway-content .cheque-details,
417 .gateway-content .cash-details {
418 margin-bottom: 16px;
419 }
420
421 .gateway-content .payable-to,
422 .gateway-content .invoice-number {
423 font-size: 16px;
424 margin: 8px 0 16px;
425 color: #0073aa;
426 font-weight: 600;
427 }
428
429 .gateway-content .mailing-address {
430 padding: 12px;
431 background: #f0f7ff;
432 border-left: 3px solid #0073aa;
433 margin-top: 8px;
434 border-radius: 6px;
435 }
436
437 .gateway-content .cheque-notification-form {
438 margin-top: 20px;
439 border-top: 1px solid #e5e7eb;
440 padding-top: 16px;
441 }
442
443 .gateway-content .submit-notification-btn {
444 background: #0073aa;
445 color: white;
446 padding: 10px 20px;
447 border: none;
448 border-radius: 6px;
449 cursor: pointer;
450 font-size: 14px;
451 font-weight: 500;
452 transition: background-color 0.2s ease;
453 }
454
455 .gateway-content .submit-notification-btn:hover {
456 background: #005d8c;
457 }
458
459 .gateway-content .submit-notification-btn:disabled {
460 background: #9ca3af;
461 cursor: not-allowed;
462 }
463
464
465
466 .method-option {
467 border: 2px solid #e5e7eb;
468 border-radius: 12px;
469 transition: all 0.2s ease;
470 background: #ffffff;
471 overflow: hidden;
472 }
473
474 .method-option:hover {
475 border-color: #d1d5db;
476 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
477 transform: translateY(-1px);
478 }
479
480 .method-option.selected {
481 border-color: #10b981;
482 box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
483 background: #f0fdf4;
484 }
485
486 .method-radio {
487 display: none;
488 }
489
490 .method-label {
491 display: block;
492 padding: 20px;
493 cursor: pointer;
494 margin: 0;
495 }
496
497 .method-content {
498 display: flex;
499 align-items: center;
500 gap: 16px;
501 }
502
503 .method-icon {
504 width: 40px;
505 height: 40px;
506 background: #f9fafb;
507 border-radius: 10px;
508 display: flex;
509 align-items: center;
510 justify-content: center;
511 color: #6b7280;
512 font-size: 18px;
513 flex-shrink: 0;
514 transition: all 0.2s ease;
515 }
516
517 .method-option.selected .method-icon {
518 background: #10b981;
519 color: #ffffff;
520 }
521
522 .method-details {
523 flex: 1;
524 min-width: 0;
525 }
526
527 .method-name {
528 font-weight: 600;
529 font-size: 15px;
530 color: #111827;
531 margin-bottom: 4px;
532 }
533
534 .method-description {
535 font-size: 13px;
536 color: #6b7280;
537 line-height: 1.4;
538 }
539
540 .method-check {
541 flex-shrink: 0;
542 }
543
544 .checkmark {
545 width: 24px;
546 height: 24px;
547 border: 2px solid #d1d5db;
548 border-radius: 50%;
549 position: relative;
550 transition: all 0.2s ease;
551 }
552
553 .checkmark::after {
554 content: '';
555 position: absolute;
556 top: 50%;
557 left: 50%;
558 transform: translate(-50%, -50%);
559 width: 10px;
560 height: 10px;
561 background: #ffffff;
562 border-radius: 50%;
563 opacity: 0;
564 transition: opacity 0.2s ease;
565 }
566
567 .method-option.selected .checkmark {
568 background: #10b981;
569 border-color: #10b981;
570 }
571
572 .method-option.selected .checkmark::after {
573 opacity: 1;
574 }
575
576
577
578 /* Message Area */
579 .message-area {
580 margin-bottom: 24px;
581 min-height: 20px;
582 }
583
584 .payment-message {
585 padding: 16px 20px;
586 border-radius: 8px;
587 font-size: 14px;
588 display: flex;
589 align-items: center;
590 gap: 12px;
591 border: 1px solid;
592 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
593 }
594
595 .payment-message.success {
596 background: #f0fdf4;
597 color: #166534;
598 border-color: #bbf7d0;
599 }
600
601 .payment-message.error {
602 background: #fef2f2;
603 color: #dc2626;
604 border-color: #fecaca;
605 }
606
607 .payment-message.warning {
608 background: #fffbeb;
609 color: #d97706;
610 border-color: #fed7aa;
611 }
612
613 .payment-message.info {
614 background: #eff6ff;
615 color: #1d4ed8;
616 border-color: #bfdbfe;
617 }
618
619
620
621 /* Submit Section */
622 .submit-section {
623 margin-top: auto;
624 padding-top: 24px;
625 border-top: 1px solid #f3f4f6;
626 }
627
628 .submit-button {
629 width: 100%;
630 padding: 18px 24px;
631 background: linear-gradient(135deg, #10b981 0%, #059669 100%);
632 color: #ffffff;
633 border: none;
634 border-radius: 12px;
635 font-size: 16px;
636 font-weight: 600;
637 cursor: pointer;
638 transition: all 0.3s ease;
639 display: flex;
640 align-items: center;
641 justify-content: center;
642 gap: 10px;
643 position: relative;
644 box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
645 }
646
647 .submit-button:hover:not(:disabled) {
648 background: linear-gradient(135deg, #059669 0%, #047857 100%);
649 transform: translateY(-2px);
650 box-shadow: 0 8px 20px rgba(16, 185, 129, 0.4);
651 }
652
653 .submit-button:active:not(:disabled) {
654 transform: translateY(0);
655 box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
656 }
657
658 .submit-button:disabled {
659 background: #9ca3af;
660 cursor: not-allowed;
661 transform: none;
662 box-shadow: none;
663 }
664
665 .button-loader {
666 display: flex;
667 align-items: center;
668 gap: 10px;
669 }
670
671 .button-loader.hidden {
672 display: none;
673 }
674
675 .loader {
676 width: 18px;
677 height: 18px;
678 border: 2px solid rgba(255, 255, 255, 0.3);
679 border-top: 2px solid #ffffff;
680 border-radius: 50%;
681 animation: spin 1s linear infinite;
682 }
683
684 @keyframes spin {
685 0% { transform: rotate(0deg); }
686 100% { transform: rotate(360deg); }
687 }
688
689 .security-note {
690 display: flex;
691 align-items: center;
692 justify-content: center;
693 gap: 8px;
694 margin-top: 16px;
695 font-size: 13px;
696 color: #6b7280;
697 padding: 12px;
698 background: #f9fafb;
699 border-radius: 8px;
700 }
701
702 .security-note i {
703 font-size: 14px;
704 color: #10b981;
705 }
706
707 /* Responsive */
708 @media (max-width: 768px) {
709 .easy-invoice-payment-sidebar {
710 width: 100%;
711 height: 100vh;
712 }
713
714 .payment-header {
715 padding: 24px 20px 20px;
716 }
717
718 .payment-form {
719 padding: 20px;
720 }
721
722 .method-label {
723 padding: 16px;
724 }
725
726 .submit-button {
727 padding: 16px 20px;
728 }
729 }
730
731 /* Scrollbar */
732 .payment-form::-webkit-scrollbar {
733 width: 6px;
734 }
735
736 .payment-form::-webkit-scrollbar-track {
737 background: #f1f5f9;
738 border-radius: 3px;
739 }
740
741 .payment-form::-webkit-scrollbar-thumb {
742 background: #cbd5e1;
743 border-radius: 3px;
744 }
745
746 .payment-form::-webkit-scrollbar-thumb:hover {
747 background: #94a3b8;
748 }
749
750
751
752 /* Utility */
753 .hidden {
754 display: none !important;
755 }
756 </style>
757
758 <script>
759 document.addEventListener('DOMContentLoaded', function() {
760 const paymentForm = document.getElementById('easy-invoice-payment-form');
761 const payButton = document.getElementById('pay-now-button');
762 const messageArea = document.getElementById('payment-message-area');
763 const paymentMethods = document.querySelectorAll('input[name="payment_method_radio"]');
764 const paymentAmountInput = document.getElementById('payment_amount');
765
766 window.selectedGateway = null;
767
768 // Initialize payment methods
769 paymentMethods.forEach(method => {
770 method.addEventListener('change', handlePaymentMethodChange);
771 });
772
773 // Handle payment amount changes
774 if (paymentAmountInput) {
775 paymentAmountInput.addEventListener('input', updatePayButtonText);
776 paymentAmountInput.addEventListener('change', updatePayButtonText);
777 }
778
779 // Initialize button text on page load (always call this)
780 updatePayButtonText();
781
782 // Also call it after a short delay to ensure all elements are loaded
783 setTimeout(() => {
784 updatePayButtonText();
785 }, 500);
786
787 // Handle form submission
788 paymentForm.addEventListener('submit', handlePaymentSubmit);
789
790 function handlePaymentMethodChange(e) {
791 window.selectedGateway = e.target.value;
792 const methodEntry = e.target.closest('.method-option');
793
794 // Update UI
795 document.querySelectorAll('.method-option').forEach(entry => {
796 entry.classList.remove('selected');
797 });
798 methodEntry.classList.add('selected');
799
800 // Update hidden payment_method field
801 const paymentMethodField = document.getElementById('payment_method');
802 if (paymentMethodField) {
803 paymentMethodField.value = window.selectedGateway;
804 }
805
806 // Enable pay button
807 payButton.disabled = false;
808
809 // Load gateway-specific content
810 loadGatewayContent(window.selectedGateway);
811 }
812
813 function loadGatewayContent(gateway) {
814 // Find the specific gateway content div for this gateway
815 const contentDiv = document.getElementById('gateway-content-' + gateway);
816
817 if (!contentDiv) {
818 console.error('Gateway content div not found for:', gateway);
819 return;
820 }
821
822 // Hide all gateway content divs first
823 document.querySelectorAll('.gateway-content').forEach(div => {
824 div.style.display = 'none';
825 });
826
827 // Clear existing content
828 contentDiv.innerHTML = '';
829
830
831 // Load gateway content via AJAX
832 const formData = new FormData();
833 formData.append('action', 'easy_invoice_get_payment_instructions');
834 formData.append('gateway', gateway);
835 formData.append('invoice_id', document.querySelector('input[name="invoice_id"]').value);
836 formData.append('nonce', document.querySelector('input[name="payment_nonce"]').value);
837 // Per-invoice access token from the emailed link (?ik=...); the server
838 // authorises the request on it. Without it an anonymous client holding a
839 // legitimate link could not load their own payment instructions.
840 formData.append('access_token', <?php echo wp_json_encode($ei_ps_access_token); ?>);
841
842 fetch('<?php echo esc_url(admin_url('admin-ajax.php')); ?>', {
843 method: 'POST',
844 body: formData
845 })
846 .then(response => {
847 if (!response.ok) {
848 throw new Error('Network response was not ok');
849 }
850 return response.json();
851 })
852 .then(response => {
853 if (response.success && response.data.instructions) {
854 contentDiv.innerHTML = response.data.instructions;
855 contentDiv.style.display = 'block';
856 applyOfflineState(contentDiv);
857 } else {
858 contentDiv.style.display = 'none';
859 }
860 })
861 .catch(error => {
862 console.error('Error loading gateway content:', error);
863 contentDiv.style.display = 'none';
864 });
865 }
866
867 function loadGatewayContentViaAjax(gateway, contentDiv) {
868 const formData = new FormData();
869 formData.append('action', 'easy_invoice_get_payment_instructions');
870 formData.append('gateway', gateway);
871 formData.append('invoice_id', document.querySelector('input[name="invoice_id"]').value);
872 formData.append('nonce', document.querySelector('input[name="payment_nonce"]').value);
873 formData.append('access_token', <?php echo wp_json_encode($ei_ps_access_token); ?>);
874
875 fetch('<?php echo esc_url(admin_url('admin-ajax.php')); ?>', {
876 method: 'POST',
877 body: formData
878 })
879 .then(response => response.json())
880 .then(response => {
881 if (response.success && response.data.instructions) {
882 contentDiv.innerHTML = response.data.instructions;
883 contentDiv.style.display = 'block';
884 } else {
885 contentDiv.style.display = 'none';
886 }
887 })
888 .catch(error => {
889 console.error('Error loading gateway content:', error);
890 contentDiv.style.display = 'none';
891 });
892 }
893
894
895
896 // An offline method (bank transfer, cheque, cash, …) is not charged here:
897 // the button becomes "Submit payment details" and a required receipt is
898 // checked before anything is sent.
899 window.eiOfflineSelected = false;
900 function applyOfflineState(contentDiv) {
901 var offline = contentDiv ? contentDiv.querySelector('[data-ei-offline]') : null;
902 window.eiOfflineSelected = !!offline;
903 if (!payButton || !payButton.querySelector('.button-text')) { return; }
904 if (offline) {
905 payButton.querySelector('.button-text').textContent = offline.getAttribute('data-ei-submit-label') || 'Submit payment details';
906 } else {
907 updatePayButtonText();
908 }
909 }
910
911 function handlePaymentSubmit(e) {
912 e.preventDefault();
913 payButton.disabled = true;
914
915 if (!window.selectedGateway) {
916 showPaymentMessage('Please select a payment method first.', 'error');
917 payButton.disabled = false;
918 return;
919 }
920 if (window.eiOfflineSelected) {
921 var required = paymentForm.querySelector('input[type="file"][data-ei-required]');
922 if (required && !(required.files && required.files.length)) {
923 showPaymentMessage(<?php echo wp_json_encode(__('Please attach your receipt or proof of payment.', 'easy-invoice')); ?>, 'error');
924 payButton.disabled = false;
925 required.focus();
926 return;
927 }
928 }
929
930 // Show loading state
931 payButton.querySelector('.button-text').classList.add('hidden');
932 payButton.querySelector('.button-loader').classList.remove('hidden');
933
934 // Handle all payment methods (PayPal, etc.)
935 processStandardPayment();
936 }
937
938
939
940 function processStandardPayment() {
941 const formData = new FormData(paymentForm);
942 formData.append('payment_method', window.selectedGateway);
943 // The per-invoice access token from the emailed link. processPayment()
944 // authorises on it before anything else; without it a client who
945 // arrived by their own link was refused with "Invalid invoice" the
946 // moment they chose bank transfer, cheque or cash.
947 formData.append('access_token', <?php echo wp_json_encode($ei_ps_access_token); ?>);
948
949 // Get AJAX URL from WordPress
950 const ajaxUrl = '<?php echo esc_url(admin_url('admin-ajax.php')); ?>';
951
952 fetch(ajaxUrl, {
953 method: 'POST',
954 body: formData
955 })
956 .then(response => response.json())
957 .then(response => {
958 if (response.success) {
959 handlePaymentSuccess(response.data);
960 } else {
961 const errorMsg = response.data && response.data.message
962 ? response.data.message
963 : 'Payment processing failed.';
964 showPaymentMessage(errorMsg, 'error');
965 resetPayButton();
966 }
967 })
968 .catch(error => {
969 console.error('Payment AJAX error:', error);
970 showPaymentMessage('Server error during payment. Please try again.', 'error');
971 resetPayButton();
972 });
973 }
974
975
976
977 function handlePaymentSuccess(data) {
978 // Handle redirect if provided (for PayPal and other external gateways)
979 if (data.redirect_url) {
980 // Get gateway-specific message or use default
981 const redirectMessage = data.message || 'Redirecting to payment gateway to complete your payment...';
982
983 // Update button state to show redirecting
984 payButton.querySelector('.button-text').textContent = 'Processing...';
985 payButton.querySelector('.button-loader').classList.add('hidden');
986 payButton.querySelector('.button-text').classList.remove('hidden');
987 payButton.disabled = true;
988
989 // Show redirect message
990 showPaymentMessage(redirectMessage, 'info');
991
992 // Redirect immediately
993 setTimeout(() => {
994 window.location.href = data.redirect_url;
995 }, 1000);
996 return;
997 }
998
999 if (data.offline) {
1000 payButton.querySelector('.button-loader').classList.add('hidden');
1001 payButton.querySelector('.button-text').classList.remove('hidden');
1002 payButton.querySelector('.button-text').textContent = <?php echo wp_json_encode(__('Submitted', 'easy-invoice')); ?>;
1003 payButton.disabled = true;
1004 showPaymentMessage(data.message || 'Thank you — we will confirm your payment shortly.', 'success');
1005 setTimeout(function () { window.location.reload(); }, 2500);
1006 return;
1007 }
1008 // For successful payments without redirect
1009 // Update button state
1010 payButton.querySelector('.button-text').textContent = 'Payment Successful!';
1011 payButton.querySelector('.button-loader').classList.add('hidden');
1012 payButton.querySelector('.button-text').classList.remove('hidden');
1013 payButton.disabled = true;
1014
1015 // Show success message
1016 showPaymentMessage(data.message || 'Payment successful!', 'success');
1017
1018 // Reload the page after delay
1019 setTimeout(() => {
1020 window.location.reload();
1021 }, 2000);
1022 }
1023
1024 function showPaymentMessage(message, type) {
1025 let icon = 'info-circle';
1026 if (type === 'success') {
1027 icon = 'check-circle';
1028 } else if (type === 'error') {
1029 icon = 'exclamation-triangle';
1030 } else if (type === 'warning') {
1031 icon = 'exclamation-triangle';
1032 }
1033
1034 messageArea.innerHTML = `
1035 <div class="payment-message ${type}">
1036 ${message}
1037 </div>
1038 `;
1039 }
1040
1041 function updatePayButtonText() {
1042 // No button when the panel has no payment methods to offer.
1043 if (!payButton || !payButton.querySelector('.button-text')) { return; }
1044 if (window.eiOfflineSelected) { return; }
1045 const hiddenPaymentAmount = document.getElementById('payment_amount');
1046 const paymentAmount = hiddenPaymentAmount ? hiddenPaymentAmount.value : '0';
1047 const money = <?php echo wp_json_encode(easy_invoice_money_format_spec($invoice)); ?>;
1048 const payLabel = <?php echo wp_json_encode(__('Pay', 'easy-invoice')); ?>;
1049
1050 if (paymentAmount && paymentAmount > 0) {
1051 // Same rules as the PHP formatter: separators, precision, symbol position.
1052 const n = parseFloat(paymentAmount).toFixed(money.precision);
1053 const [int, dec] = n.split('.');
1054 const grouped = int.replace(/\B(?=(\d{3})+(?!\d))/g, money.thousands);
1055 const number = dec !== undefined ? grouped + money.decimal + dec : grouped;
1056 const formatted = (money.position === 'after' || money.position === 'right') ? number + money.symbol
1057 : (money.position === 'right_space') ? number + ' ' + money.symbol
1058 : (money.position === 'left_space') ? money.symbol + ' ' + number
1059 : money.symbol + number;
1060 payButton.querySelector('.button-text').textContent = `${payLabel} ${formatted}`;
1061 } else {
1062 payButton.querySelector('.button-text').textContent = 'Pay';
1063 }
1064 }
1065
1066 // Make the function globally accessible
1067 window.updatePayButtonText = updatePayButtonText;
1068
1069 function resetPayButton() {
1070 payButton.querySelector('.button-text').textContent = 'Pay Now';
1071 payButton.querySelector('.button-loader').classList.add('hidden');
1072 payButton.querySelector('.button-text').classList.remove('hidden');
1073 payButton.disabled = false;
1074 }
1075
1076
1077
1078
1079 });
1080 </script>
1081
1082 <?php
1083 // Allow Pro plugins to add additional variables
1084 do_action('easy_invoice_payment_form_variables');
1085 ?>