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.js CHANGED
@@ -2226,6 +2226,271 @@ var init_checkout_button = __esm({
2226
2226
  }
2227
2227
  });
2228
2228
 
2229
+ // src/components/portal-button.ts
2230
+ var portal_button_exports = {};
2231
+ __export(portal_button_exports, {
2232
+ RecurPortalButton: () => RecurPortalButton
2233
+ });
2234
+ var RecurPortalButton;
2235
+ var init_portal_button = __esm({
2236
+ "src/components/portal-button.ts"() {
2237
+ RecurPortalButton = class extends HTMLElement {
2238
+ constructor() {
2239
+ super();
2240
+ __publicField(this, "_isLoading", false);
2241
+ __publicField(this, "handleClick", async (e) => {
2242
+ e.preventDefault();
2243
+ if (this._isLoading || this.hasAttribute("disabled")) {
2244
+ return;
2245
+ }
2246
+ const portalUrl = this.getAttribute("portal-url");
2247
+ const apiEndpoint = this.getAttribute("api-endpoint");
2248
+ if (portalUrl) {
2249
+ this.redirectToPortal(portalUrl);
2250
+ return;
2251
+ }
2252
+ if (apiEndpoint) {
2253
+ await this.fetchAndRedirect(apiEndpoint);
2254
+ return;
2255
+ }
2256
+ this.dispatchError("Missing required attribute: either portal-url or api-endpoint must be provided");
2257
+ });
2258
+ this.attachShadow({ mode: "open" });
2259
+ }
2260
+ static get observedAttributes() {
2261
+ return [
2262
+ "portal-url",
2263
+ "api-endpoint",
2264
+ "customer-id",
2265
+ "return-url",
2266
+ "button-text",
2267
+ "button-style",
2268
+ "disabled",
2269
+ "target"
2270
+ ];
2271
+ }
2272
+ connectedCallback() {
2273
+ this.render();
2274
+ this.setupEventListeners();
2275
+ }
2276
+ disconnectedCallback() {
2277
+ const button = this.shadowRoot?.querySelector("button");
2278
+ if (button) {
2279
+ button.removeEventListener("click", this.handleClick);
2280
+ }
2281
+ }
2282
+ attributeChangedCallback(_name, oldValue, newValue) {
2283
+ if (oldValue !== newValue) {
2284
+ this.render();
2285
+ }
2286
+ }
2287
+ render() {
2288
+ const buttonText = this.getAttribute("button-text") || this.textContent?.trim() || "\u7BA1\u7406\u8A02\u95B1";
2289
+ const buttonStyle = this.getAttribute("button-style") || "primary";
2290
+ const isDisabled = this.hasAttribute("disabled") || this._isLoading;
2291
+ this.shadowRoot.innerHTML = `
2292
+ <style>
2293
+ :host {
2294
+ display: inline-block;
2295
+ }
2296
+
2297
+ button {
2298
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2299
+ font-size: 16px;
2300
+ font-weight: 500;
2301
+ padding: 12px 24px;
2302
+ border-radius: 8px;
2303
+ cursor: pointer;
2304
+ transition: all 0.2s ease;
2305
+ display: inline-flex;
2306
+ align-items: center;
2307
+ justify-content: center;
2308
+ gap: 8px;
2309
+ min-width: 120px;
2310
+ border: none;
2311
+ }
2312
+
2313
+ button:disabled {
2314
+ opacity: 0.6;
2315
+ cursor: not-allowed;
2316
+ }
2317
+
2318
+ /* Primary style (default) */
2319
+ button.primary {
2320
+ background: #18181b;
2321
+ color: #ffffff;
2322
+ }
2323
+
2324
+ button.primary:hover:not(:disabled) {
2325
+ background: #27272a;
2326
+ }
2327
+
2328
+ button.primary:active:not(:disabled) {
2329
+ background: #3f3f46;
2330
+ }
2331
+
2332
+ /* Outline style */
2333
+ button.outline {
2334
+ background: transparent;
2335
+ color: #18181b;
2336
+ border: 2px solid #18181b;
2337
+ }
2338
+
2339
+ button.outline:hover:not(:disabled) {
2340
+ background: #f4f4f5;
2341
+ }
2342
+
2343
+ button.outline:active:not(:disabled) {
2344
+ background: #e4e4e7;
2345
+ }
2346
+
2347
+ /* Gradient style */
2348
+ button.gradient {
2349
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2350
+ color: #ffffff;
2351
+ }
2352
+
2353
+ button.gradient:hover:not(:disabled) {
2354
+ opacity: 0.9;
2355
+ }
2356
+
2357
+ button.gradient:active:not(:disabled) {
2358
+ opacity: 0.8;
2359
+ }
2360
+
2361
+ /* Link style */
2362
+ button.link {
2363
+ background: transparent;
2364
+ color: #3b82f6;
2365
+ padding: 4px 8px;
2366
+ min-width: auto;
2367
+ text-decoration: underline;
2368
+ }
2369
+
2370
+ button.link:hover:not(:disabled) {
2371
+ color: #2563eb;
2372
+ }
2373
+
2374
+ button.link:active:not(:disabled) {
2375
+ color: #1d4ed8;
2376
+ }
2377
+
2378
+ /* Loading spinner */
2379
+ .spinner {
2380
+ width: 16px;
2381
+ height: 16px;
2382
+ border: 2px solid currentColor;
2383
+ border-top-color: transparent;
2384
+ border-radius: 50%;
2385
+ animation: spin 0.6s linear infinite;
2386
+ }
2387
+
2388
+ @keyframes spin {
2389
+ to { transform: rotate(360deg); }
2390
+ }
2391
+
2392
+ /* Focus state */
2393
+ button:focus-visible {
2394
+ outline: 2px solid #3b82f6;
2395
+ outline-offset: 2px;
2396
+ }
2397
+
2398
+ /* Portal icon */
2399
+ .portal-icon {
2400
+ width: 18px;
2401
+ height: 18px;
2402
+ }
2403
+ </style>
2404
+
2405
+ <button
2406
+ class="${buttonStyle}"
2407
+ ${isDisabled ? "disabled" : ""}
2408
+ aria-busy="${this._isLoading}"
2409
+ >
2410
+ ${this._isLoading ? '<span class="spinner"></span>' : this.getPortalIcon()}
2411
+ <span>${this._isLoading ? "\u8655\u7406\u4E2D..." : buttonText}</span>
2412
+ </button>
2413
+ `;
2414
+ }
2415
+ getPortalIcon() {
2416
+ return `
2417
+ <svg class="portal-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2418
+ <path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/>
2419
+ <circle cx="12" cy="7" r="4"/>
2420
+ </svg>
2421
+ `;
2422
+ }
2423
+ setupEventListeners() {
2424
+ const button = this.shadowRoot?.querySelector("button");
2425
+ if (button) {
2426
+ button.addEventListener("click", this.handleClick);
2427
+ }
2428
+ }
2429
+ redirectToPortal(url) {
2430
+ const target = this.getAttribute("target");
2431
+ this.dispatchEvent(new CustomEvent("portal-redirect", {
2432
+ detail: { url },
2433
+ bubbles: true,
2434
+ composed: true
2435
+ }));
2436
+ if (target === "_blank") {
2437
+ window.open(url, "_blank", "noopener,noreferrer");
2438
+ } else {
2439
+ window.location.href = url;
2440
+ }
2441
+ }
2442
+ async fetchAndRedirect(apiEndpoint) {
2443
+ const customerId = this.getAttribute("customer-id");
2444
+ const returnUrl = this.getAttribute("return-url");
2445
+ this._isLoading = true;
2446
+ this.render();
2447
+ this.setupEventListeners();
2448
+ try {
2449
+ const requestBody = {};
2450
+ if (customerId) requestBody.customerId = customerId;
2451
+ if (returnUrl) requestBody.returnUrl = returnUrl;
2452
+ const response = await fetch(apiEndpoint, {
2453
+ method: "POST",
2454
+ headers: {
2455
+ "Content-Type": "application/json"
2456
+ },
2457
+ body: JSON.stringify(requestBody)
2458
+ });
2459
+ if (!response.ok) {
2460
+ const error = await response.json().catch(() => ({}));
2461
+ throw new Error(error.error?.message || error.message || `HTTP ${response.status}: Failed to create portal session`);
2462
+ }
2463
+ const data = await response.json();
2464
+ const portalUrl = data.url || data.portalUrl;
2465
+ if (!portalUrl) {
2466
+ throw new Error("Portal URL not found in response");
2467
+ }
2468
+ this._isLoading = false;
2469
+ this.render();
2470
+ this.setupEventListeners();
2471
+ this.redirectToPortal(portalUrl);
2472
+ } catch (error) {
2473
+ this._isLoading = false;
2474
+ this.render();
2475
+ this.setupEventListeners();
2476
+ this.dispatchError(error.message || "Failed to create portal session");
2477
+ }
2478
+ }
2479
+ dispatchError(message) {
2480
+ console.error("[recur-portal]", message);
2481
+ this.dispatchEvent(new CustomEvent("portal-error", {
2482
+ detail: { message },
2483
+ bubbles: true,
2484
+ composed: true
2485
+ }));
2486
+ }
2487
+ };
2488
+ if (typeof window !== "undefined" && !customElements.get("recur-portal")) {
2489
+ customElements.define("recur-portal", RecurPortalButton);
2490
+ }
2491
+ }
2492
+ });
2493
+
2229
2494
  // src/components/index.ts
