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