recur-tw 0.11.0 → 0.12.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
@@ -1048,7 +1048,7 @@ var init_payment_form_shadow = __esm({
1048
1048
  var payment_form_light_default;
1049
1049
  var init_payment_form_light = __esm({
1050
1050
  "src/components/styles/payment-form-light.css"() {
1051
- 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";
1051
+ 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";
1052
1052
  }
1053
1053
  });
1054
1054
 
@@ -1097,6 +1097,8 @@ var init_payment_form = __esm({
1097
1097
  __publicField(this, "customStyles");
1098
1098
  __publicField(this, "_isInitializing", false);
1099
1099
  __publicField(this, "_initializationAborted", false);
1100
+ // Test mode state
1101
+ __publicField(this, "_showTestCardHints", false);
1100
1102
  // Coupon state
1101
1103
  __publicField(this, "_showCouponInput", false);
1102
1104
  __publicField(this, "_couponCode", "");
@@ -1163,6 +1165,8 @@ var init_payment_form = __esm({
1163
1165
  <style>${this.getShadowDOMStyles()}</style>
1164
1166
 
1165
1167
  <div class="payment-form-wrapper">
1168
+ <slot name="test-mode-banner"></slot>
1169
+
1166
1170
  <div class="form-header">
1167
1171
  <h2 class="form-title">訂閱付款</h2>
1168
1172
  </div>
@@ -1199,6 +1203,10 @@ var init_payment_form = __esm({
1199
1203
  ${this.getCustomStyles()}
1200
1204
  </style>
1201
1205
 
1206
+ <div slot="test-mode-banner">
1207
+ ${this.renderTestCardHints()}
1208
+ </div>
1209
+
1202
1210
  <div slot="order-summary">
1203
1211
  ${this.renderOrderSummary()}
1204
1212
  </div>
@@ -1235,6 +1243,63 @@ var init_payment_form = __esm({
1235
1243
  }
1236
1244
  }
1237
1245
  // ============================================
1246
+ // Test Mode Helpers
1247
+ // ============================================
1248
+ isTestMode() {
1249
+ const publishableKey = this.getAttribute("publishable-key") || "";
1250
+ return publishableKey.startsWith("pk_test_");
1251
+ }
1252
+ renderTestCardHints() {
1253
+ if (!this.isTestMode()) {
1254
+ return litHtml.html``;
1255
+ }
1256
+ const testCards = [
1257
+ { number: "4147 6310 0000 0001", desc: "VISA \u6E2C\u8A66\u5361\uFF08\u6388\u6B0A\u6210\u529F\uFF09", expiry: "\u4EFB\u610F\u672A\u4F86\u65E5\u671F", cvc: "\u4EFB\u610F3\u78BC" },
1258
+ { number: "3560 5110 0000 0001", desc: "JCB \u6E2C\u8A66\u5361\uFF08\u6388\u6B0A\u6210\u529F\uFF09", expiry: "\u4EFB\u610F\u672A\u4F86\u65E5\u671F", cvc: "\u4EFB\u610F3\u78BC" },
1259
+ { number: "4147 6310 0000 0002", desc: "VISA \u6E2C\u8A66\u5361\uFF083D\u9A57\u8B49\u5931\u6557\uFF09", expiry: "\u4EFB\u610F\u672A\u4F86\u65E5\u671F", cvc: "\u4EFB\u610F3\u78BC" }
1260
+ ];
1261
+ const copyToClipboard = (text, event) => {
1262
+ const btn = event.currentTarget;
1263
+ navigator.clipboard.writeText(text.replace(/\s/g, "")).then(() => {
1264
+ const originalText = btn.textContent;
1265
+ btn.textContent = "\u5DF2\u8907\u88FD!";
1266
+ btn.classList.add("copied");
1267
+ setTimeout(() => {
1268
+ btn.textContent = originalText;
1269
+ btn.classList.remove("copied");
1270
+ }, 1500);
1271
+ });
1272
+ };
1273
+ return litHtml.html`
1274
+ <div class="recur-test-mode-banner">
1275
+ <div class="recur-test-mode-header" @click=${() => {
1276
+ this._showTestCardHints = !this._showTestCardHints;
1277
+ this.renderLightDOM();
1278
+ }}>
1279
+ <span class="recur-test-mode-badge">🧪 測試模式</span>
1280
+ <span class="recur-test-mode-toggle">${this._showTestCardHints ? "\u6536\u5408 \u25B2" : "\u5C55\u958B\u6E2C\u8A66\u5361\u865F \u25BC"}</span>
1281
+ </div>
1282
+ ${this._showTestCardHints ? litHtml.html`
1283
+ <div class="recur-test-cards">
1284
+ <p class="recur-test-cards-info">使用以下測試卡號進行測試付款:</p>
1285
+ ${testCards.map((card) => litHtml.html`
1286
+ <div class="recur-test-card">
1287
+ <div class="recur-test-card-row">
1288
+ <span class="recur-test-card-number">${card.number}</span>
1289
+ <button type="button" class="recur-test-card-copy" @click=${(e) => copyToClipboard(card.number, e)}>複製</button>
1290
+ </div>
1291
+ <div class="recur-test-card-details">
1292
+ <span class="recur-test-card-desc">${card.desc}</span>
1293
+ <span class="recur-test-card-meta">到期日: ${card.expiry} / CVC: ${card.cvc}</span>
1294
+ </div>
1295
+ </div>
1296
+ `)}
1297
+ </div>
1298
+ ` : litHtml.nothing}
1299
+ </div>
1300
+ `;
1301
+ }
1302
+ // ============================================
1238
1303
  // Section Renderers (lit-html)
1239
1304
  // ============================================
1240
1305
  renderOrderSummary() {
@@ -1648,16 +1713,16 @@ var init_payment_form = __esm({
1648
1713
  }
1649
1714
  this._appliedCoupon = {
1650
1715
  code: data.discount.code,
1651
- couponName: data.discount.couponName,
1652
- discountType: data.discount.discountType,
1653
- discountAmount: data.discount.discountAmount,
1716
+ couponName: data.discount.coupon_name,
1717
+ discountType: data.discount.discount_type,
1718
+ discountAmount: data.discount.discount_amount,
1654
1719
  duration: data.discount.duration,
1655
- durationMonths: data.discount.durationMonths,
1656
- bonusTrialDays: data.discount.bonusTrialDays,
1657
- bonusMonths: data.discount.bonusMonths,
1658
- productInterval: data.discount.productInterval,
1659
- productIntervalCount: data.discount.productIntervalCount,
1660
- productPrice: data.discount.productPrice
1720
+ durationMonths: data.discount.duration_months,
1721
+ bonusTrialDays: data.discount.bonus_trial_days,
1722
+ bonusMonths: data.discount.bonus_months,
1723
+ productInterval: data.discount.product_interval,
1724
+ productIntervalCount: data.discount.product_interval_count,
1725
+ productPrice: data.discount.product_price
1661
1726
  };
1662
1727
  this._showCouponInput = false;
1663
1728
  this._couponCode = "";
@@ -2674,7 +2739,7 @@ function toCamelCase(obj) {
2674
2739
 
2675
2740
  // package.json
2676
2741
  var package_default = {
2677
- version: "0.11.0"};
2742
+ version: "0.12.0"};
2678
2743
  var SDK_VERSION = package_default.version;
2679
2744
  var SDK_TYPE = "react";
2680
2745
  var RecurContext = React.createContext(null);
@@ -2988,14 +3053,14 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
2988
3053
  }
2989
3054
  console.log("[Recur SDK] Step 1: Creating checkout session...");
2990
3055
  const checkoutRequestBody = {
2991
- customerName: options.customerName,
2992
- customerEmail: options.customerEmail
3056
+ customer_name: options.customerName,
3057
+ customer_email: options.customerEmail
2993
3058
  };
2994
- if (productId) checkoutRequestBody.productId = productId;
2995
- if (productSlug) checkoutRequestBody.productSlug = productSlug;
2996
- if (options.externalCustomerId) checkoutRequestBody.externalCustomerId = options.externalCustomerId;
2997
- if (options.successUrl) checkoutRequestBody.successUrl = options.successUrl;
2998
- if (options.cancelUrl) checkoutRequestBody.cancelUrl = options.cancelUrl;
3059
+ if (productId) checkoutRequestBody.product_id = productId;
3060
+ if (productSlug) checkoutRequestBody.product_slug = productSlug;
3061
+ if (options.externalCustomerId) checkoutRequestBody.external_customer_id = options.externalCustomerId;
3062
+ if (options.successUrl) checkoutRequestBody.success_url = options.successUrl;
3063
+ if (options.cancelUrl) checkoutRequestBody.cancel_url = options.cancelUrl;
2999
3064
  const checkoutResponse = await fetch(`${baseUrl}/v1/checkouts`, {
3000
3065
  method: "POST",
3001
3066
  headers,
@@ -3610,28 +3675,65 @@ function useCustomer() {
3610
3675
  throw new Error("useCustomer must be used within a RecurProvider");
3611
3676
  }
3612
3677
  const { customer, subscription, entitlements, isLoading: isLoading2, error, refetch } = context;
3613
- const check = React.useCallback(
3614
- (options) => {
3678
+ const resolveProduct = (target) => {
3679
+ if (typeof target === "string") {
3680
+ return target;
3681
+ }
3682
+ if ("product" in target) {
3683
+ return target.product;
3684
+ }
3685
+ if ("feature" in target) {
3686
+ throw new Error("Feature-based checks are not yet supported. Coming in Phase 2.");
3687
+ }
3688
+ if ("benefit" in target) {
3689
+ throw new Error("Benefit-based checks are not yet supported. Coming in Phase 2.");
3690
+ }
3691
+ throw new Error("Invalid check target");
3692
+ };
3693
+ const checkSync = React.useCallback(
3694
+ (target) => {
3695
+ const product = resolveProduct(target);
3615
3696
  if (!customer) {
3616
3697
  return { allowed: false, reason: "no_customer" };
3617
3698
  }
3618
3699
  if (entitlements.length === 0) {
3619
- return { allowed: false, reason: "no_subscription" };
3700
+ return { allowed: false, reason: "no_entitlement" };
3620
3701
  }
3621
- const entitlement = entitlements.find(
3622
- (e) => e.product === options.product || e.productId === options.product
3702
+ const matchedEntitlement = entitlements.find(
3703
+ (e) => e.product === product || e.productId === product
3623
3704
  );
3624
- if (!entitlement) {
3625
- return { allowed: false, reason: "wrong_product" };
3705
+ if (!matchedEntitlement) {
3706
+ return { allowed: false, reason: "not_found" };
3626
3707
  }
3627
3708
  let matchingSubscription;
3628
- if (subscription && (subscription.product.slug === options.product || subscription.product.id === options.product)) {
3709
+ if (subscription && (subscription.product.slug === product || subscription.product.id === product)) {
3629
3710
  matchingSubscription = subscription;
3630
3711
  }
3631
- return { allowed: true, subscription: matchingSubscription };
3712
+ return {
3713
+ allowed: true,
3714
+ entitlement: matchedEntitlement,
3715
+ // Backward compatibility
3716
+ subscription: matchingSubscription
3717
+ };
3632
3718
  },
3633
3719
  [customer, entitlements, subscription]
3634
3720
  );
3721
+ const checkLive = React.useCallback(
3722
+ async (target) => {
3723
+ await refetch();
3724
+ return checkSync(target);
3725
+ },
3726
+ [refetch, checkSync]
3727
+ );
3728
+ const check = React.useCallback(
3729
+ (target, options) => {
3730
+ if (options?.live) {
3731
+ return checkLive(target);
3732
+ }
3733
+ return checkSync(target);
3734
+ },
3735
+ [checkSync, checkLive]
3736
+ );
3635
3737
  return {
3636
3738
  customer,
3637
3739
  subscription,
package/dist/index.d.cts CHANGED
@@ -570,12 +570,27 @@ interface CustomerIdentifier {
570
570
  id?: string;
571
571
  }
572
572
  /**
573
- * Entitlement status derived from subscription status
573
+ * Entitlement status
574
+ *
575
+ * - active: Subscription is active
576
+ * - trialing: Subscription is in trial period
577
+ * - past_due: Subscription payment is past due but still has access
578
+ * - canceled: Subscription canceled but not yet expired
579
+ * - purchased: ONE_TIME product has been paid for
580
+ */
581
+ type EntitlementStatus = 'active' | 'trialing' | 'past_due' | 'canceled' | 'purchased';
582
+ /**
583
+ * Source of the entitlement
584
+ *
585
+ * - subscription: From an active subscription
586
+ * - order: From a paid ONE_TIME order
574
587
  */
575
- type EntitlementStatus = 'active' | 'trialing' | 'past_due' | 'canceled';
588
+ type EntitlementSource = 'subscription' | 'order';
576
589
  /**
577
590
  * An entitlement represents a customer's access to a product
578
- * Computed from active subscriptions
591
+ *
592
+ * Computed from active subscriptions and paid ONE_TIME orders.
593
+ * Use the `source` field to determine if access comes from a subscription or purchase.
579
594
  */
580
595
  interface Entitlement {
581
596
  /**
@@ -593,13 +608,62 @@ interface Entitlement {
593
608
  */
594
609
  status: EntitlementStatus;
595
610
  /**
596
- * Associated subscription ID
597
- * @example 'sub_xyz789'
611
+ * Source of the entitlement (subscription or order)
612
+ */
613
+ source: EntitlementSource;
614
+ /**
615
+ * ID of the source (subscriptionId or orderId)
616
+ * @example 'sub_xyz789' or 'ord_abc123'
617
+ */
618
+ sourceId: string;
619
+ /**
620
+ * When access was granted (ISO 8601)
621
+ * @example '2024-01-15T10:30:00.000Z'
622
+ */
623
+ grantedAt: string;
624
+ /**
625
+ * When access expires (ISO 8601), null means permanent access
626
+ * For subscriptions: currentPeriodEnd
627
+ * For ONE_TIME purchases: null (permanent)
628
+ * @example '2024-02-15T10:30:00.000Z' or null
629
+ */
630
+ expiresAt: string | null;
631
+ /**
632
+ * @deprecated Use sourceId instead. Kept for backward compatibility.
633
+ * Associated subscription ID (only present for subscription entitlements)
598
634
  */
599
- subscriptionId: string;
635
+ subscriptionId?: string;
600
636
  }
637
+ /**
638
+ * Target for the check() method
639
+ *
640
+ * Can be a string (product slug) or an object with product/feature/benefit
641
+ *
642
+ * @example
643
+ * // String shorthand
644
+ * check("pro-plan")
645
+ *
646
+ * // Object form
647
+ * check({ product: "pro-plan" })
648
+ *
649
+ * // Future: Feature-based check (Phase 2)
650
+ * check({ feature: "ai-chat", required: 10 })
651
+ *
652
+ * // Future: Benefit check (Phase 2)
653
+ * check({ benefit: "priority-support" })
654
+ */
655
+ type CheckTarget = string | {
656
+ product: string;
657
+ } | {
658
+ feature: string;
659
+ required?: number;
660
+ } | {
661
+ benefit: string;
662
+ };
601
663
  /**
602
664
  * Options for the check() method
665
+ *
666
+ * @deprecated Use CheckTarget and CheckLiveOptions instead
603
667
  */
604
668
  interface CheckOptions {
605
669
  /**
@@ -608,10 +672,22 @@ interface CheckOptions {
608
672
  */
609
673
  product: string;
610
674
  }
675
+ /**
676
+ * Options for check() second parameter
677
+ */
678
+ interface CheckLiveOptions {
679
+ /**
680
+ * If true, fetches fresh data from the API instead of using cache
681
+ * Use this when you need real-time accuracy (e.g., after checkout)
682
+ *
683
+ * @default false
684
+ */
685
+ live?: boolean;
686
+ }
611
687
  /**
612
688
  * Reason why access was denied
613
689
  */
614
- type CheckDeniedReason = 'no_customer' | 'no_subscription' | 'expired' | 'wrong_product';
690
+ type CheckDeniedReason = 'no_customer' | 'no_entitlement' | 'not_found' | 'expired' | 'insufficient_balance';
615
691
  /**
616
692
  * Result of the check() method
617
693
  */
@@ -625,7 +701,23 @@ interface CheckResult {
625
701
  */
626
702
  reason?: CheckDeniedReason;
627
703
  /**
628
- * Associated subscription (only present when allowed is true)
704
+ * The entitlement granting access (only present when allowed is true)
705
+ * Contains source info (subscription or order) and expiration details
706
+ */
707
+ entitlement?: Entitlement;
708
+ /**
709
+ * Current balance (for credit/usage-based entitlements)
710
+ * @example 42 (remaining credits)
711
+ */
712
+ balance?: number;
713
+ /**
714
+ * Whether the entitlement has unlimited access
715
+ * @example true (for unlimited plans)
716
+ */
717
+ unlimited?: boolean;
718
+ /**
719
+ * @deprecated Use entitlement instead. Kept for backward compatibility.
720
+ * Associated subscription (only present when allowed is true and source is subscription)
629
721
  */
630
722
  subscription?: EntitlementSubscription;
631
723
  }
@@ -693,13 +785,38 @@ interface UseCustomerResult {
693
785
  entitlements: Entitlement[];
694
786
  /**
695
787
  * Check if customer has access to a specific product
696
- * Synchronous - reads from local cache
788
+ *
789
+ * Synchronous by default - reads from local cache (zero latency).
790
+ * Use `{ live: true }` option for real-time API check.
791
+ *
792
+ * @param target - Product slug string or check target object
793
+ * @param options - Optional settings like `{ live: true }`
794
+ * @returns CheckResult with allowed status and entitlement if granted
697
795
  *
698
796
  * @example
699
- * const { allowed } = check({ product: 'pro-plan' });
700
- * if (!allowed) return <UpgradePrompt />;
797
+ * // String shorthand (recommended)
798
+ * const { allowed } = check("pro-plan");
799
+ *
800
+ * // Object form
801
+ * const { allowed } = check({ product: "pro-plan" });
802
+ *
803
+ * // Real-time check (async)
804
+ * const { allowed } = await check("pro-plan", { live: true });
805
+ *
806
+ * // Access entitlement details
807
+ * const { allowed, entitlement } = check("pro-plan");
808
+ * if (allowed && entitlement?.expiresAt) {
809
+ * console.log(`Access until ${entitlement.expiresAt}`);
810
+ * }
701
811
  */
702
- check: (options: CheckOptions) => CheckResult;
812
+ check: {
813
+ (target: CheckTarget, options?: CheckLiveOptions & {
814
+ live?: false;
815
+ }): CheckResult;
816
+ (target: CheckTarget, options: CheckLiveOptions & {
817
+ live: true;
818
+ }): Promise<CheckResult>;
819
+ };
703
820
  /**
704
821
  * Manually refresh entitlements from the API
705
822
  * Call this after checkout to update permissions
@@ -730,6 +847,7 @@ declare class RecurPaymentForm extends HTMLElement {
730
847
  private customStyles;
731
848
  private _isInitializing;
732
849
  private _initializationAborted;
850
+ private _showTestCardHints;
733
851
  private _showCouponInput;
734
852
  private _couponCode;
735
853
  private _couponLoading;
@@ -747,6 +865,8 @@ declare class RecurPaymentForm extends HTMLElement {
747
865
  private renderLightDOM;
748
866
  private getCustomStyles;
749
867
  private updateCustomStyles;
868
+ private isTestMode;
869
+ private renderTestCardHints;
750
870
  private renderOrderSummary;
751
871
  private getIntervalText;
752
872
  private getDiscountDescription;
@@ -1135,6 +1255,8 @@ declare function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult
1135
1255
  * your own database or webhook integration. The hook fetches entitlements
1136
1256
  * from Recur on mount and provides synchronous access via the check() method.
1137
1257
  *
1258
+ * Supports both SUBSCRIPTION and ONE_TIME product types.
1259
+ *
1138
1260
  * Must be used within a RecurProvider with a customer identifier:
1139
1261
  * ```tsx
1140
1262
  * <RecurProvider
@@ -1153,7 +1275,8 @@ declare function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult
1153
1275
  *
1154
1276
  * if (isLoading) return <Spinner />;
1155
1277
  *
1156
- * const { allowed } = check({ product: 'pro-plan' });
1278
+ * // String shorthand (recommended)
1279
+ * const { allowed } = check("pro-plan");
1157
1280
  *
1158
1281
  * if (!allowed) {
1159
1282
  * return <UpgradePrompt />;
@@ -1163,6 +1286,43 @@ declare function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult
1163
1286
  * }
1164
1287
  * ```
1165
1288
  *
1289
+ * ## Check with Entitlement Details
1290
+ * @example
1291
+ * ```tsx
1292
+ * function SubscriptionInfo() {
1293
+ * const { check } = useCustomer();
1294
+ *
1295
+ * const { allowed, entitlement } = check("pro-plan");
1296
+ *
1297
+ * if (!allowed) return <UpgradePrompt />;
1298
+ *
1299
+ * return (
1300
+ * <div>
1301
+ * <p>Status: {entitlement?.status}</p>
1302
+ * <p>Source: {entitlement?.source}</p>
1303
+ * {entitlement?.expiresAt && (
1304
+ * <p>Expires: {new Date(entitlement.expiresAt).toLocaleDateString()}</p>
1305
+ * )}
1306
+ * </div>
1307
+ * );
1308
+ * }
1309
+ * ```
1310
+ *
1311
+ * ## Real-time Check (After Payment)
1312
+ * @example
1313
+ * ```tsx
1314
+ * async function verifyAccess() {
1315
+ * const { check } = useCustomer();
1316
+ *
1317
+ * // Force fresh check from API
1318
+ * const { allowed } = await check("pro-plan", { live: true });
1319
+ *
1320
+ * if (allowed) {
1321
+ * console.log("Access confirmed!");
1322
+ * }
1323
+ * }
1324
+ * ```
1325
+ *
1166
1326
  * ## Display Subscription Status
1167
1327
  * @example
1168
1328
  * ```tsx
@@ -1225,8 +1385,8 @@ declare function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult
1225
1385
  * @returns {UseCustomerResult} Object containing:
1226
1386
  * - `customer` - Customer info (null if not found)
1227
1387
  * - `subscription` - Most recent active subscription (null if none)
1228
- * - `entitlements` - Array of all active entitlements
1229
- * - `check(options)` - Synchronous permission check
1388
+ * - `entitlements` - Array of all active entitlements (from subscriptions and purchases)
1389
+ * - `check(target, options?)` - Permission check (sync or async with `{ live: true }`)
1230
1390
  * - `refetch()` - Manual refresh (debounced)
1231
1391
  * - `isLoading` - Whether data is being fetched
1232
1392
  * - `error` - Error from last fetch attempt