recur-tw 0.6.1 → 0.7.1
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 +269 -2
- package/dist/index.d.cts +71 -3
- package/dist/index.d.ts +71 -3
- package/dist/index.js +269 -2
- package/dist/recur.umd.js +146 -19
- package/dist/server.cjs +117 -0
- package/dist/server.d.cts +157 -0
- package/dist/server.d.ts +157 -0
- package/dist/server.js +114 -0
- package/package.json +8 -3
package/dist/index.cjs
CHANGED
|
@@ -2232,6 +2232,271 @@ var init_checkout_button = __esm({
|
|
|
2232
2232
|
}
|
|
2233
2233
|
});
|
|
2234
2234
|
|
|
2235
|
+
// src/components/portal-button.ts
|
|
2236
|
+
var portal_button_exports = {};
|
|
2237
|
+
__export(portal_button_exports, {
|
|
2238
|
+
RecurPortalButton: () => RecurPortalButton
|
|
2239
|
+
});
|
|
2240
|
+
var RecurPortalButton;
|
|
2241
|
+
var init_portal_button = __esm({
|
|
2242
|
+
"src/components/portal-button.ts"() {
|
|
2243
|
+
RecurPortalButton = class extends HTMLElement {
|
|
2244
|
+
constructor() {
|
|
2245
|
+
super();
|
|
2246
|
+
__publicField(this, "_isLoading", false);
|
|
2247
|
+
__publicField(this, "handleClick", async (e) => {
|
|
2248
|
+
e.preventDefault();
|
|
2249
|
+
if (this._isLoading || this.hasAttribute("disabled")) {
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
const portalUrl = this.getAttribute("portal-url");
|
|
2253
|
+
const apiEndpoint = this.getAttribute("api-endpoint");
|
|
2254
|
+
if (portalUrl) {
|
|
2255
|
+
this.redirectToPortal(portalUrl);
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
if (apiEndpoint) {
|
|
2259
|
+
await this.fetchAndRedirect(apiEndpoint);
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
this.dispatchError("Missing required attribute: either portal-url or api-endpoint must be provided");
|
|
2263
|
+
});
|
|
2264
|
+
this.attachShadow({ mode: "open" });
|
|
2265
|
+
}
|
|
2266
|
+
static get observedAttributes() {
|
|
2267
|
+
return [
|
|
2268
|
+
"portal-url",
|
|
2269
|
+
"api-endpoint",
|
|
2270
|
+
"customer-id",
|
|
2271
|
+
"return-url",
|
|
2272
|
+
"button-text",
|
|
2273
|
+
"button-style",
|
|
2274
|
+
"disabled",
|
|
2275
|
+
"target"
|
|
2276
|
+
];
|
|
2277
|
+
}
|
|
2278
|
+
connectedCallback() {
|
|
2279
|
+
this.render();
|
|
2280
|
+
this.setupEventListeners();
|
|
2281
|
+
}
|
|
2282
|
+
disconnectedCallback() {
|
|
2283
|
+
const button = this.shadowRoot?.querySelector("button");
|
|
2284
|
+
if (button) {
|
|
2285
|
+
button.removeEventListener("click", this.handleClick);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
attributeChangedCallback(_name, oldValue, newValue) {
|
|
2289
|
+
if (oldValue !== newValue) {
|
|
2290
|
+
this.render();
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
render() {
|
|
2294
|
+
const buttonText = this.getAttribute("button-text") || this.textContent?.trim() || "\u7BA1\u7406\u8A02\u95B1";
|
|
2295
|
+
const buttonStyle = this.getAttribute("button-style") || "primary";
|
|
2296
|
+
const isDisabled = this.hasAttribute("disabled") || this._isLoading;
|
|
2297
|
+
this.shadowRoot.innerHTML = `
|
|
2298
|
+
<style>
|
|
2299
|
+
:host {
|
|
2300
|
+
display: inline-block;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
button {
|
|
2304
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2305
|
+
font-size: 16px;
|
|
2306
|
+
font-weight: 500;
|
|
2307
|
+
padding: 12px 24px;
|
|
2308
|
+
border-radius: 8px;
|
|
2309
|
+
cursor: pointer;
|
|
2310
|
+
transition: all 0.2s ease;
|
|
2311
|
+
display: inline-flex;
|
|
2312
|
+
align-items: center;
|
|
2313
|
+
justify-content: center;
|
|
2314
|
+
gap: 8px;
|
|
2315
|
+
min-width: 120px;
|
|
2316
|
+
border: none;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
button:disabled {
|
|
2320
|
+
opacity: 0.6;
|
|
2321
|
+
cursor: not-allowed;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
/* Primary style (default) */
|
|
2325
|
+
button.primary {
|
|
2326
|
+
background: #18181b;
|
|
2327
|
+
color: #ffffff;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
button.primary:hover:not(:disabled) {
|
|
2331
|
+
background: #27272a;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
button.primary:active:not(:disabled) {
|
|
2335
|
+
background: #3f3f46;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
/* Outline style */
|
|
2339
|
+
button.outline {
|
|
2340
|
+
background: transparent;
|
|
2341
|
+
color: #18181b;
|
|
2342
|
+
border: 2px solid #18181b;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
button.outline:hover:not(:disabled) {
|
|
2346
|
+
background: #f4f4f5;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
button.outline:active:not(:disabled) {
|
|
2350
|
+
background: #e4e4e7;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
/* Gradient style */
|
|
2354
|
+
button.gradient {
|
|
2355
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
2356
|
+
color: #ffffff;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
button.gradient:hover:not(:disabled) {
|
|
2360
|
+
opacity: 0.9;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
button.gradient:active:not(:disabled) {
|
|
2364
|
+
opacity: 0.8;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
/* Link style */
|
|
2368
|
+
button.link {
|
|
2369
|
+
background: transparent;
|
|
2370
|
+
color: #3b82f6;
|
|
2371
|
+
padding: 4px 8px;
|
|
2372
|
+
min-width: auto;
|
|
2373
|
+
text-decoration: underline;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
button.link:hover:not(:disabled) {
|
|
2377
|
+
color: #2563eb;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
button.link:active:not(:disabled) {
|
|
2381
|
+
color: #1d4ed8;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/* Loading spinner */
|
|
2385
|
+
.spinner {
|
|
2386
|
+
width: 16px;
|
|
2387
|
+
height: 16px;
|
|
2388
|
+
border: 2px solid currentColor;
|
|
2389
|
+
border-top-color: transparent;
|
|
2390
|
+
border-radius: 50%;
|
|
2391
|
+
animation: spin 0.6s linear infinite;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
@keyframes spin {
|
|
2395
|
+
to { transform: rotate(360deg); }
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
/* Focus state */
|
|
2399
|
+
button:focus-visible {
|
|
2400
|
+
outline: 2px solid #3b82f6;
|
|
2401
|
+
outline-offset: 2px;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
/* Portal icon */
|
|
2405
|
+
.portal-icon {
|
|
2406
|
+
width: 18px;
|
|
2407
|
+
height: 18px;
|
|
2408
|
+
}
|
|
2409
|
+
</style>
|
|
2410
|
+
|
|
2411
|
+
<button
|
|
2412
|
+
class="${buttonStyle}"
|
|
2413
|
+
${isDisabled ? "disabled" : ""}
|
|
2414
|
+
aria-busy="${this._isLoading}"
|
|
2415
|
+
>
|
|
2416
|
+
${this._isLoading ? '<span class="spinner"></span>' : this.getPortalIcon()}
|
|
2417
|
+
<span>${this._isLoading ? "\u8655\u7406\u4E2D..." : buttonText}</span>
|
|
2418
|
+
</button>
|
|
2419
|
+
`;
|
|
2420
|
+
}
|
|
2421
|
+
getPortalIcon() {
|
|
2422
|
+
return `
|
|
2423
|
+
<svg class="portal-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2424
|
+
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/>
|
|
2425
|
+
<circle cx="12" cy="7" r="4"/>
|
|
2426
|
+
</svg>
|
|
2427
|
+
`;
|
|
2428
|
+
}
|
|
2429
|
+
setupEventListeners() {
|
|
2430
|
+
const button = this.shadowRoot?.querySelector("button");
|
|
2431
|
+
if (button) {
|
|
2432
|
+
button.addEventListener("click", this.handleClick);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
redirectToPortal(url) {
|
|
2436
|
+
const target = this.getAttribute("target");
|
|
2437
|
+
this.dispatchEvent(new CustomEvent("portal-redirect", {
|
|
2438
|
+
detail: { url },
|
|
2439
|
+
bubbles: true,
|
|
2440
|
+
composed: true
|
|
2441
|
+
}));
|
|
2442
|
+
if (target === "_blank") {
|
|
2443
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
2444
|
+
} else {
|
|
2445
|
+
window.location.href = url;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
async fetchAndRedirect(apiEndpoint) {
|
|
2449
|
+
const customerId = this.getAttribute("customer-id");
|
|
2450
|
+
const returnUrl = this.getAttribute("return-url");
|
|
2451
|
+
this._isLoading = true;
|
|
2452
|
+
this.render();
|
|
2453
|
+
this.setupEventListeners();
|
|
2454
|
+
try {
|
|
2455
|
+
const requestBody = {};
|
|
2456
|
+
if (customerId) requestBody.customerId = customerId;
|
|
2457
|
+
if (returnUrl) requestBody.returnUrl = returnUrl;
|
|
2458
|
+
const response = await fetch(apiEndpoint, {
|
|
2459
|
+
method: "POST",
|
|
2460
|
+
headers: {
|
|
2461
|
+
"Content-Type": "application/json"
|
|
2462
|
+
},
|
|
2463
|
+
body: JSON.stringify(requestBody)
|
|
2464
|
+
});
|
|
2465
|
+
if (!response.ok) {
|
|
2466
|
+
const error = await response.json().catch(() => ({}));
|
|
2467
|
+
throw new Error(error.error?.message || error.message || `HTTP ${response.status}: Failed to create portal session`);
|
|
2468
|
+
}
|
|
2469
|
+
const data = await response.json();
|
|
2470
|
+
const portalUrl = data.url || data.portalUrl;
|
|
2471
|
+
if (!portalUrl) {
|
|
2472
|
+
throw new Error("Portal URL not found in response");
|
|
2473
|
+
}
|
|
2474
|
+
this._isLoading = false;
|
|
2475
|
+
this.render();
|
|
2476
|
+
this.setupEventListeners();
|
|
2477
|
+
this.redirectToPortal(portalUrl);
|
|
2478
|
+
} catch (error) {
|
|
2479
|
+
this._isLoading = false;
|
|
2480
|
+
this.render();
|
|
2481
|
+
this.setupEventListeners();
|
|
2482
|
+
this.dispatchError(error.message || "Failed to create portal session");
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
dispatchError(message) {
|
|
2486
|
+
console.error("[recur-portal]", message);
|
|
2487
|
+
this.dispatchEvent(new CustomEvent("portal-error", {
|
|
2488
|
+
detail: { message },
|
|
2489
|
+
bubbles: true,
|
|
2490
|
+
composed: true
|
|
2491
|
+
}));
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
if (typeof window !== "undefined" && !customElements.get("recur-portal")) {
|
|
2495
|
+
customElements.define("recur-portal", RecurPortalButton);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
2499
|
+
|
|
2235
2500
|
// src/components/index.ts
|
|
2236
2501
|
async function registerComponents() {
|
|
2237
2502
|
if (typeof window === "undefined") {
|
|
@@ -2246,7 +2511,8 @@ async function registerComponents() {
|
|
|
2246
2511
|
Promise.resolve().then(() => (init_payment_form_skeleton(), payment_form_skeleton_exports)),
|
|
2247
2512
|
Promise.resolve().then(() => (init_toast(), toast_exports)),
|
|
2248
2513
|
Promise.resolve().then(() => (init_payment_form(), payment_form_exports)),
|
|
2249
|
-
Promise.resolve().then(() => (init_checkout_button(), checkout_button_exports))
|
|
2514
|
+
Promise.resolve().then(() => (init_checkout_button(), checkout_button_exports)),
|
|
2515
|
+
Promise.resolve().then(() => (init_portal_button(), portal_button_exports))
|
|
2250
2516
|
]);
|
|
2251
2517
|
const components = [
|
|
2252
2518
|
"recur-loading-spinner",
|
|
@@ -2257,7 +2523,8 @@ async function registerComponents() {
|
|
|
2257
2523
|
"recur-toast",
|
|
2258
2524
|
"recur-toast-container",
|
|
2259
2525
|
"recur-payment-form",
|
|
2260
|
-
"recur-checkout"
|
|
2526
|
+
"recur-checkout",
|
|
2527
|
+
"recur-portal"
|
|
2261
2528
|
];
|
|
2262
2529
|
const unregistered = components.filter((name) => !customElements.get(name));
|
|
2263
2530
|
if (unregistered.length > 0) {
|
package/dist/index.d.cts
CHANGED
|
@@ -205,11 +205,24 @@ interface RecurConfig {
|
|
|
205
205
|
}
|
|
206
206
|
interface CheckoutOptions {
|
|
207
207
|
/**
|
|
208
|
-
*
|
|
208
|
+
* Product ID to purchase
|
|
209
|
+
* Either productId or productSlug must be provided
|
|
209
210
|
*/
|
|
210
|
-
|
|
211
|
+
productId?: string;
|
|
212
|
+
/**
|
|
213
|
+
* Product slug to purchase (alternative to productId)
|
|
214
|
+
* Either productId or productSlug must be provided
|
|
215
|
+
*
|
|
216
|
+
* @example 'premium-monthly', 'basic-yearly'
|
|
217
|
+
*/
|
|
218
|
+
productSlug?: string;
|
|
211
219
|
/**
|
|
212
|
-
*
|
|
220
|
+
* @deprecated Use productId instead
|
|
221
|
+
* Plan ID to subscribe to (kept for backward compatibility)
|
|
222
|
+
*/
|
|
223
|
+
planId?: string;
|
|
224
|
+
/**
|
|
225
|
+
* Customer name
|
|
213
226
|
*/
|
|
214
227
|
customerName?: string;
|
|
215
228
|
/**
|
|
@@ -530,6 +543,61 @@ declare global {
|
|
|
530
543
|
}
|
|
531
544
|
}
|
|
532
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Recur Portal Button Web Component
|
|
548
|
+
*
|
|
549
|
+
* A simple, drop-in button for redirecting customers to the Customer Portal.
|
|
550
|
+
*
|
|
551
|
+
* Usage patterns:
|
|
552
|
+
*
|
|
553
|
+
* 1. Direct URL mode (recommended for server-rendered apps):
|
|
554
|
+
* Use when you already have the portal URL from your server.
|
|
555
|
+
* ```html
|
|
556
|
+
* <recur-portal
|
|
557
|
+
* portal-url="https://portal.recur.tw/s/ps_xxx">
|
|
558
|
+
* 管理訂閱
|
|
559
|
+
* </recur-portal>
|
|
560
|
+
* ```
|
|
561
|
+
*
|
|
562
|
+
* 2. API mode (for dynamic portal session creation):
|
|
563
|
+
* Calls your backend API endpoint to create a portal session.
|
|
564
|
+
* ```html
|
|
565
|
+
* <recur-portal
|
|
566
|
+
* api-endpoint="/api/create-portal-session"
|
|
567
|
+
* customer-id="cus_xxx">
|
|
568
|
+
* 管理訂閱
|
|
569
|
+
* </recur-portal>
|
|
570
|
+
* ```
|
|
571
|
+
*
|
|
572
|
+
* Styling:
|
|
573
|
+
* - button-style: "primary" | "outline" | "gradient" | "link"
|
|
574
|
+
* - button-text: Override button text (default: slot content or "管理訂閱")
|
|
575
|
+
*
|
|
576
|
+
* Events:
|
|
577
|
+
* - portal-redirect: Fired before redirecting to portal
|
|
578
|
+
* - portal-error: Fired when an error occurs
|
|
579
|
+
*/
|
|
580
|
+
declare class RecurPortalButton extends HTMLElement {
|
|
581
|
+
private _isLoading;
|
|
582
|
+
static get observedAttributes(): string[];
|
|
583
|
+
constructor();
|
|
584
|
+
connectedCallback(): void;
|
|
585
|
+
disconnectedCallback(): void;
|
|
586
|
+
attributeChangedCallback(_name: string, oldValue: string, newValue: string): void;
|
|
587
|
+
private render;
|
|
588
|
+
private getPortalIcon;
|
|
589
|
+
private setupEventListeners;
|
|
590
|
+
private handleClick;
|
|
591
|
+
private redirectToPortal;
|
|
592
|
+
private fetchAndRedirect;
|
|
593
|
+
private dispatchError;
|
|
594
|
+
}
|
|
595
|
+
declare global {
|
|
596
|
+
interface HTMLElementTagNameMap {
|
|
597
|
+
'recur-portal': RecurPortalButton;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
533
601
|
interface RecurProviderProps {
|
|
534
602
|
children: React.ReactNode;
|
|
535
603
|
config?: RecurConfig;
|
package/dist/index.d.ts
CHANGED
|
@@ -205,11 +205,24 @@ interface RecurConfig {
|
|
|
205
205
|
}
|
|
206
206
|
interface CheckoutOptions {
|
|
207
207
|
/**
|
|
208
|
-
*
|
|
208
|
+
* Product ID to purchase
|
|
209
|
+
* Either productId or productSlug must be provided
|
|
209
210
|
*/
|
|
210
|
-
|
|
211
|
+
productId?: string;
|
|
212
|
+
/**
|
|
213
|
+
* Product slug to purchase (alternative to productId)
|
|
214
|
+
* Either productId or productSlug must be provided
|
|
215
|
+
*
|
|
216
|
+
* @example 'premium-monthly', 'basic-yearly'
|
|
217
|
+
*/
|
|
218
|
+
productSlug?: string;
|
|
211
219
|
/**
|
|
212
|
-
*
|
|
220
|
+
* @deprecated Use productId instead
|
|
221
|
+
* Plan ID to subscribe to (kept for backward compatibility)
|
|
222
|
+
*/
|
|
223
|
+
planId?: string;
|
|
224
|
+
/**
|
|
225
|
+
* Customer name
|
|
213
226
|
*/
|
|
214
227
|
customerName?: string;
|
|
215
228
|
/**
|
|
@@ -530,6 +543,61 @@ declare global {
|
|
|
530
543
|
}
|
|
531
544
|
}
|
|
532
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Recur Portal Button Web Component
|
|
548
|
+
*
|
|
549
|
+
* A simple, drop-in button for redirecting customers to the Customer Portal.
|
|
550
|
+
*
|
|
551
|
+
* Usage patterns:
|
|
552
|
+
*
|
|
553
|
+
* 1. Direct URL mode (recommended for server-rendered apps):
|
|
554
|
+
* Use when you already have the portal URL from your server.
|
|
555
|
+
* ```html
|
|
556
|
+
* <recur-portal
|
|
557
|
+
* portal-url="https://portal.recur.tw/s/ps_xxx">
|
|
558
|
+
* 管理訂閱
|
|
559
|
+
* </recur-portal>
|
|
560
|
+
* ```
|
|
561
|
+
*
|
|
562
|
+
* 2. API mode (for dynamic portal session creation):
|
|
563
|
+
* Calls your backend API endpoint to create a portal session.
|
|
564
|
+
* ```html
|
|
565
|
+
* <recur-portal
|
|
566
|
+
* api-endpoint="/api/create-portal-session"
|
|
567
|
+
* customer-id="cus_xxx">
|
|
568
|
+
* 管理訂閱
|
|
569
|
+
* </recur-portal>
|
|
570
|
+
* ```
|
|
571
|
+
*
|
|
572
|
+
* Styling:
|
|
573
|
+
* - button-style: "primary" | "outline" | "gradient" | "link"
|
|
574
|
+
* - button-text: Override button text (default: slot content or "管理訂閱")
|
|
575
|
+
*
|
|
576
|
+
* Events:
|
|
577
|
+
* - portal-redirect: Fired before redirecting to portal
|
|
578
|
+
* - portal-error: Fired when an error occurs
|
|
579
|
+
*/
|
|
580
|
+
declare class RecurPortalButton extends HTMLElement {
|
|
581
|
+
private _isLoading;
|
|
582
|
+
static get observedAttributes(): string[];
|
|
583
|
+
constructor();
|
|
584
|
+
connectedCallback(): void;
|
|
585
|
+
disconnectedCallback(): void;
|
|
586
|
+
attributeChangedCallback(_name: string, oldValue: string, newValue: string): void;
|
|
587
|
+
private render;
|
|
588
|
+
private getPortalIcon;
|
|
589
|
+
private setupEventListeners;
|
|
590
|
+
private handleClick;
|
|
591
|
+
private redirectToPortal;
|
|
592
|
+
private fetchAndRedirect;
|
|
593
|
+
private dispatchError;
|
|
594
|
+
}
|
|
595
|
+
declare global {
|
|
596
|
+
interface HTMLElementTagNameMap {
|
|
597
|
+
'recur-portal': RecurPortalButton;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
533
601
|
interface RecurProviderProps {
|
|
534
602
|
children: React.ReactNode;
|
|
535
603
|
config?: RecurConfig;
|