recur-tw 0.17.0 → 0.18.0

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
@@ -757,7 +757,7 @@ var init_payment_form_skeleton = __esmMin((() => {
757
757
  }));
758
758
 
759
759
  //#endregion
760
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/typeof.js
760
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/typeof.js
761
761
  function _typeof(o) {
762
762
  "@babel/helpers - typeof";
763
763
  return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
@@ -769,7 +769,7 @@ function _typeof(o) {
769
769
  var init_typeof = __esmMin((() => {}));
770
770
 
771
771
  //#endregion
772
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/toPrimitive.js
772
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/toPrimitive.js
773
773
  function toPrimitive(t, r) {
774
774
  if ("object" != _typeof(t) || !t) return t;
775
775
  var e = t[Symbol.toPrimitive];
@@ -785,7 +785,7 @@ var init_toPrimitive = __esmMin((() => {
785
785
  }));
786
786
 
787
787
  //#endregion
788
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/toPropertyKey.js
788
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/toPropertyKey.js
789
789
  function toPropertyKey(t) {
790
790
  var i = toPrimitive(t, "string");
791
791
  return "symbol" == _typeof(i) ? i : i + "";
@@ -796,7 +796,7 @@ var init_toPropertyKey = __esmMin((() => {
796
796
  }));
797
797
 
798
798
  //#endregion
799
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/defineProperty.js
799
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/defineProperty.js
800
800
  function _defineProperty(e, r, t) {
801
801
  return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
802
802
  value: t,
@@ -1078,7 +1078,7 @@ var init_payment_form_shadow = __esmMin((() => {
1078
1078
  //#region src/components/styles/payment-form-light.css?css-text
1079
1079
  var payment_form_light_default;
1080
1080
  var init_payment_form_light = __esmMin((() => {
1081
- 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 text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.recur-order-summary__total {\n font-size: 18px;\n line-height: 26px;\n font-weight: 700;\n color: #1a202c;\n text-align: right;\n font-variant-numeric: tabular-nums;\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: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\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 align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__input-wrapper {\n position: relative;\n flex: 1;\n min-width: 0;\n}\n\n.recur-coupon__input {\n width: 100%;\n height: 36px;\n padding: 0 32px 0 12px;\n font-size: 14px;\n text-transform: uppercase;\n background-color: #ffffff;\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:hover:not(:focus) {\n border-color: #9ca3af;\n}\n\n.recur-coupon__input:focus {\n border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.recur-coupon__clear {\n position: absolute;\n right: 8px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #9ca3af;\n border-radius: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: color 0.2s;\n}\n\n.recur-coupon__clear:hover {\n color: #6b7280;\n}\n\n.recur-coupon__clear-icon {\n width: 16px;\n height: 16px;\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 display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.recur-coupon__btn--apply {\n background: #18181b;\n color: white;\n border: none;\n min-width: 52px;\n}\n\n.recur-coupon__btn--apply .recur-coupon__spinner {\n width: 14px;\n height: 14px;\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__error {\n width: 100%;\n margin-top: 8px;\n font-size: 13px;\n color: #dc2626;\n}\n\n.recur-coupon__applied-wrapper {\n width: 100%;\n}\n\n.recur-coupon__description {\n font-size: 12px;\n color: #6b7280;\n margin: 4px 0 0 22px;\n}\n\n.recur-coupon__bonus {\n display: flex;\n align-items: center;\n gap: 6px;\n background: #dcfce7;\n border-radius: 6px;\n padding: 8px 12px;\n margin-top: 8px;\n font-size: 12px;\n color: #15803d;\n}\n\n.recur-coupon__applied {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__applied-info {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n flex: 1;\n}\n\n.recur-coupon__applied-icon {\n width: 16px;\n height: 16px;\n color: #dc2626;\n flex-shrink: 0;\n}\n\n.recur-coupon__applied-name {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n}\n\n.recur-coupon__applied-code {\n font-size: 14px;\n color: #9ca3af;\n white-space: nowrap;\n margin-right: 2px;\n}\n\n.recur-coupon__applied-amount {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n flex-shrink: 0;\n margin-left: auto;\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 flex-shrink: 0;\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 height: 36px;\n padding: 0 12px;\n font-size: 14px;\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 border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\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\n/* ============================================\n * Skeleton Loading Styles\n * ============================================ */\n.recur-skeleton {\n display: inline-block;\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}\n\n/* ============================================\n * Test Mode Banner Styles\n * ============================================ */\n.recur-test-mode-banner {\n background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);\n border: 1px solid #f59e0b;\n border-radius: 8px;\n margin-bottom: 16px;\n overflow: hidden;\n}\n\n.recur-test-mode-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 10px 14px;\n cursor: pointer;\n user-select: none;\n transition: background 0.2s;\n}\n\n.recur-test-mode-header:hover {\n background: rgba(245, 158, 11, 0.1);\n}\n\n.recur-test-mode-badge {\n font-size: 13px;\n font-weight: 600;\n color: #92400e;\n}\n\n.recur-test-mode-toggle {\n font-size: 12px;\n color: #b45309;\n}\n\n.recur-test-cards {\n background: rgba(255, 255, 255, 0.7);\n padding: 12px 14px;\n border-top: 1px solid rgba(245, 158, 11, 0.3);\n}\n\n.recur-test-cards-info {\n font-size: 12px;\n color: #78350f;\n margin: 0 0 10px 0;\n}\n\n.recur-test-card {\n background: white;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n padding: 10px 12px;\n margin-bottom: 8px;\n}\n\n.recur-test-card:last-child {\n margin-bottom: 0;\n}\n\n.recur-test-card-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 4px;\n}\n\n.recur-test-card-number {\n font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;\n font-size: 14px;\n font-weight: 600;\n color: #1f2937;\n letter-spacing: 0.5px;\n}\n\n.recur-test-card-copy {\n font-size: 11px;\n font-weight: 500;\n color: #3b82f6;\n background: #eff6ff;\n border: 1px solid #bfdbfe;\n border-radius: 4px;\n padding: 3px 8px;\n cursor: pointer;\n transition: all 0.2s;\n}\n\n.recur-test-card-copy:hover {\n background: #dbeafe;\n border-color: #93c5fd;\n}\n\n.recur-test-card-copy.copied {\n background: #dcfce7;\n border-color: #86efac;\n color: #16a34a;\n}\n\n.recur-test-card-details {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n font-size: 11px;\n color: #6b7280;\n}\n\n.recur-test-card-desc {\n color: #374151;\n font-weight: 500;\n}\n\n.recur-test-card-meta {\n color: #9ca3af;\n}\n";
1081
+ 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 text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.recur-order-summary__total {\n font-size: 18px;\n line-height: 26px;\n font-weight: 700;\n color: #1a202c;\n text-align: right;\n font-variant-numeric: tabular-nums;\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: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\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 align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__input-wrapper {\n position: relative;\n flex: 1;\n min-width: 0;\n}\n\n.recur-coupon__input {\n width: 100%;\n height: 36px;\n padding: 0 32px 0 12px;\n font-size: 14px;\n text-transform: uppercase;\n background-color: #ffffff;\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:hover:not(:focus) {\n border-color: #9ca3af;\n}\n\n.recur-coupon__input:focus {\n border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.recur-coupon__clear {\n position: absolute;\n right: 8px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #9ca3af;\n border-radius: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: color 0.2s;\n}\n\n.recur-coupon__clear:hover {\n color: #6b7280;\n}\n\n.recur-coupon__clear-icon {\n width: 16px;\n height: 16px;\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 display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.recur-coupon__btn--apply {\n background: #18181b;\n color: white;\n border: none;\n min-width: 52px;\n}\n\n.recur-coupon__btn--apply .recur-coupon__spinner {\n width: 14px;\n height: 14px;\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__error {\n width: 100%;\n margin-top: 8px;\n font-size: 13px;\n color: #dc2626;\n}\n\n.recur-coupon__applied-wrapper {\n width: 100%;\n}\n\n.recur-coupon__description {\n font-size: 12px;\n color: #6b7280;\n margin: 4px 0 0 22px;\n}\n\n.recur-coupon__bonus {\n display: flex;\n align-items: center;\n gap: 6px;\n background: #dcfce7;\n border-radius: 6px;\n padding: 8px 12px;\n margin-top: 8px;\n font-size: 12px;\n color: #15803d;\n}\n\n.recur-coupon__applied {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__applied-info {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n flex: 1;\n}\n\n.recur-coupon__applied-icon {\n width: 16px;\n height: 16px;\n color: #dc2626;\n flex-shrink: 0;\n}\n\n.recur-coupon__applied-name {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n}\n\n.recur-coupon__applied-code {\n font-size: 14px;\n color: #9ca3af;\n white-space: nowrap;\n margin-right: 2px;\n}\n\n.recur-coupon__applied-amount {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n flex-shrink: 0;\n margin-left: auto;\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 flex-shrink: 0;\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 height: 36px;\n padding: 0 12px;\n font-size: 14px;\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 border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\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/* Card verification notice ($0 total, card still bound for future charges) */\n.recur-verification-notice {\n border: 1px solid #bfdbfe;\n background-color: #eff6ff;\n color: #1e40af;\n border-radius: 8px;\n padding: 12px 16px;\n font-size: 14px;\n font-weight: 500;\n margin-bottom: 16px;\n}\n\n.recur-verification-notice.hidden {\n display: none;\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\n/* ============================================\n * Skeleton Loading Styles\n * ============================================ */\n.recur-skeleton {\n display: inline-block;\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}\n\n/* ============================================\n * Test Mode Banner Styles\n * ============================================ */\n.recur-test-mode-banner {\n background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);\n border: 1px solid #f59e0b;\n border-radius: 8px;\n margin-bottom: 16px;\n overflow: hidden;\n}\n\n.recur-test-mode-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 10px 14px;\n cursor: pointer;\n user-select: none;\n transition: background 0.2s;\n}\n\n.recur-test-mode-header:hover {\n background: rgba(245, 158, 11, 0.1);\n}\n\n.recur-test-mode-badge {\n font-size: 13px;\n font-weight: 600;\n color: #92400e;\n}\n\n.recur-test-mode-toggle {\n font-size: 12px;\n color: #b45309;\n}\n\n.recur-test-cards {\n background: rgba(255, 255, 255, 0.7);\n padding: 12px 14px;\n border-top: 1px solid rgba(245, 158, 11, 0.3);\n}\n\n.recur-test-cards-info {\n font-size: 12px;\n color: #78350f;\n margin: 0 0 10px 0;\n}\n\n.recur-test-card {\n background: white;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n padding: 10px 12px;\n margin-bottom: 8px;\n}\n\n.recur-test-card:last-child {\n margin-bottom: 0;\n}\n\n.recur-test-card-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 4px;\n}\n\n.recur-test-card-number {\n font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;\n font-size: 14px;\n font-weight: 600;\n color: #1f2937;\n letter-spacing: 0.5px;\n}\n\n.recur-test-card-copy {\n font-size: 11px;\n font-weight: 500;\n color: #3b82f6;\n background: #eff6ff;\n border: 1px solid #bfdbfe;\n border-radius: 4px;\n padding: 3px 8px;\n cursor: pointer;\n transition: all 0.2s;\n}\n\n.recur-test-card-copy:hover {\n background: #dbeafe;\n border-color: #93c5fd;\n}\n\n.recur-test-card-copy.copied {\n background: #dcfce7;\n border-color: #86efac;\n color: #16a34a;\n}\n\n.recur-test-card-details {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n font-size: 11px;\n color: #6b7280;\n}\n\n.recur-test-card-desc {\n color: #374151;\n font-weight: 500;\n}\n\n.recur-test-card-meta {\n color: #9ca3af;\n}\n";
1082
1082
  }));
1083
1083
 
1084
1084
  //#endregion
@@ -1107,6 +1107,7 @@ function formatPrice(amount) {
1107
1107
  function getCheckoutDiscountDisplay(input) {
1108
1108
  const { discountType, couponDiscountValue, discountAmount, duration, durationMonths, productPrice, productInterval, productIntervalCount, bonusMonths, bonusTrialDays } = input;
1109
1109
  const intervalText = getIntervalText(productInterval, productIntervalCount);
1110
+ const isOneTime = !intervalText;
1110
1111
  const originalPriceText = intervalText ? `${formatPrice(productPrice)}/${intervalText}` : formatPrice(productPrice);
1111
1112
  const bonusSuffix = buildBonusSuffix(bonusMonths, bonusTrialDays);
1112
1113
  if (discountAmount === 0 && (bonusMonths || bonusTrialDays)) return {
@@ -1121,9 +1122,10 @@ function getCheckoutDiscountDisplay(input) {
1121
1122
  if (discountType === "FIRST_PERIOD_PRICE") {
1122
1123
  const firstPeriodPrice = couponDiscountValue;
1123
1124
  const priceText = firstPeriodPrice === 0 ? "免費" : `特價 ${formatPrice(firstPeriodPrice)}`;
1125
+ const base = isOneTime ? priceText : `首期${priceText}`;
1124
1126
  return {
1125
- line1: bonusSuffix ? `首期${priceText},${bonusSuffix}` : `首期${priceText}`,
1126
- line2: afterLine
1127
+ line1: bonusSuffix ? `${base},${bonusSuffix}` : base,
1128
+ line2: isOneTime ? null : afterLine
1127
1129
  };
1128
1130
  }
1129
1131
  if (discountType === "FIXED_AMOUNT") return buildStandardDiscount({
@@ -1131,14 +1133,16 @@ function getCheckoutDiscountDisplay(input) {
1131
1133
  duration,
1132
1134
  durationMonths,
1133
1135
  bonusSuffix,
1134
- afterLine
1136
+ afterLine,
1137
+ isOneTime
1135
1138
  });
1136
1139
  if (discountType === "PERCENTAGE") return buildStandardDiscount({
1137
- discountText: `享 ${formatZhe(couponDiscountValue)}`,
1140
+ discountText: couponDiscountValue >= 1e4 ? "免費" : `享 ${formatZhe(couponDiscountValue)}`,
1138
1141
  duration,
1139
1142
  durationMonths,
1140
1143
  bonusSuffix,
1141
- afterLine
1144
+ afterLine,
1145
+ isOneTime
1142
1146
  });
1143
1147
  return {
1144
1148
  line1: "",
@@ -1146,7 +1150,11 @@ function getCheckoutDiscountDisplay(input) {
1146
1150
  };
1147
1151
  }
1148
1152
  function buildStandardDiscount(opts) {
1149
- const { discountText, duration, durationMonths, bonusSuffix, afterLine } = opts;
1153
+ const { discountText, duration, durationMonths, bonusSuffix, afterLine, isOneTime } = opts;
1154
+ if (isOneTime) return {
1155
+ line1: bonusSuffix ? `${discountText},${bonusSuffix}` : discountText,
1156
+ line2: null
1157
+ };
1150
1158
  switch (duration) {
1151
1159
  case "ONCE": {
1152
1160
  const base = `首期${discountText}`;
@@ -1737,6 +1745,7 @@ var init_payment_form = __esmMin((() => {
1737
1745
  const totalValue = summaryContainer.querySelector(".recur-order-summary__row--total")?.querySelector(".recur-order-summary__total");
1738
1746
  if (totalValue) totalValue.textContent = `NT$ ${finalAmount.toLocaleString()}`;
1739
1747
  this.updateCouponSectionDOM();
1748
+ this.updateVerificationUI();
1740
1749
  }
1741
1750
  async applyCoupon() {
1742
1751
  const code = this._couponCode.trim();
@@ -1875,8 +1884,41 @@ var init_payment_form = __esmMin((() => {
1875
1884
  </div>
1876
1885
  `;
1877
1886
  }
1887
+ /** Final payable amount after any applied coupon (never negative). */
1888
+ getFinalAmount() {
1889
+ const amount = Number(this.getAttribute("amount") || 0);
1890
+ const discountAmount = this._appliedCoupon?.discountAmount || 0;
1891
+ return Math.max(0, amount - discountAmount);
1892
+ }
1893
+ /**
1894
+ * $0 total but the card form is still shown: the backend runs a NT$2
1895
+ * verification charge (voided afterwards) to bind the card for future
1896
+ * charges. The UI must say 驗證 instead of 付款.
1897
+ */
1898
+ isCardVerificationMode() {
1899
+ const amountAttr = this.getAttribute("amount");
1900
+ if (amountAttr === null || amountAttr === "") return false;
1901
+ return this.getFinalAmount() === 0;
1902
+ }
1903
+ getSubmitLabel() {
1904
+ return this.isCardVerificationMode() ? "驗證卡片" : "確認付款";
1905
+ }
1906
+ /** Sync the verification notice + submit label with the current amount. */
1907
+ updateVerificationUI() {
1908
+ const notice = document.getElementById(`${this.containerId}-verification-notice`);
1909
+ if (notice) notice.classList.toggle("hidden", !this.isCardVerificationMode());
1910
+ const submitText = document.getElementById(`${this.containerId}-submit-text`);
1911
+ if (submitText) submitText.textContent = this.getSubmitLabel();
1912
+ }
1878
1913
  renderCardFields() {
1879
1914
  return lit_html.html`
1915
+ <div
1916
+ id="${this.containerId}-verification-notice"
1917
+ class="recur-verification-notice${this.isCardVerificationMode() ? "" : " hidden"}"
1918
+ >
1919
+ 您的優惠碼已抵扣全額,仍需驗證信用卡以供日後扣款使用
1920
+ </div>
1921
+
1880
1922
  <div class="recur-card-field">
1881
1923
  <label class="recur-card-field-label">卡號</label>
1882
1924
  <div class="recur-card-field-container" id="${this.containerId}-card-no-wrapper">
@@ -1922,7 +1964,7 @@ var init_payment_form = __esmMin((() => {
1922
1964
  class="recur-submit-button"
1923
1965
  disabled
1924
1966
  >
1925
- <span id="${this.containerId}-submit-text">確認付款</span>
1967
+ <span id="${this.containerId}-submit-text">${this.getSubmitLabel()}</span>
1926
1968
  </button>
1927
1969
  `;
1928
1970
  }
@@ -2067,7 +2109,7 @@ var init_payment_form = __esmMin((() => {
2067
2109
  } else {
2068
2110
  submitBtn.classList.remove("loading");
2069
2111
  submitBtn.disabled = false;
2070
- submitBtn.innerHTML = "<span id=\"" + this.containerId + "-submit-text\">確認付款</span>";
2112
+ submitBtn.innerHTML = "<span id=\"" + this.containerId + "-submit-text\">" + this.getSubmitLabel() + "</span>";
2071
2113
  }
2072
2114
  }
2073
2115
  resetButton() {
@@ -2763,9 +2805,66 @@ function toCamelCase(obj) {
2763
2805
  return obj;
2764
2806
  }
2765
2807
 
2808
+ //#endregion
2809
+ //#region src/einvoice.ts
2810
+ /** 手機條碼: `/` + 7 chars from the MOF alphabet. */
2811
+ const MOBILE_BARCODE_RE = /^\/[0-9A-Z.+-]{7}$/;
2812
+ /** 統一編號: 8 digits. */
2813
+ const UBN_RE = /^\d{8}$/;
2814
+ /** 愛心碼: 3–7 digits. */
2815
+ const DONATION_CODE_RE = /^\d{3,7}$/;
2816
+ /**
2817
+ * 統一編號 checksum (post-2023 rule: weighted digit-sum divisible by 5, with
2818
+ * the 7-in-7th-digit alternate). Providers reject checksum-invalid UBNs at
2819
+ * issue time as a non-retryable failure, so the SDK catches this at entry.
2820
+ */
2821
+ function isValidTaiwanUbn(ubn) {
2822
+ if (!UBN_RE.test(ubn)) return false;
2823
+ const weights = [
2824
+ 1,
2825
+ 2,
2826
+ 1,
2827
+ 2,
2828
+ 1,
2829
+ 2,
2830
+ 4,
2831
+ 1
2832
+ ];
2833
+ let sum = 0;
2834
+ for (let i = 0; i < 8; i++) {
2835
+ const product = Number(ubn.charAt(i)) * (weights[i] ?? 0);
2836
+ sum += Math.floor(product / 10) + product % 10;
2837
+ }
2838
+ if (sum % 5 === 0) return true;
2839
+ return ubn.charAt(6) === "7" && (sum + 1) % 5 === 0;
2840
+ }
2841
+ /**
2842
+ * Validate buyer invoice preferences.
2843
+ *
2844
+ * @returns an error message, or `null` when the prefs are valid
2845
+ */
2846
+ function validateEinvoicePrefs(prefs) {
2847
+ if (typeof prefs !== "object" || prefs === null) return "Invalid einvoice prefs: expected an object like { type: \"personal\" }";
2848
+ switch (prefs.type) {
2849
+ case "personal": return null;
2850
+ case "mobile_barcode": return MOBILE_BARCODE_RE.test(prefs.carrierCode) ? null : "Invalid einvoice mobile barcode: must be \"/\" followed by 7 characters (0-9, A-Z, ., +, -)";
2851
+ case "ubn":
2852
+ if (!isValidTaiwanUbn(prefs.ubn)) return "Invalid einvoice UBN (統一編號): must be 8 digits with a valid checksum";
2853
+ if (!prefs.buyerName || !prefs.buyerName.trim() || prefs.buyerName.trim().length > 60) return "Invalid einvoice buyerName (公司抬頭): required, at most 60 characters";
2854
+ return null;
2855
+ case "donation": return DONATION_CODE_RE.test(prefs.donationCode) ? null : "Invalid einvoice donation code (愛心碼): must be 3-7 digits";
2856
+ default: return `Invalid einvoice type: ${String(prefs.type)}`;
2857
+ }
2858
+ }
2859
+ /** Throw when buyer invoice preferences are invalid (fail fast, before the API call). */
2860
+ function assertValidEinvoicePrefs(prefs) {
2861
+ const error = validateEinvoicePrefs(prefs);
2862
+ if (error) throw new Error(error);
2863
+ }
2864
+
2766
2865
  //#endregion
2767
2866
  //#region package.json
2768
- var version = "0.17.0";
2867
+ var version = "0.18.0";
2769
2868
 
2770
2869
  //#endregion
2771
2870
  //#region src/context.tsx
@@ -3108,6 +3207,10 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3108
3207
  if (options.externalCustomerId) checkoutRequestBody.external_customer_id = options.externalCustomerId;
3109
3208
  if (options.successUrl) checkoutRequestBody.success_url = options.successUrl;
3110
3209
  if (options.cancelUrl) checkoutRequestBody.cancel_url = options.cancelUrl;
3210
+ if (options.einvoice) {
3211
+ assertValidEinvoicePrefs(options.einvoice);
3212
+ checkoutRequestBody.einvoice = options.einvoice;
3213
+ }
3111
3214
  const checkoutResponse = await fetch(`${baseUrl}/v1/checkouts`, {
3112
3215
  method: "POST",
3113
3216
  headers,
@@ -3559,6 +3662,10 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3559
3662
  if (options.customerEmail) requestBody.customerEmail = options.customerEmail;
3560
3663
  if (options.customerName) requestBody.customerName = options.customerName;
3561
3664
  if (options.externalCustomerId) requestBody.externalCustomerId = options.externalCustomerId;
3665
+ if (options.einvoice) {
3666
+ assertValidEinvoicePrefs(options.einvoice);
3667
+ requestBody.einvoice = options.einvoice;
3668
+ }
3562
3669
  const response = await fetch(`${baseUrl}/v1/checkout/sessions`, {
3563
3670
  method: "POST",
3564
3671
  headers: {
@@ -3579,7 +3686,8 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3579
3686
  id: result.id,
3580
3687
  url: result.url,
3581
3688
  expiresAt: result.expiresAt,
3582
- clientSecret: result.clientSecret
3689
+ clientSecret: result.clientSecret,
3690
+ einvoiceEnabled: result.einvoiceEnabled
3583
3691
  };
3584
3692
  }, [config]);
3585
3693
  const redirectToCheckout = (0, react.useCallback)(async (options) => {
@@ -4331,11 +4439,17 @@ function useCustomer() {
4331
4439
  }
4332
4440
 
4333
4441
  //#endregion
4442
+ exports.DONATION_CODE_RE = DONATION_CODE_RE;
4443
+ exports.MOBILE_BARCODE_RE = MOBILE_BARCODE_RE;
4334
4444
  exports.PromoCodeInput = PromoCodeInput;
4335
4445
  exports.RecurProvider = RecurProvider;
4446
+ exports.UBN_RE = UBN_RE;
4447
+ exports.assertValidEinvoicePrefs = assertValidEinvoicePrefs;
4448
+ exports.isValidTaiwanUbn = isValidTaiwanUbn;
4336
4449
  exports.useCustomer = useCustomer;
4337
4450
  exports.usePlans = useProducts;
4338
4451
  exports.useProducts = useProducts;
4339
4452
  exports.usePromoCode = usePromoCode;
4340
4453
  exports.useRecur = useRecur;
4341
- exports.useSubscribe = useSubscribe;
4454
+ exports.useSubscribe = useSubscribe;
4455
+ exports.validateEinvoicePrefs = validateEinvoicePrefs;
package/dist/index.d.cts CHANGED
@@ -159,12 +159,58 @@ declare global {
159
159
  }
160
160
  }
161
161
  //#endregion
162
- //#region src/types.d.ts
162
+ //#region src/einvoice.d.ts
163
+ /**
164
+ * Taiwan e-invoice (電子發票) buyer preferences
165
+ *
166
+ * Mirrors the server's einvoicePrefsSchema (@workspace/einvoice). Validation is
167
+ * kept inline — no zod, no dependency on the einvoice packages — so the browser
168
+ * bundles stay lean. A contract test (__tests__/einvoice.test.ts) pins this
169
+ * implementation against the server schema and @paid-tw/einvoice's validators.
170
+ */
171
+ /**
172
+ * Buyer invoice choice, collected at checkout when the merchant has an active
173
+ * e-invoice integration:
174
+ * - `personal`: 個人雲端發票, delivered via the buyer's email
175
+ * - `mobile_barcode`: 手機條碼載具, e.g. `/ABC1234`
176
+ * - `ubn`: 公司統編三聯式 (B2B), 8-digit 統一編號 + company title
177
+ * - `donation`: 捐贈發票 via 愛心碼
178
+ */
179
+ type EinvoicePrefs = {
180
+ type: 'personal';
181
+ } | {
182
+ type: 'mobile_barcode';
183
+ carrierCode: string;
184
+ } | {
185
+ type: 'ubn';
186
+ ubn: string;
187
+ buyerName: string;
188
+ } | {
189
+ type: 'donation';
190
+ donationCode: string;
191
+ };
192
+ /** 手機條碼: `/` + 7 chars from the MOF alphabet. */
193
+ declare const MOBILE_BARCODE_RE: RegExp;
194
+ /** 統一編號: 8 digits. */
195
+ declare const UBN_RE: RegExp;
196
+ /** 愛心碼: 3–7 digits. */
197
+ declare const DONATION_CODE_RE: RegExp;
198
+ /**
199
+ * 統一編號 checksum (post-2023 rule: weighted digit-sum divisible by 5, with
200
+ * the 7-in-7th-digit alternate). Providers reject checksum-invalid UBNs at
201
+ * issue time as a non-retryable failure, so the SDK catches this at entry.
202
+ */
203
+ declare function isValidTaiwanUbn(ubn: string): boolean;
163
204
  /**
164
- * Recur SDK Types
205
+ * Validate buyer invoice preferences.
165
206
  *
166
- * Type definitions for the Recur checkout SDK
207
+ * @returns an error message, or `null` when the prefs are valid
167
208
  */
209
+ declare function validateEinvoicePrefs(prefs: EinvoicePrefs): string | null;
210
+ /** Throw when buyer invoice preferences are invalid (fail fast, before the API call). */
211
+ declare function assertValidEinvoicePrefs(prefs: EinvoicePrefs): void;
212
+ //#endregion
213
+ //#region src/types.d.ts
168
214
  interface RecurConfig {
169
215
  /**
170
216
  * Publishable API key for authentication
@@ -251,11 +297,23 @@ interface CheckoutOptions {
251
297
  /**
252
298
  * External customer ID from your system (optional)
253
299
  * Use this to link Recur subscriptions to your existing users.
254
- * Once set during customer creation, this cannot be changed.
300
+ * Applied when the customer is created or first linked during checkout;
301
+ * can later be updated via the Secret Key API (PATCH /v1/customers/:id).
255
302
  *
256
303
  * @example 'user_123', 'cus_abc456'
257
304
  */
258
305
  externalCustomerId?: string;
306
+ /**
307
+ * Taiwan e-invoice buyer preferences (optional)
308
+ * Used when the organization has an active e-invoice integration
309
+ * (`einvoiceEnabled` in the checkout response). Collect the buyer's choice
310
+ * in your own UI and pass it here; it is validated locally and stored with
311
+ * the checkout so the invoice is issued accordingly after payment.
312
+ *
313
+ * @example { type: 'mobile_barcode', carrierCode: '/ABC1234' }
314
+ * @example { type: 'ubn', ubn: '04595252', buyerName: '恆吉股份有限公司' }
315
+ */
316
+ einvoice?: EinvoicePrefs;
259
317
  /**
260
318
  * URL to redirect after successful payment (recommended for mobile)
261
319
  * Enables redirect after 3D verification in mobile environments.
@@ -367,6 +425,8 @@ interface CheckoutResult {
367
425
  * Used to determine PAYUNi SDK environment
368
426
  */
369
427
  livemode?: boolean;
428
+ /** Whether the organization issues e-invoices for paid checkouts */
429
+ einvoiceEnabled?: boolean;
370
430
  }
371
431
  /**
372
432
  * Payment failure codes from payment provider
@@ -568,6 +628,16 @@ interface RedirectToCheckoutOptions {
568
628
  * @example 'user_123', 'cus_abc456'
569
629
  */
570
630
  externalCustomerId?: string;
631
+ /**
632
+ * Taiwan e-invoice buyer preferences (optional)
633
+ * Prefills the 發票資訊 section on the hosted checkout page (the buyer can
634
+ * still change it there). Only used when the organization has an active
635
+ * e-invoice integration.
636
+ *
637
+ * @example { type: 'mobile_barcode', carrierCode: '/ABC1234' }
638
+ * @example { type: 'ubn', ubn: '04595252', buyerName: '恆吉股份有限公司' }
639
+ */
640
+ einvoice?: EinvoicePrefs;
571
641
  }
572
642
  /**
573
643
  * Result from creating a checkout session
@@ -581,6 +651,8 @@ interface CheckoutSessionResult {
581
651
  expiresAt: string;
582
652
  /** Stripe-like client secret for secure status polling */
583
653
  clientSecret?: string;
654
+ /** Whether the organization issues e-invoices for paid checkouts */
655
+ einvoiceEnabled?: boolean;
584
656
  }
585
657
  interface RecurContextValue {
586
658
  /**
@@ -1064,6 +1136,17 @@ declare class RecurPaymentForm extends HTMLElement {
1064
1136
  productPrice?: number;
1065
1137
  } | null;
1066
1138
  private renderCustomerInfo;
1139
+ /** Final payable amount after any applied coupon (never negative). */
1140
+ private getFinalAmount;
1141
+ /**
1142
+ * $0 total but the card form is still shown: the backend runs a NT$2
1143
+ * verification charge (voided afterwards) to bind the card for future
1144
+ * charges. The UI must say 驗證 instead of 付款.
1145
+ */
1146
+ private isCardVerificationMode;
1147
+ private getSubmitLabel;
1148
+ /** Sync the verification notice + submit label with the current amount. */
1149
+ private updateVerificationUI;
1067
1150
  private renderCardFields;
1068
1151
  private hideCardSkeletons;
1069
1152
  private renderActions;
@@ -1775,4 +1858,4 @@ declare function PromoCodeInput({ promo, placeholder, applyText, clearText, disa
1775
1858
  */
1776
1859
  declare function useCustomer(): UseCustomerResult;
1777
1860
  //#endregion
1778
- export { type ApiErrorCode, type ApplicableProduct, type CheckDeniedReason, type CheckOptions, type CheckResult, type CheckoutError, type CheckoutErrorDetails, type CheckoutOptions, type CheckoutResult, type CheckoutSessionResult, type CustomerIdentifier, type DuplicateSubscriptionDetails, type Entitlement, type EntitlementCustomer, type EntitlementStatus, type EntitlementSubscription, type PaymentFailedAction, type PaymentFailureCode, type PaymentFailureDetails, type Plan, type PlansResult, type Product, type ProductMetadata, type ProductsResult, type PromoCodeDiscount, PromoCodeInput, type PromoCodeInputProps, type PromoCodeStatus, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type RedirectToCheckoutOptions, type SubscriptionResult, type UseCustomerResult, type UseProductsOptions as UsePlansOptions, type UseProductsOptions, type UseProductsResult as UsePlansResult, type UseProductsResult, type UsePromoCodeOptions, type UsePromoCodeReturn, type UseSubscribeOptions, type UseSubscribeResult, useCustomer, useProducts as usePlans, useProducts, usePromoCode, useRecur, useSubscribe };
1861
+ export { type ApiErrorCode, type ApplicableProduct, type CheckDeniedReason, type CheckOptions, type CheckResult, type CheckoutError, type CheckoutErrorDetails, type CheckoutOptions, type CheckoutResult, type CheckoutSessionResult, type CustomerIdentifier, DONATION_CODE_RE, type DuplicateSubscriptionDetails, type EinvoicePrefs, type Entitlement, type EntitlementCustomer, type EntitlementStatus, type EntitlementSubscription, MOBILE_BARCODE_RE, type PaymentFailedAction, type PaymentFailureCode, type PaymentFailureDetails, type Plan, type PlansResult, type Product, type ProductMetadata, type ProductsResult, type PromoCodeDiscount, PromoCodeInput, type PromoCodeInputProps, type PromoCodeStatus, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type RedirectToCheckoutOptions, type SubscriptionResult, UBN_RE, type UseCustomerResult, type UseProductsOptions as UsePlansOptions, type UseProductsOptions, type UseProductsResult as UsePlansResult, type UseProductsResult, type UsePromoCodeOptions, type UsePromoCodeReturn, type UseSubscribeOptions, type UseSubscribeResult, assertValidEinvoicePrefs, isValidTaiwanUbn, useCustomer, useProducts as usePlans, useProducts, usePromoCode, useRecur, useSubscribe, validateEinvoicePrefs };
package/dist/index.d.ts CHANGED
@@ -159,12 +159,58 @@ declare global {
159
159
  }
160
160
  }
161
161
  //#endregion
162
- //#region src/types.d.ts
162
+ //#region src/einvoice.d.ts
163
+ /**
164
+ * Taiwan e-invoice (電子發票) buyer preferences
165
+ *
166
+ * Mirrors the server's einvoicePrefsSchema (@workspace/einvoice). Validation is
167
+ * kept inline — no zod, no dependency on the einvoice packages — so the browser
168
+ * bundles stay lean. A contract test (__tests__/einvoice.test.ts) pins this
169
+ * implementation against the server schema and @paid-tw/einvoice's validators.
170
+ */
171
+ /**
172
+ * Buyer invoice choice, collected at checkout when the merchant has an active
173
+ * e-invoice integration:
174
+ * - `personal`: 個人雲端發票, delivered via the buyer's email
175
+ * - `mobile_barcode`: 手機條碼載具, e.g. `/ABC1234`
176
+ * - `ubn`: 公司統編三聯式 (B2B), 8-digit 統一編號 + company title
177
+ * - `donation`: 捐贈發票 via 愛心碼
178
+ */
179
+ type EinvoicePrefs = {
180
+ type: 'personal';
181
+ } | {
182
+ type: 'mobile_barcode';
183
+ carrierCode: string;
184
+ } | {
185
+ type: 'ubn';
186
+ ubn: string;
187
+ buyerName: string;
188
+ } | {
189
+ type: 'donation';
190
+ donationCode: string;
191
+ };
192
+ /** 手機條碼: `/` + 7 chars from the MOF alphabet. */
193
+ declare const MOBILE_BARCODE_RE: RegExp;
194
+ /** 統一編號: 8 digits. */
195
+ declare const UBN_RE: RegExp;
196
+ /** 愛心碼: 3–7 digits. */
197
+ declare const DONATION_CODE_RE: RegExp;
198
+ /**
199
+ * 統一編號 checksum (post-2023 rule: weighted digit-sum divisible by 5, with
200
+ * the 7-in-7th-digit alternate). Providers reject checksum-invalid UBNs at
201
+ * issue time as a non-retryable failure, so the SDK catches this at entry.
202
+ */
203
+ declare function isValidTaiwanUbn(ubn: string): boolean;
163
204
  /**
164
- * Recur SDK Types
205
+ * Validate buyer invoice preferences.
165
206
  *
166
- * Type definitions for the Recur checkout SDK
207
+ * @returns an error message, or `null` when the prefs are valid
167
208
  */
209
+ declare function validateEinvoicePrefs(prefs: EinvoicePrefs): string | null;
210
+ /** Throw when buyer invoice preferences are invalid (fail fast, before the API call). */
211
+ declare function assertValidEinvoicePrefs(prefs: EinvoicePrefs): void;
212
+ //#endregion
213
+ //#region src/types.d.ts
168
214
  interface RecurConfig {
169
215
  /**
170
216
  * Publishable API key for authentication
@@ -251,11 +297,23 @@ interface CheckoutOptions {
251
297
  /**
252
298
  * External customer ID from your system (optional)
253
299
  * Use this to link Recur subscriptions to your existing users.
254
- * Once set during customer creation, this cannot be changed.
300
+ * Applied when the customer is created or first linked during checkout;
301
+ * can later be updated via the Secret Key API (PATCH /v1/customers/:id).
255
302
  *
256
303
  * @example 'user_123', 'cus_abc456'
257
304
  */
258
305
  externalCustomerId?: string;
306
+ /**
307
+ * Taiwan e-invoice buyer preferences (optional)
308
+ * Used when the organization has an active e-invoice integration
309
+ * (`einvoiceEnabled` in the checkout response). Collect the buyer's choice
310
+ * in your own UI and pass it here; it is validated locally and stored with
311
+ * the checkout so the invoice is issued accordingly after payment.
312
+ *
313
+ * @example { type: 'mobile_barcode', carrierCode: '/ABC1234' }
314
+ * @example { type: 'ubn', ubn: '04595252', buyerName: '恆吉股份有限公司' }
315
+ */
316
+ einvoice?: EinvoicePrefs;
259
317
  /**
260
318
  * URL to redirect after successful payment (recommended for mobile)
261
319
  * Enables redirect after 3D verification in mobile environments.
@@ -367,6 +425,8 @@ interface CheckoutResult {
367
425
  * Used to determine PAYUNi SDK environment
368
426
  */
369
427
  livemode?: boolean;
428
+ /** Whether the organization issues e-invoices for paid checkouts */
429
+ einvoiceEnabled?: boolean;
370
430
  }
371
431
  /**
372
432
  * Payment failure codes from payment provider
@@ -568,6 +628,16 @@ interface RedirectToCheckoutOptions {
568
628
  * @example 'user_123', 'cus_abc456'
569
629
  */
570
630
  externalCustomerId?: string;
631
+ /**
632
+ * Taiwan e-invoice buyer preferences (optional)
633
+ * Prefills the 發票資訊 section on the hosted checkout page (the buyer can
634
+ * still change it there). Only used when the organization has an active
635
+ * e-invoice integration.
636
+ *
637
+ * @example { type: 'mobile_barcode', carrierCode: '/ABC1234' }
638
+ * @example { type: 'ubn', ubn: '04595252', buyerName: '恆吉股份有限公司' }
639
+ */
640
+ einvoice?: EinvoicePrefs;
571
641
  }
572
642
  /**
573
643
  * Result from creating a checkout session
@@ -581,6 +651,8 @@ interface CheckoutSessionResult {
581
651
  expiresAt: string;
582
652
  /** Stripe-like client secret for secure status polling */
583
653
  clientSecret?: string;
654
+ /** Whether the organization issues e-invoices for paid checkouts */
655
+ einvoiceEnabled?: boolean;
584
656
  }
585
657
  interface RecurContextValue {
586
658
  /**
@@ -1064,6 +1136,17 @@ declare class RecurPaymentForm extends HTMLElement {
1064
1136
  productPrice?: number;
1065
1137
  } | null;
1066
1138
  private renderCustomerInfo;
1139
+ /** Final payable amount after any applied coupon (never negative). */
1140
+ private getFinalAmount;
1141
+ /**
1142
+ * $0 total but the card form is still shown: the backend runs a NT$2
1143
+ * verification charge (voided afterwards) to bind the card for future
1144
+ * charges. The UI must say 驗證 instead of 付款.
1145
+ */
1146
+ private isCardVerificationMode;
1147
+ private getSubmitLabel;
1148
+ /** Sync the verification notice + submit label with the current amount. */
1149
+ private updateVerificationUI;
1067
1150
  private renderCardFields;
1068
1151
  private hideCardSkeletons;
1069
1152
  private renderActions;
@@ -1775,4 +1858,4 @@ declare function PromoCodeInput({ promo, placeholder, applyText, clearText, disa
1775
1858
  */
1776
1859
  declare function useCustomer(): UseCustomerResult;
1777
1860
  //#endregion
1778
- export { type ApiErrorCode, type ApplicableProduct, type CheckDeniedReason, type CheckOptions, type CheckResult, type CheckoutError, type CheckoutErrorDetails, type CheckoutOptions, type CheckoutResult, type CheckoutSessionResult, type CustomerIdentifier, type DuplicateSubscriptionDetails, type Entitlement, type EntitlementCustomer, type EntitlementStatus, type EntitlementSubscription, type PaymentFailedAction, type PaymentFailureCode, type PaymentFailureDetails, type Plan, type PlansResult, type Product, type ProductMetadata, type ProductsResult, type PromoCodeDiscount, PromoCodeInput, type PromoCodeInputProps, type PromoCodeStatus, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type RedirectToCheckoutOptions, type SubscriptionResult, type UseCustomerResult, type UseProductsOptions as UsePlansOptions, type UseProductsOptions, type UseProductsResult as UsePlansResult, type UseProductsResult, type UsePromoCodeOptions, type UsePromoCodeReturn, type UseSubscribeOptions, type UseSubscribeResult, useCustomer, useProducts as usePlans, useProducts, usePromoCode, useRecur, useSubscribe };
1861
+ export { type ApiErrorCode, type ApplicableProduct, type CheckDeniedReason, type CheckOptions, type CheckResult, type CheckoutError, type CheckoutErrorDetails, type CheckoutOptions, type CheckoutResult, type CheckoutSessionResult, type CustomerIdentifier, DONATION_CODE_RE, type DuplicateSubscriptionDetails, type EinvoicePrefs, type Entitlement, type EntitlementCustomer, type EntitlementStatus, type EntitlementSubscription, MOBILE_BARCODE_RE, type PaymentFailedAction, type PaymentFailureCode, type PaymentFailureDetails, type Plan, type PlansResult, type Product, type ProductMetadata, type ProductsResult, type PromoCodeDiscount, PromoCodeInput, type PromoCodeInputProps, type PromoCodeStatus, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type RedirectToCheckoutOptions, type SubscriptionResult, UBN_RE, type UseCustomerResult, type UseProductsOptions as UsePlansOptions, type UseProductsOptions, type UseProductsResult as UsePlansResult, type UseProductsResult, type UsePromoCodeOptions, type UsePromoCodeReturn, type UseSubscribeOptions, type UseSubscribeResult, assertValidEinvoicePrefs, isValidTaiwanUbn, useCustomer, useProducts as usePlans, useProducts, usePromoCode, useRecur, useSubscribe, validateEinvoicePrefs };