recur-tw 0.21.1 → 0.21.3
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/AGENTS.md +35 -24
- package/dist/index.cjs +99 -167
- package/dist/index.d.cts +24 -18
- package/dist/index.d.ts +24 -18
- package/dist/index.js +99 -167
- package/dist/recur.umd.js +89 -148
- package/dist/server.cjs +10 -9
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +10 -9
- package/dist/vanilla.d.ts +16 -10
- package/dist/vanilla.js +93 -152
- package/package.json +3 -1
package/AGENTS.md
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
recur-tw
|
|
9
9
|
├── index.js → React SDK (needs React 18+)
|
|
10
10
|
├── server.js → Server SDK (Node.js only, uses secret key)
|
|
11
|
-
|
|
11
|
+
├── vanilla.js → Vanilla JS, ESM (`recur-tw/vanilla`, for bundlers)
|
|
12
|
+
└── recur.umd.js → Vanilla JS, UMD (script tag / CDN, global `RecurCheckout`)
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
## Which Module to Use?
|
|
@@ -17,28 +18,30 @@ recur-tw
|
|
|
17
18
|
|-------------|--------|-------------|
|
|
18
19
|
| React/Next.js | `import { RecurProvider, useRecur } from 'recur-tw'` | `dist/index.js` |
|
|
19
20
|
| Node.js Server | `import { Recur } from 'recur-tw/server'` | `dist/server.js` |
|
|
21
|
+
| Vanilla JS (bundler) | `import { init } from 'recur-tw/vanilla'` | `dist/vanilla.js` (ESM, typed) |
|
|
20
22
|
| Vanilla JS (CDN) | `<script src="https://unpkg.com/recur-tw/dist/recur.umd.js">` | UMD global `RecurCheckout` |
|
|
21
|
-
| Vanilla JS (bundler) | NOT SUPPORTED as ESM - use CDN script tag | - |
|
|
22
23
|
|
|
23
|
-
##
|
|
24
|
+
## Vanilla JS Usage
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
With a bundler (Vite, webpack, Next.js):
|
|
27
|
+
```javascript
|
|
28
|
+
import { init } from 'recur-tw/vanilla';
|
|
29
|
+
|
|
30
|
+
const recur = init({ publishableKey: 'pk_xxx' });
|
|
31
|
+
recur.redirectToCheckout({ productId: 'prod_xxx', successUrl: `${location.origin}/success`, cancelUrl: `${location.origin}/cancel` });
|
|
32
|
+
```
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
Without a bundler, load the UMD build. It exposes the same API on `window.RecurCheckout`;
|
|
35
|
+
don't `import` the `.umd.js` file itself, it has no module exports. `successUrl` / `cancelUrl`
|
|
36
|
+
must be absolute URLs (HTTPS outside local development); relative paths are rejected:
|
|
28
37
|
```html
|
|
29
38
|
<script src="https://unpkg.com/recur-tw/dist/recur.umd.js"></script>
|
|
30
39
|
<script>
|
|
31
40
|
const recur = RecurCheckout.init({ publishableKey: 'pk_xxx' });
|
|
32
|
-
recur.redirectToCheckout({ productId: 'prod_xxx', successUrl:
|
|
41
|
+
recur.redirectToCheckout({ productId: 'prod_xxx', successUrl: `${location.origin}/success`, cancelUrl: `${location.origin}/cancel` });
|
|
33
42
|
</script>
|
|
34
43
|
```
|
|
35
44
|
|
|
36
|
-
**WRONG** - Do NOT try to import as ESM:
|
|
37
|
-
```javascript
|
|
38
|
-
// This will NOT work!
|
|
39
|
-
import { init } from 'recur-tw/vanilla';
|
|
40
|
-
```
|
|
41
|
-
|
|
42
45
|
## React Quick Start
|
|
43
46
|
|
|
44
47
|
```tsx
|
|
@@ -54,15 +57,19 @@ function App() {
|
|
|
54
57
|
);
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
// 2. Use hooks
|
|
60
|
+
// 2. Use hooks. Hosted Checkout (redirect) needs no extra provider config;
|
|
61
|
+
// checkout() opens modal/embedded and needs `checkoutMode` on the provider
|
|
62
|
+
// (the default, 'embedded', also needs `containerElementId`).
|
|
58
63
|
function CheckoutPage() {
|
|
59
64
|
const { data: products } = useProducts();
|
|
60
|
-
const {
|
|
65
|
+
const { redirectToCheckout } = useRecur();
|
|
66
|
+
if (!products?.length) return null;
|
|
61
67
|
|
|
62
68
|
return (
|
|
63
|
-
<button onClick={() =>
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
<button onClick={() => redirectToCheckout({
|
|
70
|
+
productId: products[0].id,
|
|
71
|
+
successUrl: `${window.location.origin}/success`,
|
|
72
|
+
cancelUrl: `${window.location.origin}/pricing`,
|
|
66
73
|
})}>
|
|
67
74
|
Subscribe
|
|
68
75
|
</button>
|
|
@@ -75,7 +82,7 @@ function CheckoutPage() {
|
|
|
75
82
|
```typescript
|
|
76
83
|
import { Recur } from 'recur-tw/server';
|
|
77
84
|
|
|
78
|
-
const recur = new Recur(
|
|
85
|
+
const recur = new Recur('sk_xxx');
|
|
79
86
|
|
|
80
87
|
// Create checkout session
|
|
81
88
|
const session = await recur.checkoutSessions.create({
|
|
@@ -91,18 +98,22 @@ console.log(session.url); // Redirect user here
|
|
|
91
98
|
|
|
92
99
|
### Main Module (`recur-tw`)
|
|
93
100
|
- `RecurProvider` - React context provider
|
|
94
|
-
- `useRecur()` - Checkout hook (
|
|
101
|
+
- `useRecur()` - Checkout hook: `redirectToCheckout` (hosted), `checkout` / `isCheckingOut` (modal/embedded)
|
|
102
|
+
- `useSubscribe()` - Modal/embedded checkout with `onPaymentComplete` / `onPaymentFailed` callbacks
|
|
95
103
|
- `useProducts()` - Fetch products hook
|
|
104
|
+
- `useCustomer()` - Entitlements: `check()`, `refetch()`
|
|
96
105
|
- Types: `RecurConfig`, `CheckoutOptions`, `Product`, etc.
|
|
97
106
|
|
|
98
107
|
### Server Module (`recur-tw/server`)
|
|
99
108
|
- `Recur` - Server SDK class
|
|
100
109
|
- `recur.checkoutSessions.create()` - Create checkout session
|
|
101
110
|
- `recur.checkoutSessions.retrieve()` - Get session details
|
|
102
|
-
- `recur.portal.
|
|
111
|
+
- `recur.portal.sessions.create()` - Create customer portal session
|
|
112
|
+
- `recur.webhooks.verify(payload, signature, secret)` - Verify a webhook signature, returns the event
|
|
113
|
+
- `recur.entitlements.check({ product, customer })` - Server-side access check
|
|
103
114
|
|
|
104
|
-
### Vanilla Module (UMD global `RecurCheckout`)
|
|
105
|
-
- `RecurCheckout.init(config)` - Initialize SDK
|
|
115
|
+
### Vanilla Module (`import { init } from 'recur-tw/vanilla'`, or UMD global `RecurCheckout`)
|
|
116
|
+
- `init(config)` / `RecurCheckout.init(config)` - Initialize SDK
|
|
106
117
|
- `recur.checkout(options)` - Embedded checkout
|
|
107
118
|
- `recur.redirectToCheckout(options)` - Hosted checkout redirect
|
|
108
119
|
- `recur.createCheckoutSession(options)` - Get session URL
|
|
@@ -113,12 +124,12 @@ console.log(session.url); // Redirect user here
|
|
|
113
124
|
|---------|-----------|------------|------------|
|
|
114
125
|
| Environment | Browser | Node.js | Browser |
|
|
115
126
|
| Auth Key | Publishable (`pk_*`) | Secret (`sk_*`) | Publishable (`pk_*`) |
|
|
116
|
-
| Import Style | ESM | ESM |
|
|
127
|
+
| Import Style | ESM | ESM | ESM (`recur-tw/vanilla`) or script tag (UMD) |
|
|
117
128
|
| React Required | Yes | No | No |
|
|
118
129
|
|
|
119
130
|
## Common Mistakes
|
|
120
131
|
|
|
121
|
-
1. **
|
|
132
|
+
1. **Importing `dist/recur.umd.js` from a bundler** - It has no module exports; import `recur-tw/vanilla` instead
|
|
122
133
|
2. **Using secret key in browser** - Only use `pk_*` keys client-side
|
|
123
134
|
3. **Missing RecurProvider** - React hooks need the provider wrapper
|
|
124
135
|
4. **Wrong import path** - Server SDK is `recur-tw/server`, not `recur-tw`
|
package/dist/index.cjs
CHANGED
|
@@ -1471,6 +1471,39 @@ var init_einvoice_section = __esmMin((() => {
|
|
|
1471
1471
|
if (typeof window !== "undefined" && !customElements.get("recur-einvoice-section")) customElements.define("recur-einvoice-section", RecurEinvoiceSection);
|
|
1472
1472
|
}));
|
|
1473
1473
|
|
|
1474
|
+
//#endregion
|
|
1475
|
+
//#region src/components/css-ident.ts
|
|
1476
|
+
/**
|
|
1477
|
+
* Escape a value for use as a CSS identifier.
|
|
1478
|
+
*
|
|
1479
|
+
* `container-id` comes from the host's own container, and ids like
|
|
1480
|
+
* `checkout:main` are perfectly legal HTML. Dropped into a selector unescaped
|
|
1481
|
+
* the colon parses as a pseudo-class, so `#checkout:main-card-no { … }` is not
|
|
1482
|
+
* a rule that fails loudly — it is a rule that silently never matches, which is
|
|
1483
|
+
* how a `height` the card fields depend on can go missing on one integration
|
|
1484
|
+
* and nowhere else.
|
|
1485
|
+
*
|
|
1486
|
+
* Prefers the platform's own `CSS.escape`; the fallback covers the identifier
|
|
1487
|
+
* cases that matter here (jsdom has no CSS object at all).
|
|
1488
|
+
*/
|
|
1489
|
+
function cssIdent(value) {
|
|
1490
|
+
const native = globalThis.CSS?.escape;
|
|
1491
|
+
if (typeof native === "function") return native(value);
|
|
1492
|
+
let escaped = "";
|
|
1493
|
+
for (const character of value) {
|
|
1494
|
+
if (/[\w-]/.test(character)) {
|
|
1495
|
+
escaped += character;
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
const code = character.codePointAt(0);
|
|
1499
|
+
escaped += code > 127 || code < 32 || code === 127 ? `\\${code.toString(16)} ` : `\\${character}`;
|
|
1500
|
+
}
|
|
1501
|
+
if (/^\d/.test(escaped)) escaped = `\\3${escaped[0]} ${escaped.slice(1)}`;
|
|
1502
|
+
else if (/^-\d/.test(escaped)) escaped = `-\\3${escaped[1]} ${escaped.slice(2)}`;
|
|
1503
|
+
return escaped;
|
|
1504
|
+
}
|
|
1505
|
+
var init_css_ident = __esmMin((() => {}));
|
|
1506
|
+
|
|
1474
1507
|
//#endregion
|
|
1475
1508
|
//#region src/components/styles/payment-form-shadow.css?css-text
|
|
1476
1509
|
var payment_form_shadow_default;
|
|
@@ -1482,7 +1515,7 @@ var init_payment_form_shadow = __esmMin((() => {
|
|
|
1482
1515
|
//#region src/components/styles/payment-form-light.css?css-text
|
|
1483
1516
|
var payment_form_light_default;
|
|
1484
1517
|
var init_payment_form_light = __esmMin((() => {
|
|
1485
|
-
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";
|
|
1518
|
+
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 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: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";
|
|
1486
1519
|
}));
|
|
1487
1520
|
|
|
1488
1521
|
//#endregion
|
|
@@ -1598,15 +1631,10 @@ var init_checkout_display = __esmMin((() => {}));
|
|
|
1598
1631
|
|
|
1599
1632
|
//#endregion
|
|
1600
1633
|
//#region src/components/payment-form.ts
|
|
1601
|
-
/**
|
|
1602
|
-
* Recur Payment Form Web Component (混合模式)
|
|
1603
|
-
*
|
|
1604
|
-
* 使用 Shadow DOM 包裝外層 UI,PAYUNi 容器保持在 Light DOM
|
|
1605
|
-
* 這樣既能享受樣式隔離,又能讓 PAYUNi SDK 正常工作
|
|
1606
|
-
*/
|
|
1607
1634
|
var payment_form_exports = /* @__PURE__ */ __exportAll({ RecurPaymentForm: () => RecurPaymentForm });
|
|
1608
1635
|
var icons, RecurPaymentForm;
|
|
1609
1636
|
var init_payment_form = __esmMin((() => {
|
|
1637
|
+
init_css_ident();
|
|
1610
1638
|
init_payment_form_shadow();
|
|
1611
1639
|
init_payment_form_light();
|
|
1612
1640
|
init_checkout_display();
|
|
@@ -1652,6 +1680,7 @@ var init_payment_form = __esmMin((() => {
|
|
|
1652
1680
|
_defineProperty(this, "_couponCode", "");
|
|
1653
1681
|
_defineProperty(this, "_couponLoading", false);
|
|
1654
1682
|
_defineProperty(this, "_couponError", null);
|
|
1683
|
+
_defineProperty(this, "_submitState", "idle");
|
|
1655
1684
|
_defineProperty(this, "_appliedCoupon", null);
|
|
1656
1685
|
this.containerId = this.getAttribute("container-id") || `recur-${Date.now()}`;
|
|
1657
1686
|
this.customStyles = this.getAttribute("custom-styles") || "";
|
|
@@ -1692,7 +1721,7 @@ var init_payment_form = __esmMin((() => {
|
|
|
1692
1721
|
this.updateCustomStyles();
|
|
1693
1722
|
} else if (name === "einvoice-enabled" || name === "einvoice-defaults") {
|
|
1694
1723
|
if (this.isConnected) this.render();
|
|
1695
|
-
} else this.
|
|
1724
|
+
} else this.renderLightDOM();
|
|
1696
1725
|
}
|
|
1697
1726
|
isEinvoiceEnabled() {
|
|
1698
1727
|
return this.getAttribute("einvoice-enabled") === "true";
|
|
@@ -1750,11 +1779,16 @@ var init_payment_form = __esmMin((() => {
|
|
|
1750
1779
|
`, this.shadowRoot);
|
|
1751
1780
|
}
|
|
1752
1781
|
renderLightDOM() {
|
|
1782
|
+
if (!this.isConnected) return;
|
|
1753
1783
|
(0, lit_html.render)(lit_html.html`
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1784
|
+
<!-- Deliberately empty: the stylesheet is written imperatively below.
|
|
1785
|
+
Interpolating it here would make the element's content a lit
|
|
1786
|
+
ChildPart, and updateCustomStyles() writes to that element whenever
|
|
1787
|
+
the host changes custom-styles — which would delete the markers lit
|
|
1788
|
+
renders through and make the next render throw. With no expression
|
|
1789
|
+
inside, lit owns the element but not its content, and the write is
|
|
1790
|
+
harmless. -->
|
|
1791
|
+
<style id="${this.containerId}-styles"></style>
|
|
1758
1792
|
|
|
1759
1793
|
<div slot="test-mode-banner">
|
|
1760
1794
|
${this.renderTestCardHints()}
|
|
@@ -1785,21 +1819,33 @@ var init_payment_form = __esmMin((() => {
|
|
|
1785
1819
|
${this.renderActions()}
|
|
1786
1820
|
</div>
|
|
1787
1821
|
`, this);
|
|
1822
|
+
this.applyStyles();
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Fill the stylesheet lit deliberately leaves empty.
|
|
1826
|
+
*
|
|
1827
|
+
* Safe precisely because the template interpolates nothing into that
|
|
1828
|
+
* element: there are no marker nodes inside it to eject.
|
|
1829
|
+
*/
|
|
1830
|
+
applyStyles() {
|
|
1831
|
+
const styleId = `${this.containerId}-styles`;
|
|
1832
|
+
const styleElement = [...this.children].find((child) => child instanceof HTMLStyleElement && child.id === styleId);
|
|
1833
|
+
if (styleElement) styleElement.textContent = this.getLightDOMStyles() + this.getCustomStyles();
|
|
1788
1834
|
}
|
|
1789
1835
|
getCustomStyles() {
|
|
1836
|
+
const field = (suffix) => `#${cssIdent(`${this.containerId}-${suffix}`)}`;
|
|
1790
1837
|
return `
|
|
1791
1838
|
/* PAYUNi iframe container height */
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1839
|
+
${field("card-no")},
|
|
1840
|
+
${field("card-exp")},
|
|
1841
|
+
${field("card-cvc")} {
|
|
1795
1842
|
height: 36px !important;
|
|
1796
1843
|
}
|
|
1797
1844
|
${this.customStyles}
|
|
1798
1845
|
`;
|
|
1799
1846
|
}
|
|
1800
1847
|
updateCustomStyles() {
|
|
1801
|
-
|
|
1802
|
-
if (styleElement) styleElement.textContent = this.getLightDOMStyles() + this.getCustomStyles();
|
|
1848
|
+
this.applyStyles();
|
|
1803
1849
|
}
|
|
1804
1850
|
isTestMode() {
|
|
1805
1851
|
return (this.getAttribute("publishable-key") || "").startsWith("pk_test_");
|
|
@@ -2032,7 +2078,7 @@ var init_payment_form = __esmMin((() => {
|
|
|
2032
2078
|
showCouponInput() {
|
|
2033
2079
|
this._showCouponInput = true;
|
|
2034
2080
|
this._couponError = null;
|
|
2035
|
-
this.
|
|
2081
|
+
this.renderLightDOM();
|
|
2036
2082
|
requestAnimationFrame(() => {
|
|
2037
2083
|
document.getElementById(`${this.containerId}-coupon-input`)?.focus();
|
|
2038
2084
|
});
|
|
@@ -2041,18 +2087,18 @@ var init_payment_form = __esmMin((() => {
|
|
|
2041
2087
|
this._showCouponInput = false;
|
|
2042
2088
|
this._couponCode = "";
|
|
2043
2089
|
this._couponError = null;
|
|
2044
|
-
this.
|
|
2090
|
+
this.renderLightDOM();
|
|
2045
2091
|
}
|
|
2046
2092
|
clearCouponInput() {
|
|
2047
2093
|
this._couponCode = "";
|
|
2048
2094
|
this._couponError = null;
|
|
2049
|
-
this.
|
|
2095
|
+
this.renderLightDOM();
|
|
2050
2096
|
requestAnimationFrame(() => {
|
|
2051
2097
|
document.getElementById(`${this.containerId}-coupon-input`)?.focus();
|
|
2052
2098
|
});
|
|
2053
2099
|
}
|
|
2054
2100
|
handleCouponInputChange(e) {
|
|
2055
|
-
this._couponCode = e.target.value
|
|
2101
|
+
this._couponCode = e.target.value;
|
|
2056
2102
|
this._couponError = null;
|
|
2057
2103
|
this.updateApplyButtonState();
|
|
2058
2104
|
}
|
|
@@ -2066,131 +2112,23 @@ var init_payment_form = __esmMin((() => {
|
|
|
2066
2112
|
const btn = this.querySelector(".recur-coupon__btn--apply");
|
|
2067
2113
|
if (btn) btn.disabled = this._couponLoading || !this._couponCode.trim();
|
|
2068
2114
|
}
|
|
2069
|
-
updateCouponSectionDOM() {
|
|
2070
|
-
const containerId = `${this.containerId}-coupon`;
|
|
2071
|
-
const container = document.getElementById(containerId);
|
|
2072
|
-
if (!container) return;
|
|
2073
|
-
if (this._appliedCoupon) {
|
|
2074
|
-
container.innerHTML = this.getCouponAppliedHTML();
|
|
2075
|
-
this.bindCouponAppliedEvents(container);
|
|
2076
|
-
} else if (this._showCouponInput) {
|
|
2077
|
-
container.innerHTML = this.getCouponInputHTML();
|
|
2078
|
-
this.bindCouponInputEvents(container);
|
|
2079
|
-
} else {
|
|
2080
|
-
container.innerHTML = this.getCouponTriggerHTML();
|
|
2081
|
-
this.bindCouponTriggerEvents(container);
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
getCouponTriggerHTML() {
|
|
2085
|
-
return `
|
|
2086
|
-
<button type="button" class="recur-coupon__trigger">
|
|
2087
|
-
<svg class="recur-coupon__trigger-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2088
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
|
2089
|
-
</svg>
|
|
2090
|
-
有優惠碼?
|
|
2091
|
-
</button>
|
|
2092
|
-
`;
|
|
2093
|
-
}
|
|
2094
|
-
getCouponInputHTML() {
|
|
2095
|
-
const errorHTML = this._couponError ? `<div class="recur-coupon__error">${this._couponError}</div>` : "";
|
|
2096
|
-
const clearBtnHTML = this._couponCode && !this._couponLoading ? `<button type="button" class="recur-coupon__clear" aria-label="清除">
|
|
2097
|
-
<svg class="recur-coupon__clear-icon" width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2098
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
2099
|
-
</svg>
|
|
2100
|
-
</button>` : "";
|
|
2101
|
-
return `
|
|
2102
|
-
<div style="width: 100%;">
|
|
2103
|
-
<div class="recur-coupon__input-row">
|
|
2104
|
-
<div class="recur-coupon__input-wrapper">
|
|
2105
|
-
<input
|
|
2106
|
-
type="text"
|
|
2107
|
-
class="recur-coupon__input"
|
|
2108
|
-
id="${this.containerId}-coupon-input"
|
|
2109
|
-
placeholder="輸入優惠碼"
|
|
2110
|
-
value="${this._couponCode}"
|
|
2111
|
-
${this._couponLoading ? "disabled" : ""}
|
|
2112
|
-
/>
|
|
2113
|
-
${clearBtnHTML}
|
|
2114
|
-
</div>
|
|
2115
|
-
<button
|
|
2116
|
-
type="button"
|
|
2117
|
-
class="recur-coupon__btn recur-coupon__btn--apply"
|
|
2118
|
-
${this._couponLoading || !this._couponCode.trim() ? "disabled" : ""}
|
|
2119
|
-
>
|
|
2120
|
-
${this._couponLoading ? "<span class=\"recur-coupon__spinner\"></span>" : "套用"}
|
|
2121
|
-
</button>
|
|
2122
|
-
</div>
|
|
2123
|
-
${errorHTML}
|
|
2124
|
-
</div>
|
|
2125
|
-
`;
|
|
2126
|
-
}
|
|
2127
|
-
getCouponAppliedHTML() {
|
|
2128
|
-
const coupon = this._appliedCoupon;
|
|
2129
|
-
const nameHTML = coupon.couponName ? `<span class="recur-coupon__applied-name">${coupon.couponName}</span><span class="recur-coupon__applied-code">(${coupon.code})</span>` : `<span class="recur-coupon__applied-name">${coupon.code}</span>`;
|
|
2130
|
-
const display = this.getDiscountDisplay();
|
|
2131
|
-
const amountHTML = coupon.discountAmount > 0 ? `<span class="recur-coupon__applied-amount">-NT$ ${coupon.discountAmount.toLocaleString()}</span>` : "";
|
|
2132
|
-
const line1HTML = display?.line1 ? `<p class="recur-coupon__description">${display.line1}</p>` : "";
|
|
2133
|
-
const line2HTML = display?.line2 ? `<p class="recur-coupon__description">${display.line2}</p>` : "";
|
|
2134
|
-
return `
|
|
2135
|
-
<div class="recur-coupon__applied-wrapper">
|
|
2136
|
-
<div class="recur-coupon__applied">
|
|
2137
|
-
<div class="recur-coupon__applied-info">
|
|
2138
|
-
<svg class="recur-coupon__applied-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2139
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
|
2140
|
-
</svg>
|
|
2141
|
-
${nameHTML}
|
|
2142
|
-
<button type="button" class="recur-coupon__remove" ${this._couponLoading ? "disabled" : ""}>
|
|
2143
|
-
${this._couponLoading ? "<span class=\"recur-coupon__spinner\"></span>" : "<svg class=\"recur-coupon__remove-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" /></svg>"}
|
|
2144
|
-
</button>
|
|
2145
|
-
</div>
|
|
2146
|
-
${amountHTML}
|
|
2147
|
-
</div>
|
|
2148
|
-
${line1HTML}
|
|
2149
|
-
${line2HTML}
|
|
2150
|
-
</div>
|
|
2151
|
-
`;
|
|
2152
|
-
}
|
|
2153
|
-
bindCouponTriggerEvents(container) {
|
|
2154
|
-
container.querySelector(".recur-coupon__trigger")?.addEventListener("click", () => this.showCouponInput());
|
|
2155
|
-
}
|
|
2156
|
-
bindCouponInputEvents(container) {
|
|
2157
|
-
const input = container.querySelector(".recur-coupon__input");
|
|
2158
|
-
const applyBtn = container.querySelector(".recur-coupon__btn--apply");
|
|
2159
|
-
const clearBtn = container.querySelector(".recur-coupon__clear");
|
|
2160
|
-
input?.addEventListener("input", (e) => this.handleCouponInputChange(e));
|
|
2161
|
-
input?.addEventListener("keydown", (e) => this.handleCouponKeydown(e));
|
|
2162
|
-
applyBtn?.addEventListener("click", () => this.applyCoupon());
|
|
2163
|
-
clearBtn?.addEventListener("click", () => this.clearCouponInput());
|
|
2164
|
-
}
|
|
2165
|
-
bindCouponAppliedEvents(container) {
|
|
2166
|
-
container.querySelector(".recur-coupon__remove")?.addEventListener("click", () => this.removeCoupon());
|
|
2167
|
-
}
|
|
2168
|
-
updateOrderSummaryDOM() {
|
|
2169
|
-
const summaryContainer = document.getElementById(`${this.containerId}-order-summary`);
|
|
2170
|
-
if (!summaryContainer) return;
|
|
2171
|
-
const finalAmount = Number(this.getAttribute("amount") || 0) - (this._appliedCoupon?.discountAmount || 0);
|
|
2172
|
-
const totalValue = summaryContainer.querySelector(".recur-order-summary__row--total")?.querySelector(".recur-order-summary__total");
|
|
2173
|
-
if (totalValue) totalValue.textContent = `NT$ ${finalAmount.toLocaleString()}`;
|
|
2174
|
-
this.updateCouponSectionDOM();
|
|
2175
|
-
this.updateVerificationUI();
|
|
2176
|
-
}
|
|
2177
2115
|
async applyCoupon() {
|
|
2178
2116
|
const code = this._couponCode.trim();
|
|
2179
2117
|
if (!code) {
|
|
2180
2118
|
this._couponError = "請輸入優惠碼";
|
|
2181
|
-
this.
|
|
2119
|
+
this.renderLightDOM();
|
|
2182
2120
|
return;
|
|
2183
2121
|
}
|
|
2184
2122
|
const checkoutId = this.getAttribute("checkout-id");
|
|
2185
2123
|
const apiBaseUrl = this.getAttribute("api-base-url") || "";
|
|
2186
2124
|
if (!checkoutId) {
|
|
2187
2125
|
this._couponError = "無法套用優惠碼";
|
|
2188
|
-
this.
|
|
2126
|
+
this.renderLightDOM();
|
|
2189
2127
|
return;
|
|
2190
2128
|
}
|
|
2191
2129
|
this._couponLoading = true;
|
|
2192
2130
|
this._couponError = null;
|
|
2193
|
-
this.
|
|
2131
|
+
this.renderLightDOM();
|
|
2194
2132
|
try {
|
|
2195
2133
|
const response = await fetch(`${apiBaseUrl}/v1/checkouts/${checkoutId}/coupon`, {
|
|
2196
2134
|
method: "POST",
|
|
@@ -2201,7 +2139,7 @@ var init_payment_form = __esmMin((() => {
|
|
|
2201
2139
|
if (!response.ok) {
|
|
2202
2140
|
this._couponError = data.error?.message || "優惠碼無效";
|
|
2203
2141
|
this._couponLoading = false;
|
|
2204
|
-
this.
|
|
2142
|
+
this.renderLightDOM();
|
|
2205
2143
|
return;
|
|
2206
2144
|
}
|
|
2207
2145
|
this._appliedCoupon = {
|
|
@@ -2221,7 +2159,7 @@ var init_payment_form = __esmMin((() => {
|
|
|
2221
2159
|
this._showCouponInput = false;
|
|
2222
2160
|
this._couponCode = "";
|
|
2223
2161
|
this._couponLoading = false;
|
|
2224
|
-
this.
|
|
2162
|
+
this.renderLightDOM();
|
|
2225
2163
|
this.dispatchEvent(new CustomEvent("coupon-applied", {
|
|
2226
2164
|
detail: { coupon: this._appliedCoupon },
|
|
2227
2165
|
bubbles: true,
|
|
@@ -2230,7 +2168,7 @@ var init_payment_form = __esmMin((() => {
|
|
|
2230
2168
|
} catch {
|
|
2231
2169
|
this._couponError = "網路錯誤,請稍後再試";
|
|
2232
2170
|
this._couponLoading = false;
|
|
2233
|
-
this.
|
|
2171
|
+
this.renderLightDOM();
|
|
2234
2172
|
}
|
|
2235
2173
|
}
|
|
2236
2174
|
async removeCoupon() {
|
|
@@ -2238,20 +2176,20 @@ var init_payment_form = __esmMin((() => {
|
|
|
2238
2176
|
const apiBaseUrl = this.getAttribute("api-base-url") || "";
|
|
2239
2177
|
if (!checkoutId) return;
|
|
2240
2178
|
this._couponLoading = true;
|
|
2241
|
-
this.
|
|
2179
|
+
this.renderLightDOM();
|
|
2242
2180
|
try {
|
|
2243
2181
|
const response = await fetch(`${apiBaseUrl}/v1/checkouts/${checkoutId}/coupon`, { method: "DELETE" });
|
|
2244
2182
|
if (!response.ok) {
|
|
2245
2183
|
const data = await response.json();
|
|
2246
2184
|
this._couponError = data.error?.message || "移除失敗";
|
|
2247
2185
|
this._couponLoading = false;
|
|
2248
|
-
this.
|
|
2186
|
+
this.renderLightDOM();
|
|
2249
2187
|
return;
|
|
2250
2188
|
}
|
|
2251
2189
|
const removedCoupon = this._appliedCoupon;
|
|
2252
2190
|
this._appliedCoupon = null;
|
|
2253
2191
|
this._couponLoading = false;
|
|
2254
|
-
this.
|
|
2192
|
+
this.renderLightDOM();
|
|
2255
2193
|
this.dispatchEvent(new CustomEvent("coupon-removed", {
|
|
2256
2194
|
detail: { coupon: removedCoupon },
|
|
2257
2195
|
bubbles: true,
|
|
@@ -2260,7 +2198,7 @@ var init_payment_form = __esmMin((() => {
|
|
|
2260
2198
|
} catch {
|
|
2261
2199
|
this._couponError = "網路錯誤,請稍後再試";
|
|
2262
2200
|
this._couponLoading = false;
|
|
2263
|
-
this.
|
|
2201
|
+
this.renderLightDOM();
|
|
2264
2202
|
}
|
|
2265
2203
|
}
|
|
2266
2204
|
/**
|
|
@@ -2330,13 +2268,6 @@ var init_payment_form = __esmMin((() => {
|
|
|
2330
2268
|
getSubmitLabel() {
|
|
2331
2269
|
return this.isCardVerificationMode() ? "驗證卡片" : "確認付款";
|
|
2332
2270
|
}
|
|
2333
|
-
/** Sync the verification notice + submit label with the current amount. */
|
|
2334
|
-
updateVerificationUI() {
|
|
2335
|
-
const notice = document.getElementById(`${this.containerId}-verification-notice`);
|
|
2336
|
-
if (notice) notice.classList.toggle("hidden", !this.isCardVerificationMode());
|
|
2337
|
-
const submitText = document.getElementById(`${this.containerId}-submit-text`);
|
|
2338
|
-
if (submitText) submitText.textContent = this.getSubmitLabel();
|
|
2339
|
-
}
|
|
2340
2271
|
renderCardFields() {
|
|
2341
2272
|
return lit_html.html`
|
|
2342
2273
|
<div
|
|
@@ -2391,10 +2322,22 @@ var init_payment_form = __esmMin((() => {
|
|
|
2391
2322
|
class="recur-submit-button"
|
|
2392
2323
|
disabled
|
|
2393
2324
|
>
|
|
2394
|
-
|
|
2325
|
+
${this.renderSubmitContent()}
|
|
2395
2326
|
</button>
|
|
2396
2327
|
`;
|
|
2397
2328
|
}
|
|
2329
|
+
/**
|
|
2330
|
+
* The submit button's content belongs to lit like everything else in the light
|
|
2331
|
+
* DOM: writing it by hand detaches the label lit renders into, and a later
|
|
2332
|
+
* full render (test-card banner, einvoice change) then either throws or keeps
|
|
2333
|
+
* updating a node nobody can see. The button's `disabled` and `loading` class
|
|
2334
|
+
* stay imperative — the template sets them statically, so lit never rewrites them.
|
|
2335
|
+
*/
|
|
2336
|
+
renderSubmitContent() {
|
|
2337
|
+
if (this._submitState === "processing") return lit_html.html`<span class="recur-loading-spinner"></span><span>處理中...</span>`;
|
|
2338
|
+
if (this._submitState === "verifying") return lit_html.html`<span class="recur-loading-spinner"></span><span>3D 驗證中...</span>`;
|
|
2339
|
+
return lit_html.html`<span id="${this.containerId}-submit-text">${this.getSubmitLabel()}</span>`;
|
|
2340
|
+
}
|
|
2398
2341
|
async initializePayment(sdkToken, environment) {
|
|
2399
2342
|
if (this._initializationAborted) {
|
|
2400
2343
|
console.log("[PaymentForm] Initialization aborted before start");
|
|
@@ -2539,18 +2482,10 @@ var init_payment_form = __esmMin((() => {
|
|
|
2539
2482
|
setButtonLoading(loading) {
|
|
2540
2483
|
const submitBtn = document.getElementById(`${this.containerId}-submit-btn`);
|
|
2541
2484
|
if (!submitBtn) return;
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
<span class="recur-loading-spinner"></span>
|
|
2547
|
-
<span>處理中...</span>
|
|
2548
|
-
`;
|
|
2549
|
-
} else {
|
|
2550
|
-
submitBtn.classList.remove("loading");
|
|
2551
|
-
submitBtn.disabled = false;
|
|
2552
|
-
submitBtn.innerHTML = "<span id=\"" + this.containerId + "-submit-text\">" + this.getSubmitLabel() + "</span>";
|
|
2553
|
-
}
|
|
2485
|
+
this._submitState = loading ? "processing" : "idle";
|
|
2486
|
+
submitBtn.classList.toggle("loading", loading);
|
|
2487
|
+
submitBtn.disabled = loading;
|
|
2488
|
+
this.renderLightDOM();
|
|
2554
2489
|
}
|
|
2555
2490
|
resetButton() {
|
|
2556
2491
|
this.setButtonLoading(false);
|
|
@@ -2559,12 +2494,10 @@ var init_payment_form = __esmMin((() => {
|
|
|
2559
2494
|
const submitBtn = document.getElementById(`${this.containerId}-submit-btn`);
|
|
2560
2495
|
if (!submitBtn) return;
|
|
2561
2496
|
if (verifying) {
|
|
2497
|
+
this._submitState = "verifying";
|
|
2562
2498
|
submitBtn.classList.add("loading");
|
|
2563
2499
|
submitBtn.disabled = true;
|
|
2564
|
-
|
|
2565
|
-
<span class="recur-loading-spinner"></span>
|
|
2566
|
-
<span>3D 驗證中...</span>
|
|
2567
|
-
`;
|
|
2500
|
+
this.renderLightDOM();
|
|
2568
2501
|
} else this.setButtonLoading(false);
|
|
2569
2502
|
}
|
|
2570
2503
|
/**
|
|
@@ -3249,7 +3182,7 @@ function toCamelCase(obj) {
|
|
|
3249
3182
|
|
|
3250
3183
|
//#endregion
|
|
3251
3184
|
//#region package.json
|
|
3252
|
-
var version = "0.21.
|
|
3185
|
+
var version = "0.21.3";
|
|
3253
3186
|
|
|
3254
3187
|
//#endregion
|
|
3255
3188
|
//#region src/context.tsx
|
|
@@ -4406,7 +4339,7 @@ function usePromoCode(options = {}) {
|
|
|
4406
4339
|
if (controller.signal.aborted) return;
|
|
4407
4340
|
if (result.valid) {
|
|
4408
4341
|
setStatus("valid");
|
|
4409
|
-
setCode(trimmed
|
|
4342
|
+
setCode(result.promotionCode?.code || trimmed);
|
|
4410
4343
|
setDiscount(result.discount || null);
|
|
4411
4344
|
setAppliesToAllProducts(result.appliesToAllProducts ?? false);
|
|
4412
4345
|
setApplicableProducts(result.applicableProducts ?? []);
|
|
@@ -4469,7 +4402,6 @@ const defaultStyles = {
|
|
|
4469
4402
|
fontSize: "14px",
|
|
4470
4403
|
fontFamily: "inherit",
|
|
4471
4404
|
outline: "none",
|
|
4472
|
-
textTransform: "uppercase",
|
|
4473
4405
|
letterSpacing: "0.05em"
|
|
4474
4406
|
},
|
|
4475
4407
|
button: {
|
|
@@ -4514,7 +4446,7 @@ const defaultStyles = {
|
|
|
4514
4446
|
* promo={promo}
|
|
4515
4447
|
* unstyled
|
|
4516
4448
|
* className="flex flex-col gap-1"
|
|
4517
|
-
* inputClassName="border rounded px-3 py-2
|
|
4449
|
+
* inputClassName="border rounded px-3 py-2"
|
|
4518
4450
|
* buttonClassName="bg-blue-500 text-white px-4 py-2 rounded"
|
|
4519
4451
|
* messageClassName="text-sm"
|
|
4520
4452
|
* />
|