2230
2495
  async function registerComponents() {
2231
2496
  if (typeof window === "undefined") {
@@ -2240,7 +2505,8 @@ async function registerComponents() {
2240
2505
  Promise.resolve().then(() => (init_payment_form_skeleton(), payment_form_skeleton_exports)),
2241
2506
  Promise.resolve().then(() => (init_toast(), toast_exports)),
2242
2507
  Promise.resolve().then(() => (init_payment_form(), payment_form_exports)),
2243
- Promise.resolve().then(() => (init_checkout_button(), checkout_button_exports))
2508
+ Promise.resolve().then(() => (init_checkout_button(), checkout_button_exports)),
2509
+ Promise.resolve().then(() => (init_portal_button(), portal_button_exports))
2244
2510
  ]);
2245
2511
  const components = [
2246
2512
  "recur-loading-spinner",
@@ -2251,7 +2517,8 @@ async function registerComponents() {
2251
2517
  "recur-toast",
2252
2518
  "recur-toast-container",
2253
2519
  "recur-payment-form",
2254
- "recur-checkout"
2520
+ "recur-checkout",
2521
+ "recur-portal"
2255
2522
  ];
2256
2523
  const unregistered = components.filter((name) => !customElements.get(name));
2257
2524
  if (unregistered.length > 0) {