recur-tw 0.9.8 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { html, render, nothing } from 'lit-html';
1
2
  import React, { createContext, useState, useCallback, useMemo, useContext, useEffect } from 'react';
2
3
  import { jsx } from 'react/jsx-runtime';
3
4
 
@@ -604,14 +605,28 @@ var init_skeleton_loader = __esm({
604
605
  }
605
606
  });
606
607
 
608
+ // src/components/styles/payment-form-skeleton.css
609
+ var payment_form_skeleton_default;
610
+ var init_payment_form_skeleton = __esm({
611
+ "src/components/styles/payment-form-skeleton.css"() {
612
+ payment_form_skeleton_default = "/**\n * Payment Form Skeleton - Shadow DOM Styles\n * Loading skeleton that matches the structure of RecurPaymentForm\n */\n\n:host {\n display: block;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.payment-form-wrapper {\n max-width: 500px;\n margin: 0 auto;\n padding: 24px;\n background: white;\n border-radius: 12px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n}\n\n/* Header */\n.form-header {\n margin-bottom: 24px;\n}\n\n.form-title {\n font-size: 20px;\n font-weight: 600;\n color: #1a202c;\n margin: 0 0 4px 0;\n}\n\n.form-subtitle {\n font-size: 14px;\n color: #718096;\n margin: 0;\n}\n\n/* Sections */\n.form-section {\n margin-bottom: 24px;\n}\n\n.section-title {\n font-size: 14px;\n font-weight: 600;\n color: #2d3748;\n margin: 0 0 12px 0;\n}\n\n.label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #2d3748;\n margin-bottom: 6px;\n}\n\n/* Skeleton base styles */\n.skeleton {\n background: linear-gradient(\n 90deg,\n #f0f0f0 0%,\n #e8e8e8 50%,\n #f0f0f0 100%\n );\n background-size: 200% 100%;\n animation: skeleton-loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n}\n\n@keyframes skeleton-loading {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n}\n\n/* Order Summary */\n.order-summary {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n padding: 16px;\n}\n\n.order-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n\n.order-row:last-child {\n margin-bottom: 0;\n}\n\n.order-row--total {\n padding-top: 12px;\n border-top: 1px solid #e2e8f0;\n}\n\n.order-label {\n font-size: 14px;\n color: #4a5568;\n}\n\n.skeleton-order-value {\n height: 20px;\n width: 80px;\n}\n\n.skeleton-order-total {\n height: 26px;\n width: 100px;\n}\n\n/* Coupon Skeleton */\n.coupon-section {\n padding: 12px 0;\n border-top: 1px solid #e2e8f0;\n margin-top: 12px;\n min-height: 36px;\n display: flex;\n align-items: center;\n}\n\n.skeleton-coupon-trigger {\n height: 20px;\n width: 88px;\n}\n\n/* Customer Info Display */\n.customer-info-display {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 6px;\n padding: 12px;\n margin-bottom: 8px;\n}\n\n.info-row {\n margin-bottom: 12px;\n}\n\n.info-row:last-child {\n margin-bottom: 0;\n}\n\n.info-label {\n display: block;\n font-size: 12px;\n line-height: 16px;\n color: #718096;\n margin-bottom: 2px;\n}\n\n.skeleton-info-value {\n height: 20px;\n width: 120px;\n}\n\n/* Card Fields */\n.card-field {\n margin-bottom: 16px;\n}\n\n.card-field:last-child {\n margin-bottom: 0;\n}\n\n.skeleton-input {\n height: 36px;\n width: 100%;\n border-radius: 6px;\n}\n\n.card-row {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n column-gap: 16px;\n}\n\n/* Submit Button - matches .recur-submit-button: padding 14px*2 + font 16px * line-height ~1.5 */\n.skeleton-button {\n height: 52px;\n width: 100%;\n border-radius: 6px;\n}\n\n/* Security Note */\n.security-note {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px;\n background: #f7fafc;\n border-radius: 6px;\n margin-top: 16px;\n}\n\n.security-icon {\n color: #48bb78;\n}\n\n.security-text {\n font-size: 13px;\n color: #4a5568;\n margin: 0;\n}\n";
613
+ }
614
+ });
615
+
607
616
  // src/components/base/payment-form-skeleton.ts
608
617
  var payment_form_skeleton_exports = {};
609
618
  __export(payment_form_skeleton_exports, {
610
619
  RecurPaymentFormSkeleton: () => RecurPaymentFormSkeleton
611
620
  });
612
- var RecurPaymentFormSkeleton;
613
- var init_payment_form_skeleton = __esm({
621
+ var lockIcon, RecurPaymentFormSkeleton;
622
+ var init_payment_form_skeleton2 = __esm({
614
623
  "src/components/base/payment-form-skeleton.ts"() {
624
+ init_payment_form_skeleton();
625
+ lockIcon = html`
626
+ <svg class="security-icon" width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
627
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
628
+ </svg>
629
+ `;
615
630
  RecurPaymentFormSkeleton = class extends HTMLElement {
616
631
  constructor() {
617
632
  super();
@@ -620,275 +635,106 @@ var init_payment_form_skeleton = __esm({
620
635
  connectedCallback() {
621
636
  this.render();
622
637
  }
623
- render() {
624
- this.shadowRoot.innerHTML = `
625
- <style>
626
- :host {
627
- display: block;
628
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
629
- }
630
-
631
- .payment-form-wrapper {
632
- max-width: 500px;
633
- margin: 0 auto;
634
- padding: 24px;
635
- background: white;
636
- border-radius: 12px;
637
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
638
- }
639
-
640
- .form-header {
641
- margin-bottom: 24px;
642
- }
643
-
644
- .form-title {
645
- font-size: 20px;
646
- font-weight: 600;
647
- color: #1a202c;
648
- margin: 0 0 4px 0;
649
- }
650
-
651
- .form-subtitle {
652
- font-size: 14px;
653
- color: #718096;
654
- margin: 0;
655
- }
656
-
657
- .form-section {
658
- margin-bottom: 24px;
659
- }
660
-
661
- .section-title {
662
- font-size: 14px;
663
- font-weight: 600;
664
- color: #2d3748;
665
- margin: 0 0 12px 0;
666
- }
667
-
668
- .label {
669
- display: block;
670
- font-size: 14px;
671
- font-weight: 500;
672
- color: #2d3748;
673
- margin-bottom: 6px;
674
- }
675
-
676
- /* Skeleton base styles */
677
- .skeleton {
678
- background: linear-gradient(
679
- 90deg,
680
- #f0f0f0 0%,
681
- #e8e8e8 50%,
682
- #f0f0f0 100%
683
- );
684
- background-size: 200% 100%;
685
- animation: skeleton-loading 1.5s ease-in-out infinite;
686
- border-radius: 4px;
687
- }
688
-
689
- @keyframes skeleton-loading {
690
- 0% {
691
- background-position: 200% 0;
692
- }
693
- 100% {
694
- background-position: -200% 0;
695
- }
696
- }
697
-
698
- /* Order Summary */
699
- .order-summary {
700
- background: #f7fafc;
701
- border: 1px solid #e2e8f0;
702
- border-radius: 8px;
703
- padding: 16px;
704
- margin-bottom: 24px;
705
- }
706
-
707
- .order-summary-title {
708
- font-size: 14px;
709
- font-weight: 600;
710
- color: #2d3748;
711
- margin: 0 0 12px 0;
712
- }
713
-
714
- .order-row {
715
- display: flex;
716
- justify-content: space-between;
717
- align-items: center;
718
- margin-bottom: 8px;
719
- }
720
-
721
- .order-row:last-child {
722
- margin-bottom: 0;
723
- padding-top: 12px;
724
- border-top: 1px solid #e2e8f0;
725
- }
726
-
727
- .order-label {
728
- font-size: 14px;
729
- color: #4a5568;
730
- }
731
-
732
- .skeleton-order-value {
733
- height: 20px;
734
- width: 80px;
735
- }
736
-
737
- .skeleton-order-total {
738
- height: 26px;
739
- width: 100px;
740
- }
741
-
742
- /* Customer Info Display */
743
- .customer-info-display {
744
- background: #f7fafc;
745
- border: 1px solid #e2e8f0;
746
- border-radius: 6px;
747
- padding: 12px;
748
- margin-bottom: 8px;
749
- }
750
-
751
- .info-row {
752
- margin-bottom: 12px;
753
- }
754
-
755
- .info-row:last-child {
756
- margin-bottom: 0;
757
- }
758
-
759
- .info-label {
760
- display: block;
761
- font-size: 12px;
762
- line-height: 16px;
763
- color: #718096;
764
- margin-bottom: 2px;
765
- }
766
-
767
- .skeleton-info-value {
768
- height: 20px;
769
- width: 120px;
770
- }
771
-
772
- /* Card Fields */
773
- .card-field {
774
- margin-bottom: 16px;
775
- }
776
-
777
- .card-field:last-child {
778
- margin-bottom: 0;
779
- }
780
-
781
- .skeleton-input {
782
- height: 36px;
783
- width: 100%;
784
- border-radius: 6px;
785
- }
786
-
787
- .card-row {
788
- display: grid;
789
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
790
- column-gap: 16px;
791
- }
792
-
793
- /* Submit Button - matches .recur-submit-button: padding 14px*2 + font 16px * line-height ~1.5 */
794
- .skeleton-button {
795
- height: 52px;
796
- width: 100%;
797
- border-radius: 6px;
798
- }
799
-
800
- /* Security Note */
801
- .security-note {
802
- display: flex;
803
- align-items: center;
804
- gap: 8px;
805
- padding: 12px;
806
- background: #f7fafc;
807
- border-radius: 6px;
808
- margin-top: 16px;
809
- }
810
-
811
- .security-icon {
812
- color: #48bb78;
813
- }
814
-
815
- .security-text {
816
- font-size: 13px;
817
- color: #4a5568;
818
- margin: 0;
819
- }
820
- </style>
821
-
822
- <div class="payment-form-wrapper">
823
- <!-- Header (matches payment-form.ts) -->
824
- <div class="form-header">
825
- <h2 class="form-title">\u8A02\u95B1\u4ED8\u6B3E</h2>
826
- <p class="form-subtitle">\u8ACB\u586B\u5BEB\u4FE1\u7528\u5361\u8CC7\u8A0A\u5B8C\u6210\u8A02\u95B1</p>
827
- </div>
828
-
829
- <!-- Order Summary (matches createOrderSummarySection) -->
638
+ // ============================================
639
+ // Section Renderers
640
+ // ============================================
641
+ renderHeader() {
642
+ return html`
643
+ <div class="form-header">
644
+ <h2 class="form-title">訂閱付款</h2>
645
+ </div>
646
+ `;
647
+ }
648
+ renderOrderSummary() {
649
+ return html`
650
+ <div class="form-section">
651
+ <h3 class="section-title">訂單摘要</h3>
830
652
  <div class="order-summary">
831
- <h3 class="order-summary-title">\u8A02\u55AE\u6458\u8981</h3>
832
653
  <div class="order-row">
833
- <span class="order-label">\u8A02\u95B1\u65B9\u6848</span>
654
+ <span class="order-label">訂閱方案</span>
834
655
  <div class="skeleton skeleton-order-value"></div>
835
656
  </div>
836
657
  <div class="order-row">
837
- <span class="order-label">\u8A08\u8CBB\u9031\u671F</span>
658
+ <span class="order-label">計費週期</span>
838
659
  <div class="skeleton skeleton-order-value"></div>
839
660
  </div>
840
- <div class="order-row">
841
- <span class="order-label">\u7E3D\u8A08</span>
661
+ <div class="coupon-section">
662
+ <div class="skeleton skeleton-coupon-trigger"></div>
663
+ </div>
664
+ <div class="order-row order-row--total">
665
+ <span class="order-label">總計</span>
842
666
  <div class="skeleton skeleton-order-total"></div>
843
667
  </div>
844
668
  </div>
845
-
846
- <!-- Customer Info Section (matches createCustomerInfoSection read-only display) -->
847
- <div class="form-section">
848
- <h3 class="section-title">\u806F\u7D61\u8CC7\u8A0A</h3>
849
- <div class="customer-info-display">
850
- <div class="info-row">
851
- <span class="info-label">\u59D3\u540D</span>
852
- <div class="skeleton skeleton-info-value"></div>
853
- </div>
854
- <div class="info-row">
855
- <span class="info-label">\u96FB\u5B50\u90F5\u4EF6</span>
856
- <div class="skeleton skeleton-info-value" style="width: 160px;"></div>
857
- </div>
669
+ </div>
670
+ `;
671
+ }
672
+ renderCustomerInfo() {
673
+ return html`
674
+ <div class="form-section">
675
+ <h3 class="section-title">聯絡資訊</h3>
676
+ <div class="customer-info-display">
677
+ <div class="info-row">
678
+ <span class="info-label">姓名</span>
679
+ <div class="skeleton skeleton-info-value"></div>
680
+ </div>
681
+ <div class="info-row">
682
+ <span class="info-label">電子郵件</span>
683
+ <div class="skeleton skeleton-info-value" style="width: 160px;"></div>
858
684
  </div>
859
685
  </div>
860
-
861
- <!-- Card Fields Section (matches createCardFieldsSection) -->
862
- <div class="form-section">
863
- <h3 class="section-title">\u4FE1\u7528\u5361\u8CC7\u8A0A</h3>
686
+ </div>
687
+ `;
688
+ }
689
+ renderCardFields() {
690
+ return html`
691
+ <div class="form-section">
692
+ <h3 class="section-title">信用卡資訊</h3>
693
+ <div class="card-field">
694
+ <label class="label">卡號</label>
695
+ <div class="skeleton skeleton-input"></div>
696
+ </div>
697
+ <div class="card-row">
864
698
  <div class="card-field">
865
- <label class="label">\u5361\u865F</label>
699
+ <label class="label">有效期限</label>
866
700
  <div class="skeleton skeleton-input"></div>
867
701
  </div>
868
- <div class="card-row">
869
- <div class="card-field">
870
- <label class="label">\u6709\u6548\u671F\u9650</label>
871
- <div class="skeleton skeleton-input"></div>
872
- </div>
873
- <div class="card-field">
874
- <label class="label">\u5B89\u5168\u78BC</label>
875
- <div class="skeleton skeleton-input"></div>
876
- </div>
702
+ <div class="card-field">
703
+ <label class="label">安全碼</label>
704
+ <div class="skeleton skeleton-input"></div>
877
705
  </div>
878
706
  </div>
707
+ </div>
708
+ `;
709
+ }
710
+ renderSubmitButton() {
711
+ return html`<div class="skeleton skeleton-button"></div>`;
712
+ }
713
+ renderSecurityNote() {
714
+ return html`
715
+ <div class="security-note">
716
+ ${lockIcon}
717
+ <p class="security-text">您的付款資訊經過加密保護</p>
718
+ </div>
719
+ `;
720
+ }
721
+ // ============================================
722
+ // Main Render
723
+ // ============================================
724
+ render() {
725
+ const template = html`
726
+ <style>${payment_form_skeleton_default}</style>
879
727
 
880
- <!-- Submit Button -->
881
- <div class="skeleton skeleton-button"></div>
882
-
883
- <!-- Security Note (matches payment-form.ts) -->
884
- <div class="security-note">
885
- <svg class="security-icon" width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
886
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
887
- </svg>
888
- <p class="security-text">\u60A8\u7684\u4ED8\u6B3E\u8CC7\u8A0A\u7D93\u904E\u52A0\u5BC6\u4FDD\u8B77</p>
889
- </div>
728
+ <div class="payment-form-wrapper">
729
+ ${this.renderHeader()}
730
+ ${this.renderOrderSummary()}
731
+ ${this.renderCustomerInfo()}
732
+ ${this.renderCardFields()}
733
+ ${this.renderSubmitButton()}
734
+ ${this.renderSecurityNote()}
890
735
  </div>
891
736
  `;
737
+ render(template, this.shadowRoot);
892
738
  }
893
739
  };
894
740
  if (typeof window !== "undefined" && !customElements.get("recur-payment-form-skeleton")) {
@@ -1171,14 +1017,55 @@ var init_toast = __esm({
1171
1017
  }
1172
1018
  });
1173
1019
 
1020
+ // src/components/styles/payment-form-shadow.css
1021
+ var payment_form_shadow_default;
1022
+ var init_payment_form_shadow = __esm({
1023
+ "src/components/styles/payment-form-shadow.css"() {
1024
+ payment_form_shadow_default = "/**\n * Payment Form - Shadow DOM Styles\n * These styles are encapsulated within the Shadow DOM\n */\n\n:host {\n display: block;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.payment-form-wrapper {\n max-width: 500px;\n margin: 0 auto;\n padding: 24px;\n background: white;\n border-radius: 12px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n touch-action: manipulation;\n container-type: inline-size;\n}\n\n.form-header {\n margin-bottom: 24px;\n}\n\n.form-title {\n font-size: 20px;\n font-weight: 600;\n color: #1a202c;\n margin: 0 0 4px 0;\n}\n\n.form-subtitle {\n font-size: 14px;\n color: #718096;\n margin: 0;\n}\n\n.form-section {\n margin-bottom: 24px;\n}\n\n.section-title {\n font-size: 14px;\n font-weight: 600;\n color: #2d3748;\n margin: 0 0 12px 0;\n}\n\n::slotted(*) {\n display: block;\n}\n\n.security-note {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px;\n background: #f7fafc;\n border-radius: 6px;\n margin-top: 16px;\n}\n\n.security-icon {\n color: #48bb78;\n}\n\n.security-text {\n font-size: 13px;\n color: #4a5568;\n margin: 0;\n}\n";
1025
+ }
1026
+ });
1027
+
1028
+ // src/components/styles/payment-form-light.css
1029
+ var payment_form_light_default;
1030
+ var init_payment_form_light = __esm({
1031
+ "src/components/styles/payment-form-light.css"() {
1032
+ payment_form_light_default = "/**\n * Payment Form - Light DOM Styles\n * These styles are applied to slotted content in the Light DOM\n * Using BEM naming convention with 'recur-' prefix for isolation\n */\n\n/* ============================================\n * Order Summary Styles\n * ============================================ */\n.recur-order-summary {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n padding: 16px;\n}\n\n.recur-order-summary__row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n\n.recur-order-summary__row--total {\n margin-bottom: 0;\n padding-top: 12px;\n border-top: 1px solid #e2e8f0;\n}\n\n.recur-order-summary__label {\n font-size: 14px;\n color: #4a5568;\n}\n\n.recur-order-summary__value {\n font-size: 14px;\n line-height: 20px;\n font-weight: 500;\n color: #2d3748;\n}\n\n.recur-order-summary__total {\n font-size: 18px;\n line-height: 26px;\n font-weight: 700;\n color: #1a202c;\n}\n\n/* ============================================\n * Coupon Section Styles\n * ============================================ */\n.recur-coupon {\n padding: 12px 0;\n border-top: 1px solid #e2e8f0;\n margin-top: 12px;\n min-height: 36px;\n display: flex;\n align-items: center;\n}\n\n.recur-coupon__trigger {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 36px;\n background: none;\n border: none;\n padding: 0;\n font-size: 14px;\n line-height: 1;\n color: #718096;\n cursor: pointer;\n transition: color 0.2s;\n}\n\n.recur-coupon__trigger:hover {\n color: #4a5568;\n}\n\n.recur-coupon__trigger-icon {\n width: 16px;\n height: 16px;\n}\n\n.recur-coupon__input-row {\n display: flex;\n gap: 8px;\n width: 100%;\n}\n\n.recur-coupon__input {\n flex: 1;\n height: 36px;\n padding: 0 12px;\n font-size: 14px;\n text-transform: uppercase;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n outline: none;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n.recur-coupon__input::placeholder {\n text-transform: none;\n}\n\n.recur-coupon__input:focus {\n border-color: #3b82f6;\n box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);\n}\n\n.recur-coupon__btn {\n height: 36px;\n padding: 0 12px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n transition: background-color 0.2s, opacity 0.2s;\n white-space: nowrap;\n}\n\n.recur-coupon__btn--apply {\n background: #18181b;\n color: white;\n border: none;\n}\n\n.recur-coupon__btn--apply:hover:not(:disabled) {\n background: #27272a;\n}\n\n.recur-coupon__btn--apply:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.recur-coupon__btn--cancel {\n background: transparent;\n color: #718096;\n border: 1px solid #d1d5db;\n}\n\n.recur-coupon__btn--cancel:hover {\n background: #f7fafc;\n}\n\n.recur-coupon__error {\n width: 100%;\n margin-top: 8px;\n font-size: 13px;\n color: #dc2626;\n}\n\n.recur-coupon__applied {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n}\n\n.recur-coupon__applied-info {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n min-width: 0;\n}\n\n.recur-coupon__applied-icon {\n width: 16px;\n height: 16px;\n color: #16a34a;\n flex-shrink: 0;\n margin-top: 2px;\n}\n\n.recur-coupon__applied-details {\n min-width: 0;\n}\n\n.recur-coupon__applied-name {\n font-size: 14px;\n font-weight: 500;\n color: #16a34a;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.recur-coupon__applied-code {\n font-size: 12px;\n color: #718096;\n}\n\n.recur-coupon__applied-amount {\n font-size: 14px;\n font-weight: 500;\n color: #16a34a;\n white-space: nowrap;\n}\n\n.recur-coupon__remove {\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #9ca3af;\n border-radius: 4px;\n transition: color 0.2s, background-color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.recur-coupon__remove:hover {\n color: #6b7280;\n background: rgba(0, 0, 0, 0.05);\n}\n\n.recur-coupon__remove-icon {\n width: 14px;\n height: 14px;\n}\n\n.recur-coupon__spinner {\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n animation: recur-spin 0.6s linear infinite;\n}\n\n/* Discount Row */\n.recur-order-summary__row--discount {\n color: #16a34a;\n}\n\n.recur-order-summary__row--discount .recur-order-summary__value {\n color: #16a34a;\n font-weight: 500;\n}\n\n/* ============================================\n * Customer Info Styles\n * ============================================ */\n.recur-info-display {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 6px;\n padding: 12px;\n margin-bottom: 8px;\n}\n\n.recur-info-row {\n margin-bottom: 12px;\n}\n\n.recur-info-row:last-child {\n margin-bottom: 0;\n}\n\n.recur-info-label {\n display: block;\n font-size: 12px;\n line-height: 16px;\n color: #718096;\n margin-bottom: 2px;\n}\n\n.recur-info-value {\n display: block;\n font-size: 14px;\n line-height: 20px;\n min-height: 20px;\n color: #2d3748;\n font-weight: 500;\n word-break: break-all;\n}\n\n.recur-form-group {\n margin-bottom: 16px;\n}\n\n.recur-form-label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #2d3748;\n margin-bottom: 6px;\n}\n\n.recur-form-input {\n width: 100%;\n padding: 10px 12px;\n font-size: 16px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n transition: border-color 0.2s;\n box-sizing: border-box;\n}\n\n.recur-form-input:focus {\n outline: none;\n border-color: #3b82f6;\n box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);\n}\n\n.recur-form-input:hover:not(:focus) {\n border-color: #9ca3af;\n}\n\n/* ============================================\n * Card Fields Styles\n * ============================================ */\n.recur-card-field {\n margin-bottom: 16px;\n}\n\n.recur-card-field-label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #2d3748;\n margin-bottom: 6px;\n}\n\n.recur-payuni-iframe {\n height: 36px;\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n touch-action: manipulation;\n}\n\n.recur-card-row {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n column-gap: 16px;\n max-width: 100%;\n box-sizing: border-box;\n}\n\n.recur-card-field-container {\n position: relative;\n touch-action: manipulation;\n}\n\n.recur-card-skeleton {\n position: absolute;\n top: 0;\n left: 0;\n height: 36px;\n width: 100%;\n background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 50%, #f0f0f0 100%);\n background-size: 200% 100%;\n animation: recur-skeleton-loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n z-index: 1;\n pointer-events: none;\n}\n\n.recur-card-skeleton.hidden {\n display: none;\n}\n\n@keyframes recur-skeleton-loading {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n}\n\n/* ============================================\n * Submit Button Styles\n * ============================================ */\n.recur-submit-button {\n width: 100%;\n padding: 14px 20px;\n font-size: 16px;\n font-weight: 500;\n color: #ffffff;\n background: #18181b;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n transition: background-color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n box-sizing: border-box;\n}\n\n.recur-submit-button:hover:not(:disabled) {\n background: #27272a;\n}\n\n.recur-submit-button:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.recur-submit-button.loading {\n cursor: wait;\n}\n\n.recur-loading-spinner {\n width: 16px;\n height: 16px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n animation: recur-spin 0.6s linear infinite;\n}\n\n@keyframes recur-spin {\n to { transform: rotate(360deg); }\n}\n";
1033
+ }
1034
+ });
1035
+
1174
1036
  // src/components/payment-form.ts
1175
1037
  var payment_form_exports = {};
1176
1038
  __export(payment_form_exports, {
1177
1039
  RecurPaymentForm: () => RecurPaymentForm
1178
1040
  });
1179
- var RecurPaymentForm;
1041
+ var icons, RecurPaymentForm;
1180
1042
  var init_payment_form = __esm({
1181
1043
  "src/components/payment-form.ts"() {
1044
+ init_payment_form_shadow();
1045
+ init_payment_form_light();
1046
+ icons = {
1047
+ lock: html`
1048
+ <svg class="security-icon" width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1049
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
1050
+ </svg>
1051
+ `,
1052
+ tag: html`
1053
+ <svg class="recur-coupon__trigger-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1054
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
1055
+ </svg>
1056
+ `,
1057
+ tagApplied: html`
1058
+ <svg class="recur-coupon__applied-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1059
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
1060
+ </svg>
1061
+ `,
1062
+ close: html`
1063
+ <svg class="recur-coupon__remove-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1064
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
1065
+ </svg>
1066
+ `,
1067
+ spinner: html`<span class="recur-coupon__spinner"></span>`
1068
+ };
1182
1069
  RecurPaymentForm = class extends HTMLElement {
1183
1070
  constructor() {
1184
1071
  super();
@@ -1186,6 +1073,12 @@ var init_payment_form = __esm({
1186
1073
  __publicField(this, "customStyles");
1187
1074
  __publicField(this, "_isInitializing", false);
1188
1075
  __publicField(this, "_initializationAborted", false);
1076
+ // Coupon state
1077
+ __publicField(this, "_showCouponInput", false);
1078
+ __publicField(this, "_couponCode", "");
1079
+ __publicField(this, "_couponLoading", false);
1080
+ __publicField(this, "_couponError", null);
1081
+ __publicField(this, "_appliedCoupon", null);
1189
1082
  this.containerId = this.getAttribute("container-id") || `recur-${Date.now()}`;
1190
1083
  this.customStyles = this.getAttribute("custom-styles") || "";
1191
1084
  this.attachShadow({ mode: "open" });
@@ -1203,183 +1096,129 @@ var init_payment_form = __esm({
1203
1096
  submitBtn.parentNode?.replaceChild(newBtn, submitBtn);
1204
1097
  }
1205
1098
  }
1206
- // 監聽 attribute 變化
1207
1099
  static get observedAttributes() {
1208
- return ["custom-styles", "customer-name", "customer-email", "product-name", "amount", "interval"];
1100
+ return [
1101
+ "custom-styles",
1102
+ "customer-name",
1103
+ "customer-email",
1104
+ "product-name",
1105
+ "amount",
1106
+ "interval",
1107
+ "checkout-id",
1108
+ "publishable-key",
1109
+ "api-base-url"
1110
+ ];
1209
1111
  }
1210
1112
  attributeChangedCallback(name, oldValue, newValue) {
1211
- if (name === "custom-styles" && oldValue !== newValue) {
1113
+ if (oldValue === newValue) return;
1114
+ if (name === "custom-styles") {
1212
1115
  this.customStyles = newValue || "";
1213
1116
  this.updateCustomStyles();
1214
- } else if (oldValue !== newValue) {
1215
- this.updateCustomerInfoSection();
1117
+ } else {
1118
+ this.updateOrderSummaryDOM();
1216
1119
  }
1217
1120
  }
1121
+ // ============================================
1122
+ // Styles
1123
+ // ============================================
1124
+ getShadowDOMStyles() {
1125
+ return payment_form_shadow_default;
1126
+ }
1127
+ getLightDOMStyles() {
1128
+ return payment_form_light_default;
1129
+ }
1130
+ // ============================================
1131
+ // Render Methods
1132
+ // ============================================
1218
1133
  render() {
1219
- this.shadowRoot.innerHTML = `
1220
- <style>
1221
- :host {
1222
- display: block;
1223
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1224
- }
1225
-
1226
- .payment-form-wrapper {
1227
- max-width: 500px;
1228
- margin: 0 auto;
1229
- padding: 24px;
1230
- background: white;
1231
- border-radius: 12px;
1232
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1233
- /* Prevent double-tap zoom on iOS */
1234
- touch-action: manipulation;
1235
- /* Enable container queries for responsive layout */
1236
- container-type: inline-size;
1237
- }
1238
-
1239
- .form-header {
1240
- margin-bottom: 24px;
1241
- }
1242
-
1243
- .form-title {
1244
- font-size: 20px;
1245
- font-weight: 600;
1246
- color: #1a202c;
1247
- margin: 0 0 4px 0;
1248
- }
1249
-
1250
- .form-subtitle {
1251
- font-size: 14px;
1252
- color: #718096;
1253
- margin: 0;
1254
- }
1255
-
1256
- .form-section {
1257
- margin-bottom: 24px;
1258
- }
1259
-
1260
- .section-title {
1261
- font-size: 14px;
1262
- font-weight: 600;
1263
- color: #2d3748;
1264
- margin: 0 0 12px 0;
1265
- }
1266
-
1267
- ::slotted(*) {
1268
- display: block;
1269
- }
1270
-
1271
- .security-note {
1272
- display: flex;
1273
- align-items: center;
1274
- gap: 8px;
1275
- padding: 12px;
1276
- background: #f7fafc;
1277
- border-radius: 6px;
1278
- margin-top: 16px;
1279
- }
1280
-
1281
- .security-icon {
1282
- color: #48bb78;
1283
- }
1284
-
1285
- .security-text {
1286
- font-size: 13px;
1287
- color: #4a5568;
1288
- margin: 0;
1289
- }
1290
- </style>
1134
+ this.renderShadowDOM();
1135
+ this.renderLightDOM();
1136
+ }
1137
+ renderShadowDOM() {
1138
+ const template = html`
1139
+ <style>${this.getShadowDOMStyles()}</style>
1291
1140
 
1292
1141
  <div class="payment-form-wrapper">
1293
1142
  <div class="form-header">
1294
- <h2 class="form-title">\u8A02\u95B1\u4ED8\u6B3E</h2>
1295
- <p class="form-subtitle">\u8ACB\u586B\u5BEB\u4FE1\u7528\u5361\u8CC7\u8A0A\u5B8C\u6210\u8A02\u95B1</p>
1143
+ <h2 class="form-title">訂閱付款</h2>
1296
1144
  </div>
1297
1145
 
1298
- <!-- Slot for order summary (Light DOM) -->
1299
- <slot name="order-summary"></slot>
1146
+ <div class="form-section">
1147
+ <h3 class="section-title">訂單摘要</h3>
1148
+ <slot name="order-summary"></slot>
1149
+ </div>
1300
1150
 
1301
1151
  <div class="form-section">
1302
- <h3 class="section-title">\u806F\u7D61\u8CC7\u8A0A</h3>
1303
- <!-- Slot for customer info fields (Light DOM) -->
1152
+ <h3 class="section-title">聯絡資訊</h3>
1304
1153
  <slot name="customer-info"></slot>
1305
1154
  </div>
1306
1155
 
1307
1156
  <div class="form-section">
1308
- <h3 class="section-title">\u4FE1\u7528\u5361\u8CC7\u8A0A</h3>
1309
- <!-- Slot for PAYUNi card fields (Light DOM) -->
1157
+ <h3 class="section-title">信用卡資訊</h3>
1310
1158
  <slot name="card-fields"></slot>
1311
1159
  </div>
1312
1160
 
1313
- <!-- Slot for submit button (Light DOM) -->
1314
1161
  <slot name="actions"></slot>
1315
1162
 
1316
1163
  <div class="security-note">
1317
- <svg class="security-icon" width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1318
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
1319
- </svg>
1320
- <p class="security-text">\u60A8\u7684\u4ED8\u6B3E\u8CC7\u8A0A\u7D93\u904E\u52A0\u5BC6\u4FDD\u8B77</p>
1164
+ ${icons.lock}
1165
+ <p class="security-text">您的付款資訊經過加密保護</p>
1321
1166
  </div>
1322
1167
  </div>
1323
1168
  `;
1324
- this.createLightDOMContent();
1325
- }
1326
- createLightDOMContent() {
1327
- this.innerHTML = "";
1328
- const styleElement = document.createElement("style");
1329
- styleElement.id = `${this.containerId}-custom-styles`;
1330
- const idSpecificStyles = `
1331
- /* PAYUNi iframe \u5BB9\u5668\u9AD8\u5EA6\uFF08\u4F7F\u7528\u5BE6\u969B\u7684 container ID\uFF09 */
1169
+ render(template, this.shadowRoot);
1170
+ }
1171
+ renderLightDOM() {
1172
+ const template = html`
1173
+ <style id="${this.containerId}-styles">
1174
+ ${this.getLightDOMStyles()}
1175
+ ${this.getCustomStyles()}
1176
+ </style>
1177
+
1178
+ <div slot="order-summary">
1179
+ ${this.renderOrderSummary()}
1180
+ </div>
1181
+
1182
+ <div slot="customer-info">
1183
+ ${this.renderCustomerInfo()}
1184
+ </div>
1185
+
1186
+ <div slot="card-fields">
1187
+ ${this.renderCardFields()}
1188
+ </div>
1189
+
1190
+ <div slot="actions">
1191
+ ${this.renderActions()}
1192
+ </div>
1193
+ `;
1194
+ render(template, this);
1195
+ }
1196
+ getCustomStyles() {
1197
+ return `
1198
+ /* PAYUNi iframe container height */
1332
1199
  #${this.containerId}-card-no,
1333
1200
  #${this.containerId}-card-exp,
1334
1201
  #${this.containerId}-card-cvc {
1335
1202
  height: 36px !important;
1336
1203
  }
1204
+ ${this.customStyles}
1337
1205
  `;
1338
- styleElement.textContent = this.customStyles + "\n" + idSpecificStyles;
1339
- this.appendChild(styleElement);
1340
- const orderSummary = this.createOrderSummarySection();
1341
- orderSummary.slot = "order-summary";
1342
- this.appendChild(orderSummary);
1343
- const customerInfo = this.createCustomerInfoSection();
1344
- customerInfo.slot = "customer-info";
1345
- this.appendChild(customerInfo);
1346
- const cardFields = this.createCardFieldsSection();
1347
- cardFields.slot = "card-fields";
1348
- this.appendChild(cardFields);
1349
- const actions = this.createActionsSection();
1350
- actions.slot = "actions";
1351
- this.appendChild(actions);
1352
- }
1353
- updateCustomerInfoSection() {
1354
- const existingSection = this.querySelector('[slot="customer-info"]');
1355
- if (existingSection) {
1356
- const newSection = this.createCustomerInfoSection();
1357
- newSection.slot = "customer-info";
1358
- existingSection.replaceWith(newSection);
1359
- }
1360
1206
  }
1361
1207
  updateCustomStyles() {
1362
- const styleElement = this.querySelector(`#${this.containerId}-custom-styles`);
1208
+ const styleElement = this.querySelector(`#${this.containerId}-styles`);
1363
1209
  if (styleElement) {
1364
- const idSpecificStyles = `
1365
- /* PAYUNi iframe \u5BB9\u5668\u9AD8\u5EA6\uFF08\u4F7F\u7528\u5BE6\u969B\u7684 container ID\uFF09 */
1366
- #${this.containerId}-card-no,
1367
- #${this.containerId}-card-exp,
1368
- #${this.containerId}-card-cvc {
1369
- height: 36px !important;
1370
- }
1371
- `;
1372
- styleElement.textContent = this.customStyles + "\n" + idSpecificStyles;
1210
+ styleElement.textContent = this.getLightDOMStyles() + this.getCustomStyles();
1373
1211
  }
1374
1212
  }
1375
- createOrderSummarySection() {
1376
- const section = document.createElement("div");
1377
- section.className = "order-summary-section";
1213
+ // ============================================
1214
+ // Section Renderers (lit-html)
1215
+ // ============================================
1216
+ renderOrderSummary() {
1378
1217
  const productName = this.getAttribute("product-name");
1379
1218
  const amount = this.getAttribute("amount");
1380
1219
  const interval = this.getAttribute("interval");
1381
1220
  if (!productName || !amount) {
1382
- return section;
1221
+ return html`<div class="recur-order-summary" style="display: none;"></div>`;
1383
1222
  }
1384
1223
  const intervalMap = {
1385
1224
  "day": "\u65E5",
@@ -1388,275 +1227,444 @@ var init_payment_form = __esm({
1388
1227
  "year": "\u5E74"
1389
1228
  };
1390
1229
  const periodText = interval ? intervalMap[interval] || interval : "";
1391
- section.innerHTML = `
1392
- <style>
1393
- .order-summary-section {
1394
- background: #f7fafc;
1395
- border: 1px solid #e2e8f0;
1396
- border-radius: 8px;
1397
- padding: 16px;
1398
- margin-bottom: 24px;
1399
- }
1400
-
1401
- .order-summary-title {
1402
- font-size: 14px;
1403
- font-weight: 600;
1404
- color: #2d3748;
1405
- margin: 0 0 12px 0;
1406
- }
1407
-
1408
- .order-summary-item {
1409
- display: flex;
1410
- justify-content: space-between;
1411
- align-items: center;
1412
- margin-bottom: 8px;
1413
- }
1414
-
1415
- .order-summary-item:last-child {
1416
- margin-bottom: 0;
1417
- padding-top: 12px;
1418
- border-top: 1px solid #e2e8f0;
1419
- }
1230
+ const originalAmount = Number(amount);
1231
+ const discountAmount = this._appliedCoupon?.discountAmount || 0;
1232
+ const finalAmount = originalAmount - discountAmount;
1233
+ return html`
1234
+ <div class="recur-order-summary" id="${this.containerId}-order-summary">
1235
+ <div class="recur-order-summary__row">
1236
+ <span class="recur-order-summary__label">訂閱方案</span>
1237
+ <span class="recur-order-summary__value">${productName}</span>
1238
+ </div>
1420
1239
 
1421
- .order-summary-label {
1422
- font-size: 14px;
1423
- color: #4a5568;
1424
- }
1240
+ ${periodText ? html`
1241
+ <div class="recur-order-summary__row">
1242
+ <span class="recur-order-summary__label">計費週期</span>
1243
+ <span class="recur-order-summary__value">每${periodText}</span>
1244
+ </div>
1245
+ ` : nothing}
1425
1246
 
1426
- .order-summary-value {
1427
- font-size: 14px;
1428
- line-height: 20px;
1429
- font-weight: 500;
1430
- color: #2d3748;
1431
- }
1247
+ <div class="recur-coupon" id="${this.containerId}-coupon">
1248
+ ${this.renderCoupon()}
1249
+ </div>
1432
1250
 
1433
- .order-summary-total {
1434
- font-size: 18px;
1435
- line-height: 26px;
1436
- font-weight: 700;
1437
- color: #1a202c;
1438
- }
1439
- </style>
1251
+ ${discountAmount > 0 ? html`
1252
+ <div class="recur-order-summary__row">
1253
+ <span class="recur-order-summary__label">小計</span>
1254
+ <span class="recur-order-summary__value">NT$ ${originalAmount.toLocaleString()}</span>
1255
+ </div>
1256
+ <div class="recur-order-summary__row recur-order-summary__row--discount">
1257
+ <span class="recur-order-summary__label">折扣</span>
1258
+ <span class="recur-order-summary__value">-NT$ ${discountAmount.toLocaleString()}</span>
1259
+ </div>
1260
+ ` : nothing}
1440
1261
 
1441
- <h3 class="order-summary-title">\u8A02\u55AE\u6458\u8981</h3>
1442
- <div class="order-summary-item">
1443
- <span class="order-summary-label">\u8A02\u95B1\u65B9\u6848</span>
1444
- <span class="order-summary-value">${productName}</span>
1445
- </div>
1446
- ${periodText ? `
1447
- <div class="order-summary-item">
1448
- <span class="order-summary-label">\u8A08\u8CBB\u9031\u671F</span>
1449
- <span class="order-summary-value">\u6BCF${periodText}</span>
1262
+ <div class="recur-order-summary__row recur-order-summary__row--total">
1263
+ <span class="recur-order-summary__label">總計</span>
1264
+ <span class="recur-order-summary__total">NT$ ${finalAmount.toLocaleString()}</span>
1450
1265
  </div>
1451
- ` : ""}
1452
- <div class="order-summary-item">
1453
- <span class="order-summary-label">\u7E3D\u8A08</span>
1454
- <span class="order-summary-total">NT$ ${Number(amount).toLocaleString()}</span>
1455
1266
  </div>
1456
1267
  `;
1457
- return section;
1458
1268
  }
1459
- createCustomerInfoSection() {
1460
- const section = document.createElement("div");
1461
- section.className = "customer-info-section";
1462
- const customerName = this.getAttribute("customer-name");
1463
- const customerEmail = this.getAttribute("customer-email");
1464
- if (customerEmail) {
1465
- section.innerHTML = `
1466
- <style>
1467
- .recur-info-display {
1468
- background: #f7fafc;
1469
- border: 1px solid #e2e8f0;
1470
- border-radius: 6px;
1471
- padding: 12px;
1472
- margin-bottom: 8px;
1473
- }
1474
-
1475
- .recur-info-row {
1476
- margin-bottom: 12px;
1477
- }
1478
-
1479
- .recur-info-row:last-child {
1480
- margin-bottom: 0;
1481
- }
1482
-
1483
- .recur-info-label {
1484
- display: block;
1485
- font-size: 12px;
1486
- line-height: 16px;
1487
- color: #718096;
1488
- margin-bottom: 2px;
1489
- }
1490
-
1491
- .recur-info-value {
1492
- display: block;
1493
- font-size: 14px;
1494
- line-height: 20px;
1495
- min-height: 20px;
1496
- color: #2d3748;
1497
- font-weight: 500;
1498
- word-break: break-all;
1499
- }
1500
- </style>
1501
-
1502
- <div class="recur-info-display">
1503
- ${customerName ? `
1504
- <div class="recur-info-row">
1505
- <span class="recur-info-label">\u59D3\u540D</span>
1506
- <span class="recur-info-value">${customerName}</span>
1269
+ renderCoupon() {
1270
+ if (this._appliedCoupon) {
1271
+ return html`
1272
+ <div class="recur-coupon__applied">
1273
+ <div class="recur-coupon__applied-info">
1274
+ ${icons.tagApplied}
1275
+ <div class="recur-coupon__applied-details">
1276
+ <div class="recur-coupon__applied-name">
1277
+ ${this._appliedCoupon.couponName || this._appliedCoupon.code}
1278
+ </div>
1279
+ ${this._appliedCoupon.couponName ? html`
1280
+ <div class="recur-coupon__applied-code">代碼:${this._appliedCoupon.code}</div>
1281
+ ` : nothing}
1507
1282
  </div>
1508
- ` : ""}
1509
- <div class="recur-info-row">
1510
- <span class="recur-info-label">\u96FB\u5B50\u90F5\u4EF6</span>
1511
- <span class="recur-info-value">${customerEmail}</span>
1512
1283
  </div>
1284
+ <span class="recur-coupon__applied-amount">
1285
+ -NT$ ${this._appliedCoupon.discountAmount.toLocaleString()}
1286
+ </span>
1287
+ <button
1288
+ type="button"
1289
+ class="recur-coupon__remove"
1290
+ ?disabled=${this._couponLoading}
1291
+ @click=${() => this.removeCoupon()}
1292
+ >
1293
+ ${this._couponLoading ? icons.spinner : icons.close}
1294
+ </button>
1513
1295
  </div>
1514
1296
  `;
1515
- } else {
1516
- section.innerHTML = `
1517
- <style>
1518
- /* \u9019\u4E9B\u6A23\u5F0F\u5728 Light DOM \u4E2D\uFF0C\u6703\u5F71\u97FF\u5168\u5C40 */
1519
- /* \u4F7F\u7528\u5E36 prefix \u7684 class name \u907F\u514D\u885D\u7A81 */
1520
- .recur-form-group {
1521
- margin-bottom: 16px;
1522
- }
1523
-
1524
- .recur-form-label {
1525
- display: block;
1526
- font-size: 14px;
1527
- font-weight: 500;
1528
- color: #2d3748;
1529
- margin-bottom: 6px;
1530
- }
1531
-
1532
- .recur-form-input {
1533
- width: 100%;
1534
- padding: 10px 12px;
1535
- font-size: 16px; /* 16px prevents iOS Safari auto-zoom on focus */
1536
- border: 1px solid #d1d5db;
1537
- border-radius: 6px;
1538
- transition: border-color 0.2s;
1539
- box-sizing: border-box;
1540
- }
1541
-
1542
- .recur-form-input:focus {
1543
- outline: none;
1544
- border-color: #3b82f6;
1545
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
1546
- }
1547
-
1548
- .recur-form-input:hover:not(:focus) {
1549
- border-color: #9ca3af;
1550
- }
1551
- </style>
1552
-
1553
- <div class="recur-form-group">
1554
- <label class="recur-form-label" for="${this.containerId}-email">Email</label>
1555
- <input
1556
- type="email"
1557
- id="${this.containerId}-email"
1558
- class="recur-form-input"
1559
- placeholder="your@email.com"
1560
- required
1561
- />
1297
+ }
1298
+ if (this._showCouponInput) {
1299
+ return html`
1300
+ <div style="width: 100%;">
1301
+ <div class="recur-coupon__input-row">
1302
+ <input
1303
+ type="text"
1304
+ class="recur-coupon__input"
1305
+ id="${this.containerId}-coupon-input"
1306
+ placeholder="輸入優惠碼"
1307
+ .value=${this._couponCode}
1308
+ ?disabled=${this._couponLoading}
1309
+ @input=${(e) => this.handleCouponInputChange(e)}
1310
+ @keydown=${(e) => this.handleCouponKeydown(e)}
1311
+ />
1312
+ <button
1313
+ type="button"
1314
+ class="recur-coupon__btn recur-coupon__btn--apply"
1315
+ ?disabled=${this._couponLoading || !this._couponCode.trim()}
1316
+ @click=${() => this.applyCoupon()}
1317
+ >
1318
+ ${this._couponLoading ? icons.spinner : "\u5957\u7528"}
1319
+ </button>
1320
+ <button
1321
+ type="button"
1322
+ class="recur-coupon__btn recur-coupon__btn--cancel"
1323
+ ?disabled=${this._couponLoading}
1324
+ @click=${() => this.cancelCouponInput()}
1325
+ >
1326
+ 取消
1327
+ </button>
1328
+ </div>
1329
+ ${this._couponError ? html`
1330
+ <div class="recur-coupon__error">${this._couponError}</div>
1331
+ ` : nothing}
1562
1332
  </div>
1563
-
1564
- <div class="recur-form-group">
1565
- <label class="recur-form-label" for="${this.containerId}-name">\u59D3\u540D</label>
1333
+ `;
1334
+ }
1335
+ return html`
1336
+ <button
1337
+ type="button"
1338
+ class="recur-coupon__trigger"
1339
+ @click=${() => this.showCouponInput()}
1340
+ >
1341
+ ${icons.tag}
1342
+ 有優惠碼?
1343
+ </button>
1344
+ `;
1345
+ }
1346
+ // Coupon event handlers
1347
+ showCouponInput() {
1348
+ this._showCouponInput = true;
1349
+ this._couponError = null;
1350
+ this.updateCouponSectionDOM();
1351
+ requestAnimationFrame(() => {
1352
+ const input = document.getElementById(`${this.containerId}-coupon-input`);
1353
+ input?.focus();
1354
+ });
1355
+ }
1356
+ cancelCouponInput() {
1357
+ this._showCouponInput = false;
1358
+ this._couponCode = "";
1359
+ this._couponError = null;
1360
+ this.updateCouponSectionDOM();
1361
+ }
1362
+ handleCouponInputChange(e) {
1363
+ this._couponCode = e.target.value.toUpperCase();
1364
+ this._couponError = null;
1365
+ this.updateApplyButtonState();
1366
+ }
1367
+ handleCouponKeydown(e) {
1368
+ if (e.key === "Enter") {
1369
+ e.preventDefault();
1370
+ this.applyCoupon();
1371
+ } else if (e.key === "Escape") {
1372
+ this.cancelCouponInput();
1373
+ }
1374
+ }
1375
+ updateApplyButtonState() {
1376
+ const btn = this.querySelector(".recur-coupon__btn--apply");
1377
+ if (btn) {
1378
+ btn.disabled = this._couponLoading || !this._couponCode.trim();
1379
+ }
1380
+ }
1381
+ // Direct DOM manipulation for coupon section updates (avoids lit-html tracking issues)
1382
+ updateCouponSectionDOM() {
1383
+ const container = document.getElementById(`${this.containerId}-coupon`);
1384
+ if (!container) return;
1385
+ if (this._appliedCoupon) {
1386
+ container.innerHTML = this.getCouponAppliedHTML();
1387
+ this.bindCouponAppliedEvents(container);
1388
+ } else if (this._showCouponInput) {
1389
+ container.innerHTML = this.getCouponInputHTML();
1390
+ this.bindCouponInputEvents(container);
1391
+ } else {
1392
+ container.innerHTML = this.getCouponTriggerHTML();
1393
+ this.bindCouponTriggerEvents(container);
1394
+ }
1395
+ }
1396
+ getCouponTriggerHTML() {
1397
+ return `
1398
+ <button type="button" class="recur-coupon__trigger">
1399
+ <svg class="recur-coupon__trigger-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1400
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
1401
+ </svg>
1402
+ \u6709\u512A\u60E0\u78BC\uFF1F
1403
+ </button>
1404
+ `;
1405
+ }
1406
+ getCouponInputHTML() {
1407
+ const errorHTML = this._couponError ? `<div class="recur-coupon__error">${this._couponError}</div>` : "";
1408
+ return `
1409
+ <div style="width: 100%;">
1410
+ <div class="recur-coupon__input-row">
1566
1411
  <input
1567
1412
  type="text"
1568
- id="${this.containerId}-name"
1569
- class="recur-form-input"
1570
- placeholder="\u738B\u5C0F\u660E"
1571
- value="${customerName || ""}"
1572
- required
1413
+ class="recur-coupon__input"
1414
+ id="${this.containerId}-coupon-input"
1415
+ placeholder="\u8F38\u5165\u512A\u60E0\u78BC"
1416
+ value="${this._couponCode}"
1417
+ ${this._couponLoading ? "disabled" : ""}
1573
1418
  />
1419
+ <button
1420
+ type="button"
1421
+ class="recur-coupon__btn recur-coupon__btn--apply"
1422
+ ${this._couponLoading || !this._couponCode.trim() ? "disabled" : ""}
1423
+ >
1424
+ ${this._couponLoading ? '<span class="recur-coupon__spinner"></span>' : "\u5957\u7528"}
1425
+ </button>
1426
+ <button
1427
+ type="button"
1428
+ class="recur-coupon__btn recur-coupon__btn--cancel"
1429
+ ${this._couponLoading ? "disabled" : ""}
1430
+ >
1431
+ \u53D6\u6D88
1432
+ </button>
1574
1433
  </div>
1575
- `;
1576
- }
1577
- return section;
1434
+ ${errorHTML}
1435
+ </div>
1436
+ `;
1578
1437
  }
1579
- createCardFieldsSection() {
1580
- const section = document.createElement("div");
1581
- section.className = "card-fields-section";
1582
- section.innerHTML = `
1583
- <style>
1584
- .recur-card-field {
1585
- margin-bottom: 16px;
1438
+ getCouponAppliedHTML() {
1439
+ const coupon = this._appliedCoupon;
1440
+ const codeHTML = coupon.couponName ? `<div class="recur-coupon__applied-code">\u4EE3\u78BC\uFF1A${coupon.code}</div>` : "";
1441
+ return `
1442
+ <div class="recur-coupon__applied">
1443
+ <div class="recur-coupon__applied-info">
1444
+ <svg class="recur-coupon__applied-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1445
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
1446
+ </svg>
1447
+ <div class="recur-coupon__applied-details">
1448
+ <div class="recur-coupon__applied-name">${coupon.couponName || coupon.code}</div>
1449
+ ${codeHTML}
1450
+ </div>
1451
+ </div>
1452
+ <span class="recur-coupon__applied-amount">-NT$ ${coupon.discountAmount.toLocaleString()}</span>
1453
+ <button type="button" class="recur-coupon__remove" ${this._couponLoading ? "disabled" : ""}>
1454
+ ${this._couponLoading ? '<span class="recur-coupon__spinner"></span>' : '<svg class="recur-coupon__remove-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>'}
1455
+ </button>
1456
+ </div>
1457
+ `;
1458
+ }
1459
+ bindCouponTriggerEvents(container) {
1460
+ const btn = container.querySelector(".recur-coupon__trigger");
1461
+ btn?.addEventListener("click", () => this.showCouponInput());
1462
+ }
1463
+ bindCouponInputEvents(container) {
1464
+ const input = container.querySelector(".recur-coupon__input");
1465
+ const applyBtn = container.querySelector(".recur-coupon__btn--apply");
1466
+ const cancelBtn = container.querySelector(".recur-coupon__btn--cancel");
1467
+ input?.addEventListener("input", (e) => this.handleCouponInputChange(e));
1468
+ input?.addEventListener("keydown", (e) => this.handleCouponKeydown(e));
1469
+ applyBtn?.addEventListener("click", () => this.applyCoupon());
1470
+ cancelBtn?.addEventListener("click", () => this.cancelCouponInput());
1471
+ }
1472
+ bindCouponAppliedEvents(container) {
1473
+ const removeBtn = container.querySelector(".recur-coupon__remove");
1474
+ removeBtn?.addEventListener("click", () => this.removeCoupon());
1475
+ }
1476
+ updateOrderSummaryDOM() {
1477
+ const summaryContainer = document.getElementById(`${this.containerId}-order-summary`);
1478
+ if (!summaryContainer) return;
1479
+ const amount = Number(this.getAttribute("amount") || 0);
1480
+ const discountAmount = this._appliedCoupon?.discountAmount || 0;
1481
+ const finalAmount = amount - discountAmount;
1482
+ let discountRow = summaryContainer.querySelector(".recur-order-summary__row--discount");
1483
+ const totalRow = summaryContainer.querySelector(".recur-order-summary__row--total");
1484
+ if (discountAmount > 0) {
1485
+ if (!discountRow && totalRow) {
1486
+ const subtotalHTML = `
1487
+ <div class="recur-order-summary__row">
1488
+ <span class="recur-order-summary__label">\u5C0F\u8A08</span>
1489
+ <span class="recur-order-summary__value">NT$ ${amount.toLocaleString()}</span>
1490
+ </div>
1491
+ `;
1492
+ const discountHTML = `
1493
+ <div class="recur-order-summary__row recur-order-summary__row--discount">
1494
+ <span class="recur-order-summary__label">\u6298\u6263</span>
1495
+ <span class="recur-order-summary__value">-NT$ ${discountAmount.toLocaleString()}</span>
1496
+ </div>
1497
+ `;
1498
+ totalRow.insertAdjacentHTML("beforebegin", subtotalHTML + discountHTML);
1499
+ }
1500
+ } else {
1501
+ const subtotalRow = summaryContainer.querySelector(".recur-order-summary__row:not(.recur-order-summary__row--total):last-of-type");
1502
+ discountRow?.remove();
1503
+ if (subtotalRow?.querySelector(".recur-order-summary__label")?.textContent === "\u5C0F\u8A08") {
1504
+ subtotalRow.remove();
1505
+ }
1586
1506
  }
1587
-
1588
- .recur-card-field-label {
1589
- display: block;
1590
- font-size: 14px;
1591
- font-weight: 500;
1592
- color: #2d3748;
1593
- margin-bottom: 6px;
1507
+ const totalValue = totalRow?.querySelector(".recur-order-summary__total");
1508
+ if (totalValue) {
1509
+ totalValue.textContent = `NT$ ${finalAmount.toLocaleString()}`;
1594
1510
  }
1595
-
1596
- /* PAYUNi iframe \u5BB9\u5668\u6A23\u5F0F */
1597
- .recur-payuni-iframe {
1598
- height: 36px;
1599
- width: 100%;
1600
- max-width: 100%;
1601
- box-sizing: border-box;
1602
- /* Help reduce iOS zoom issues */
1603
- touch-action: manipulation;
1511
+ this.updateCouponSectionDOM();
1512
+ }
1513
+ // ============================================
1514
+ // Coupon API Methods
1515
+ // ============================================
1516
+ async applyCoupon() {
1517
+ const code = this._couponCode.trim();
1518
+ if (!code) {
1519
+ this._couponError = "\u8ACB\u8F38\u5165\u512A\u60E0\u78BC";
1520
+ this.updateCouponSectionDOM();
1521
+ return;
1604
1522
  }
1605
-
1606
- .recur-card-row {
1607
- display: grid;
1608
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
1609
- column-gap: 16px;
1610
- max-width: 100%;
1611
- box-sizing: border-box;
1523
+ const checkoutId = this.getAttribute("checkout-id");
1524
+ const apiBaseUrl = this.getAttribute("api-base-url") || "";
1525
+ if (!checkoutId) {
1526
+ this._couponError = "\u7121\u6CD5\u5957\u7528\u512A\u60E0\u78BC";
1527
+ this.updateCouponSectionDOM();
1528
+ return;
1612
1529
  }
1613
-
1614
- /* Container needs relative positioning for skeleton overlay */
1615
- .recur-card-field-container {
1616
- position: relative;
1617
- /* Help reduce iOS zoom issues */
1618
- touch-action: manipulation;
1530
+ this._couponLoading = true;
1531
+ this._couponError = null;
1532
+ this.updateCouponSectionDOM();
1533
+ try {
1534
+ const response = await fetch(`${apiBaseUrl}/api/checkout/${checkoutId}/coupon`, {
1535
+ method: "POST",
1536
+ headers: { "Content-Type": "application/json" },
1537
+ body: JSON.stringify({ code })
1538
+ });
1539
+ const data = await response.json();
1540
+ if (!response.ok) {
1541
+ this._couponError = data.error?.message || "\u512A\u60E0\u78BC\u7121\u6548";
1542
+ this._couponLoading = false;
1543
+ this.updateCouponSectionDOM();
1544
+ return;
1545
+ }
1546
+ this._appliedCoupon = {
1547
+ code: data.discount.code,
1548
+ couponName: data.discount.couponName,
1549
+ discountType: data.discount.discountType,
1550
+ discountAmount: data.discount.discountAmount,
1551
+ duration: data.discount.duration,
1552
+ durationMonths: data.discount.durationMonths,
1553
+ bonusTrialDays: data.discount.bonusTrialDays,
1554
+ bonusMonths: data.discount.bonusMonths
1555
+ };
1556
+ this._showCouponInput = false;
1557
+ this._couponCode = "";
1558
+ this._couponLoading = false;
1559
+ this.updateOrderSummaryDOM();
1560
+ this.dispatchEvent(new CustomEvent("coupon-applied", {
1561
+ detail: { coupon: this._appliedCoupon },
1562
+ bubbles: true,
1563
+ composed: true
1564
+ }));
1565
+ } catch (err) {
1566
+ this._couponError = "\u7DB2\u8DEF\u932F\u8AA4\uFF0C\u8ACB\u7A0D\u5F8C\u518D\u8A66";
1567
+ this._couponLoading = false;
1568
+ this.updateCouponSectionDOM();
1619
1569
  }
1620
-
1621
- /* Loading skeleton for card fields - shown until PAYUNi iframe loads */
1622
- .recur-card-skeleton {
1623
- position: absolute;
1624
- top: 0;
1625
- left: 0;
1626
- height: 36px;
1627
- width: 100%;
1628
- background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 50%, #f0f0f0 100%);
1629
- background-size: 200% 100%;
1630
- animation: recur-skeleton-loading 1.5s ease-in-out infinite;
1631
- border-radius: 4px;
1632
- z-index: 1;
1633
- pointer-events: none;
1570
+ }
1571
+ async removeCoupon() {
1572
+ const checkoutId = this.getAttribute("checkout-id");
1573
+ const apiBaseUrl = this.getAttribute("api-base-url") || "";
1574
+ if (!checkoutId) {
1575
+ return;
1634
1576
  }
1635
-
1636
- /* Hide skeleton when PAYUNi iframe is ready (has content) */
1637
- .recur-card-skeleton.hidden {
1638
- display: none;
1577
+ this._couponLoading = true;
1578
+ this.updateCouponSectionDOM();
1579
+ try {
1580
+ const response = await fetch(`${apiBaseUrl}/api/checkout/${checkoutId}/coupon`, {
1581
+ method: "DELETE"
1582
+ });
1583
+ if (!response.ok) {
1584
+ const data = await response.json();
1585
+ this._couponError = data.error?.message || "\u79FB\u9664\u5931\u6557";
1586
+ this._couponLoading = false;
1587
+ this.updateCouponSectionDOM();
1588
+ return;
1589
+ }
1590
+ const removedCoupon = this._appliedCoupon;
1591
+ this._appliedCoupon = null;
1592
+ this._couponLoading = false;
1593
+ this.updateOrderSummaryDOM();
1594
+ this.dispatchEvent(new CustomEvent("coupon-removed", {
1595
+ detail: { coupon: removedCoupon },
1596
+ bubbles: true,
1597
+ composed: true
1598
+ }));
1599
+ } catch (err) {
1600
+ this._couponError = "\u7DB2\u8DEF\u932F\u8AA4\uFF0C\u8ACB\u7A0D\u5F8C\u518D\u8A66";
1601
+ this._couponLoading = false;
1602
+ this.updateCouponSectionDOM();
1639
1603
  }
1640
-
1641
- @keyframes recur-skeleton-loading {
1642
- 0% { background-position: 200% 0; }
1643
- 100% { background-position: -200% 0; }
1604
+ }
1605
+ /**
1606
+ * Get applied coupon (for external use)
1607
+ */
1608
+ getAppliedCoupon() {
1609
+ return this._appliedCoupon;
1610
+ }
1611
+ renderCustomerInfo() {
1612
+ const customerName = this.getAttribute("customer-name");
1613
+ const customerEmail = this.getAttribute("customer-email");
1614
+ if (customerEmail) {
1615
+ return html`
1616
+ <div class="recur-info-display">
1617
+ ${customerName ? html`
1618
+ <div class="recur-info-row">
1619
+ <span class="recur-info-label">姓名</span>
1620
+ <span class="recur-info-value">${customerName}</span>
1621
+ </div>
1622
+ ` : nothing}
1623
+ <div class="recur-info-row">
1624
+ <span class="recur-info-label">電子郵件</span>
1625
+ <span class="recur-info-value">${customerEmail}</span>
1626
+ </div>
1627
+ </div>
1628
+ `;
1644
1629
  }
1645
- </style>
1630
+ return html`
1631
+ <div class="recur-form-group">
1632
+ <label class="recur-form-label" for="${this.containerId}-email">Email</label>
1633
+ <input
1634
+ type="email"
1635
+ id="${this.containerId}-email"
1636
+ class="recur-form-input"
1637
+ placeholder="your@email.com"
1638
+ required
1639
+ />
1640
+ </div>
1646
1641
 
1642
+ <div class="recur-form-group">
1643
+ <label class="recur-form-label" for="${this.containerId}-name">姓名</label>
1644
+ <input
1645
+ type="text"
1646
+ id="${this.containerId}-name"
1647
+ class="recur-form-input"
1648
+ placeholder="王小明"
1649
+ .value=${customerName || ""}
1650
+ required
1651
+ />
1652
+ </div>
1653
+ `;
1654
+ }
1655
+ renderCardFields() {
1656
+ return html`
1647
1657
  <div class="recur-card-field">
1648
- <label class="recur-card-field-label">\u5361\u865F</label>
1658
+ <label class="recur-card-field-label">卡號</label>
1649
1659
  <div class="recur-card-field-container" id="${this.containerId}-card-no-wrapper">
1650
- <!-- Skeleton shown until PAYUNi iframe loads -->
1651
1660
  <div class="recur-card-skeleton" id="${this.containerId}-card-no-skeleton"></div>
1652
- <!-- PAYUNi \u6703\u5728\u9019\u88E1\u6CE8\u5165 iframe -->
1653
1661
  <div id="${this.containerId}-card-no" class="recur-payuni-iframe"></div>
1654
1662
  </div>
1655
1663
  </div>
1656
1664
 
1657
1665
  <div class="recur-card-row">
1658
1666
  <div class="recur-card-field">
1659
- <label class="recur-card-field-label">\u6709\u6548\u671F\u9650</label>
1667
+ <label class="recur-card-field-label">有效期限</label>
1660
1668
  <div class="recur-card-field-container" id="${this.containerId}-card-exp-wrapper">
1661
1669
  <div class="recur-card-skeleton" id="${this.containerId}-card-exp-skeleton"></div>
1662
1670
  <div id="${this.containerId}-card-exp" class="recur-payuni-iframe"></div>
@@ -1664,7 +1672,7 @@ var init_payment_form = __esm({
1664
1672
  </div>
1665
1673
 
1666
1674
  <div class="recur-card-field">
1667
- <label class="recur-card-field-label">\u5B89\u5168\u78BC</label>
1675
+ <label class="recur-card-field-label">安全碼</label>
1668
1676
  <div class="recur-card-field-container" id="${this.containerId}-card-cvc-wrapper">
1669
1677
  <div class="recur-card-skeleton" id="${this.containerId}-card-cvc-skeleton"></div>
1670
1678
  <div id="${this.containerId}-card-cvc" class="recur-payuni-iframe"></div>
@@ -1672,11 +1680,7 @@ var init_payment_form = __esm({
1672
1680
  </div>
1673
1681
  </div>
1674
1682
  `;
1675
- return section;
1676
1683
  }
1677
- /**
1678
- * Hide card field skeletons after PAYUNi iframes are loaded
1679
- */
1680
1684
  hideCardSkeletons() {
1681
1685
  const skeletonIds = [
1682
1686
  `${this.containerId}-card-no-skeleton`,
@@ -1690,71 +1694,21 @@ var init_payment_form = __esm({
1690
1694
  }
1691
1695
  });
1692
1696
  }
1693
- createActionsSection() {
1694
- const section = document.createElement("div");
1695
- section.className = "actions-section";
1696
- section.innerHTML = `
1697
- <style>
1698
- .recur-submit-button {
1699
- width: 100%;
1700
- padding: 14px 20px;
1701
- font-size: 16px;
1702
- font-weight: 500;
1703
- color: #ffffff;
1704
- background: #18181b;
1705
- border: none;
1706
- border-radius: 6px;
1707
- cursor: pointer;
1708
- transition: background-color 0.2s;
1709
- display: flex;
1710
- align-items: center;
1711
- justify-content: center;
1712
- gap: 8px;
1713
- box-sizing: border-box;
1714
- }
1715
-
1716
- .recur-submit-button:hover:not(:disabled) {
1717
- background: #27272a;
1718
- }
1719
-
1720
- .recur-submit-button:disabled {
1721
- opacity: 0.6;
1722
- cursor: not-allowed;
1723
- }
1724
-
1725
- .recur-submit-button.loading {
1726
- cursor: wait;
1727
- }
1728
-
1729
- .recur-loading-spinner {
1730
- width: 16px;
1731
- height: 16px;
1732
- border: 2px solid currentColor;
1733
- border-top-color: transparent;
1734
- border-radius: 50%;
1735
- animation: recur-spin 0.6s linear infinite;
1736
- }
1737
-
1738
- @keyframes recur-spin {
1739
- to { transform: rotate(360deg); }
1740
- }
1741
- </style>
1742
-
1697
+ renderActions() {
1698
+ return html`
1743
1699
  <button
1744
1700
  type="submit"
1745
1701
  id="${this.containerId}-submit-btn"
1746
1702
  class="recur-submit-button"
1747
1703
  disabled
1748
1704
  >
1749
- <span id="${this.containerId}-submit-text">\u78BA\u8A8D\u4ED8\u6B3E</span>
1705
+ <span id="${this.containerId}-submit-text">確認付款</span>
1750
1706
  </button>
1751
1707
  `;
1752
- return section;
1753
1708
  }
1754
- /**
1755
- * 初始化 PAYUNi SDK
1756
- * 這個方法會在外部調用,傳入 SDK token
1757
- */
1709
+ // ============================================
1710
+ // PAYUNi Integration
1711
+ // ============================================
1758
1712
  async initializePayment(sdkToken, environment) {
1759
1713
  if (this._initializationAborted) {
1760
1714
  console.log("[PaymentForm] Initialization aborted before start");
@@ -1788,7 +1742,6 @@ var init_payment_form = __esm({
1788
1742
  const paymentSession = window.UniPayment.createSession(sdkToken, {
1789
1743
  env: environment === "SANDBOX" ? "S" : "P",
1790
1744
  elements: {
1791
- // 這些 ID 在 Light DOM 中,document.getElementById 可以找到
1792
1745
  CardNo: `${this.containerId}-card-no`,
1793
1746
  CardExp: `${this.containerId}-card-exp`,
1794
1747
  CardCvc: `${this.containerId}-card-cvc`
@@ -1877,17 +1830,17 @@ var init_payment_form = __esm({
1877
1830
  detail: {
1878
1831
  customerEmail: email,
1879
1832
  customerName: name,
1880
- paymentSession: this._paymentSession
1833
+ paymentSession: this._paymentSession,
1834
+ appliedCoupon: this._appliedCoupon
1881
1835
  },
1882
1836
  bubbles: true,
1883
1837
  composed: true
1884
- // 讓事件可以穿透 Shadow DOM
1885
1838
  }));
1886
1839
  });
1887
1840
  }
1888
- /**
1889
- * Set button loading state
1890
- */
1841
+ // ============================================
1842
+ // UI Helpers
1843
+ // ============================================
1891
1844
  setButtonLoading(loading) {
1892
1845
  const submitBtn = document.getElementById(`${this.containerId}-submit-btn`);
1893
1846
  if (!submitBtn) return;
@@ -1904,16 +1857,9 @@ var init_payment_form = __esm({
1904
1857
  submitBtn.innerHTML = '<span id="' + this.containerId + '-submit-text">\u78BA\u8A8D\u4ED8\u6B3E</span>';
1905
1858
  }
1906
1859
  }
1907
- /**
1908
- * Reset button to normal state (call this when payment completes or fails)
1909
- */
1910
1860
  resetButton() {
1911
1861
  this.setButtonLoading(false);
1912
1862
  }
1913
- /**
1914
- * Set verifying state (for 3D Secure verification)
1915
- * Shows a "verifying" message while polling for 3D result
1916
- */
1917
1863
  setVerifying(verifying) {
1918
1864
  const submitBtn = document.getElementById(`${this.containerId}-submit-btn`);
1919
1865
  if (!submitBtn) return;
@@ -1928,9 +1874,6 @@ var init_payment_form = __esm({
1928
1874
  this.setButtonLoading(false);
1929
1875
  }
1930
1876
  }
1931
- /**
1932
- * Show error message using RecurErrorDisplay component
1933
- */
1934
1877
  showError(message) {
1935
1878
  let errorContainer = this.querySelector(".recur-sdk__error-container");
1936
1879
  if (!errorContainer) {
@@ -1946,18 +1889,12 @@ var init_payment_form = __esm({
1946
1889
  errorContainer.appendChild(errorDisplay);
1947
1890
  errorDisplay.scrollIntoView({ behavior: "smooth", block: "center" });
1948
1891
  }
1949
- /**
1950
- * Clear any displayed errors
1951
- */
1952
1892
  clearError() {
1953
1893
  const errorContainer = this.querySelector(".recur-sdk__error-container");
1954
1894
  if (errorContainer) {
1955
1895
  errorContainer.innerHTML = "";
1956
1896
  }
1957
1897
  }
1958
- /**
1959
- * 等待組件完全連接到 DOM
1960
- */
1961
1898
  updateComplete() {
1962
1899
  return new Promise((resolve) => {
1963
1900
  if (this.isConnected) {
@@ -1973,18 +1910,12 @@ var init_payment_form = __esm({
1973
1910
  }
1974
1911
  });
1975
1912
  }
1976
- /**
1977
- * 獲取表單數據
1978
- */
1979
1913
  getFormData() {
1980
1914
  return {
1981
1915
  email: document.getElementById(`${this.containerId}-email`)?.value,
1982
1916
  name: document.getElementById(`${this.containerId}-name`)?.value
1983
1917
  };
1984
1918
  }
1985
- /**
1986
- * 設置表單數據
1987
- */
1988
1919
  setFormData(data) {
1989
1920
  if (data.email) {
1990
1921
  const emailInput = document.getElementById(`${this.containerId}-email`);
@@ -2538,7 +2469,7 @@ async function registerComponents() {
2538
2469
  Promise.resolve().then(() => (init_success_message(), success_message_exports)),
2539
2470
  Promise.resolve().then(() => (init_error_display(), error_display_exports)),
2540
2471
  Promise.resolve().then(() => (init_skeleton_loader(), skeleton_loader_exports)),
2541
- Promise.resolve().then(() => (init_payment_form_skeleton(), payment_form_skeleton_exports)),
2472
+ Promise.resolve().then(() => (init_payment_form_skeleton2(), payment_form_skeleton_exports)),
2542
2473
  Promise.resolve().then(() => (init_toast(), toast_exports)),
2543
2474
  Promise.resolve().then(() => (init_payment_form(), payment_form_exports)),
2544
2475
  Promise.resolve().then(() => (init_checkout_button(), checkout_button_exports)),