recur-tw 0.4.4 β†’ 0.6.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/README.md CHANGED
@@ -9,7 +9,7 @@ Taiwan-specific subscription checkout SDK with embedded payment forms powered by
9
9
  ## πŸš€ Features
10
10
 
11
11
  - βœ… **Hosted Checkout** - Zero-backend integration with `<recur-checkout>` Web Component
12
- - βœ… **React SDK** - Full React integration with hooks (`usePlans`, `useRecur`)
12
+ - βœ… **React SDK** - Full React integration with hooks (`useProducts`, `useRecur`)
13
13
  - βœ… **Vanilla JS** - Use with plain HTML/JavaScript (no framework required)
14
14
  - βœ… **Embedded Checkout** - Native payment forms with PAYUNi credit card fields
15
15
  - βœ… **Web Components** - Modern, encapsulated UI components
@@ -53,7 +53,7 @@ Full framework integration with React hooks and embedded checkout:
53
53
  ```tsx
54
54
  'use client';
55
55
 
56
- import { RecurProvider, usePlans, useRecur } from 'recur-tw';
56
+ import { RecurProvider, useProducts, useRecur } from 'recur-tw';
57
57
 
58
58
  // 1. Wrap your app with RecurProvider
59
59
  export default function App() {
@@ -64,31 +64,33 @@ export default function App() {
64
64
  containerElementId: 'recur-payment-container' // For embedded checkout
65
65
  }}
66
66
  >
67
- <SubscriptionPage />
67
+ <ProductsPage />
68
68
  </RecurProvider>
69
69
  );
70
70
  }
71
71
 
72
- // 2. Fetch plans and checkout
73
- function SubscriptionPage() {
74
- const { data: plans, isLoading } = usePlans();
72
+ // 2. Fetch products and checkout
73
+ function ProductsPage() {
74
+ // Fetch all products (or filter by type: 'SUBSCRIPTION', 'ONE_TIME', etc.)
75
+ const { data: products, isLoading } = useProducts();
75
76
  const { checkout, isCheckingOut } = useRecur();
76
77
 
77
- if (isLoading) return <div>Loading plans...</div>;
78
+ if (isLoading) return <div>Loading products...</div>;
78
79
 
79
80
  return (
80
81
  <div>
81
- {plans?.map((plan) => (
82
+ {products?.map((product) => (
82
83
  <button
83
- key={plan.id}
84
+ key={product.id}
84
85
  onClick={() => checkout({
85
- planId: plan.id,
86
+ planId: product.id,
86
87
  customerEmail: 'user@example.com',
87
88
  customerName: 'John Doe',
88
89
  })}
89
90
  disabled={isCheckingOut}
90
91
  >
91
- Subscribe to {plan.name} - NT${plan.price}
92
+ {/* product.type: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION' */}
93
+ {product.type === 'SUBSCRIPTION' ? 'Subscribe to' : 'Buy'} {product.name} - NT${product.price}
92
94
  </button>
93
95
  ))}
94
96
 
@@ -301,26 +303,34 @@ document.querySelector('recur-checkout').addEventListener('checkout-error', (e)
301
303
 
302
304
  ### React Hooks
303
305
 
304
- #### `usePlans()`
306
+ #### `useProducts()`
305
307
 
306
- Fetch available subscription plans:
308
+ Fetch available products with optional type filtering:
307
309
 
308
310
  ```tsx
309
- import { usePlans } from 'recur-tw';
311
+ import { useProducts } from 'recur-tw';
310
312
 
311
- function PlansPage() {
312
- const { data: plans, isLoading, error } = usePlans();
313
+ function ProductsPage() {
314
+ // Fetch all products
315
+ const { data: products, isLoading, error } = useProducts();
316
+
317
+ // Or filter by type
318
+ // const { data: subscriptions } = useProducts({ type: 'SUBSCRIPTION' });
319
+ // const { data: oneTimeProducts } = useProducts({ type: 'ONE_TIME' });
313
320
 
314
321
  if (isLoading) return <div>Loading...</div>;
315
322
  if (error) return <div>Error: {error.message}</div>;
316
323
 
317
324
  return (
318
325
  <div>
319
- {plans?.map((plan) => (
320
- <div key={plan.id}>
321
- <h3>{plan.name}</h3>
322
- <p>NT${plan.price} / {plan.billingPeriod}</p>
323
- {plan.trialDays > 0 && <p>πŸŽ‰ {plan.trialDays} days free trial</p>}
326
+ {products?.map((product) => (
327
+ <div key={product.id}>
328
+ <span className="badge">
329
+ {product.type === 'SUBSCRIPTION' ? '訂閱' : '一欑性'}
330
+ </span>
331
+ <h3>{product.name}</h3>
332
+ <p>NT${product.price} {product.billingPeriod && `/ ${product.billingPeriod}`}</p>
333
+ {product.trialDays && product.trialDays > 0 && <p>πŸŽ‰ {product.trialDays} days free trial</p>}
324
334
  </div>
325
335
  ))}
326
336
  </div>
@@ -328,6 +338,12 @@ function PlansPage() {
328
338
  }
329
339
  ```
330
340
 
341
+ **Product Types:**
342
+ - `SUBSCRIPTION` - Recurring subscription products
343
+ - `ONE_TIME` - One-time purchase products
344
+ - `CREDITS` - Credit/token packages
345
+ - `DONATION` - Donation products
346
+
331
347
  #### `useRecur()`
332
348
 
333
349
  Access checkout functionality:
@@ -354,7 +370,8 @@ function CheckoutButton({ planId }: { planId: string }) {
354
370
  - `isCheckingOut` - Boolean indicating if checkout is in progress
355
371
  - `config` - Current SDK configuration
356
372
  - `updateConfig(newConfig)` - Update configuration dynamically
357
- - `fetchPlans()` - Manually fetch plans
373
+ - `fetchProducts(options)` - Manually fetch products (optionally filter by type)
374
+ - `fetchPlans()` - Fetch subscription products only (backward compat)
358
375
 
359
376
  ---
360
377
 
@@ -581,24 +598,25 @@ export function CheckoutForm({ planId }: { planId: string }) {
581
598
  }
582
599
  ```
583
600
 
584
- #### Dynamic Plan Selection
601
+ #### Dynamic Product Selection
585
602
 
586
603
  ```tsx
587
604
  'use client';
588
605
 
589
606
  import { useState } from 'react';
590
- import { usePlans, useRecur } from 'recur-tw';
607
+ import { useProducts, useRecur } from 'recur-tw';
591
608
 
592
609
  export function PricingTable() {
593
- const { data: plans } = usePlans();
610
+ // Filter to show only subscription products
611
+ const { data: products } = useProducts({ type: 'SUBSCRIPTION' });
594
612
  const { checkout, isCheckingOut } = useRecur();
595
- const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
613
+ const [selectedProduct, setSelectedProduct] = useState<string | null>(null);
596
614
 
597
- const handleSelectPlan = async (planId: string) => {
598
- setSelectedPlan(planId);
615
+ const handleSelectProduct = async (productId: string) => {
616
+ setSelectedProduct(productId);
599
617
 
600
618
  await checkout({
601
- planId,
619
+ planId: productId,
602
620
  customerEmail: 'user@example.com',
603
621
  customerName: 'John Doe',
604
622
  });
@@ -606,18 +624,18 @@ export function PricingTable() {
606
624
 
607
625
  return (
608
626
  <div>
609
- {!selectedPlan ? (
610
- // Plan selection
611
- <div className="plans-grid">
612
- {plans?.map((plan) => (
613
- <div key={plan.id} className="plan-card">
614
- <h3>{plan.name}</h3>
615
- <p className="price">NT${plan.price} / {plan.billingPeriod}</p>
616
- {plan.trialDays > 0 && (
617
- <p className="trial">πŸŽ‰ {plan.trialDays} day free trial</p>
627
+ {!selectedProduct ? (
628
+ // Product selection
629
+ <div className="products-grid">
630
+ {products?.map((product) => (
631
+ <div key={product.id} className="product-card">
632
+ <h3>{product.name}</h3>
633
+ <p className="price">NT${product.price} / {product.billingPeriod}</p>
634
+ {product.trialDays && product.trialDays > 0 && (
635
+ <p className="trial">πŸŽ‰ {product.trialDays} day free trial</p>
618
636
  )}
619
- <button onClick={() => handleSelectPlan(plan.id)}>
620
- Select Plan
637
+ <button onClick={() => handleSelectProduct(product.id)}>
638
+ Select Product
621
639
  </button>
622
640
  </div>
623
641
  ))}
@@ -625,7 +643,7 @@ export function PricingTable() {
625
643
  ) : (
626
644
  // Payment form
627
645
  <div>
628
- <button onClick={() => setSelectedPlan(null)}>← Back to plans</button>
646
+ <button onClick={() => setSelectedProduct(null)}>← Back to products</button>
629
647
  <div id="recur-payment-container"></div>
630
648
  </div>
631
649
  )}
@@ -700,26 +718,43 @@ import type {
700
718
  CheckoutOptions,
701
719
  CheckoutResult,
702
720
  CheckoutError,
703
- Plan,
704
- PlansResult,
721
+ Product,
722
+ ProductsResult,
723
+ FetchProductsOptions,
705
724
  Subscription,
706
725
  SubscriptionResult,
726
+ // Backward compatibility aliases
727
+ Plan, // = Product
728
+ PlansResult, // = ProductsResult
707
729
  } from 'recur-tw';
708
730
  ```
709
731
 
710
732
  ### Type Definitions
711
733
 
712
734
  ```tsx
713
- interface Plan {
735
+ interface Product {
714
736
  id: string;
715
737
  name: string;
738
+ slug: string;
739
+ description: string | null;
740
+ // Product type distinguishes between recurring and one-time purchases
741
+ type: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION';
742
+ billingPeriod: 'WEEKLY' | 'MONTHLY' | 'YEARLY' | 'ONE_TIME' | 'CUSTOM' | null;
716
743
  price: number;
717
- billingPeriod: 'WEEKLY' | 'MONTHLY' | 'YEARLY' | 'CUSTOM';
718
- trialDays: number;
719
- description?: string;
720
- features?: Record<string, any>;
744
+ currency: string;
745
+ trialDays: number | null;
746
+ metadata: ProductMetadata | null;
747
+ productFamily?: string | null;
748
+ displayOrder: number;
721
749
  }
722
750
 
751
+ interface FetchProductsOptions {
752
+ type?: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION';
753
+ }
754
+
755
+ // Backward compatibility
756
+ type Plan = Product;
757
+
723
758
  interface Subscription {
724
759
  id: string;
725
760
  status: string;
@@ -819,7 +854,7 @@ POST /v1/checkouts/:id/pay β†’ 執葌付款
819
854
  **Breaking Changes:**
820
855
  - `organizationId` β†’ `publishableKey` in config
821
856
  - Checkout now uses Web Components instead of innerHTML
822
- - New `usePlans()` hook for fetching plans
857
+ - New `useProducts()` hook for fetching products (replaces `usePlans()`)
823
858
  - Embedded mode is now the default when `containerElementId` is set
824
859
 
825
860
  **Migration:**
package/dist/index.cjs CHANGED
@@ -2598,13 +2598,22 @@ function RecurProvider({ children, config: initialConfig = {} }) {
2598
2598
  const tradeResult = await paymentSession.getTradeResult();
2599
2599
  console.log("[Recur SDK] Trade result received:", tradeResult);
2600
2600
  console.log("[Recur SDK] Step 8: Executing payment...");
2601
- const creditToken = tradeResult.EncryptInfo || tradeResult.creditToken;
2602
2601
  const timestamp = tradeResult.HashTimestamp || tradeResult.timestamp;
2603
- if (!creditToken) {
2604
- console.error("[Recur SDK] Missing payment token. Available fields:", Object.keys(tradeResult));
2605
- throw new Error("Missing payment token from PAYUNi SDK");
2602
+ let paymentBody = {};
2603
+ if (checkoutResult.checkout.productType === "SUBSCRIPTION") {
2604
+ const subscriptionCreditToken = checkoutResult.creditToken;
2605
+ const subscriptionTimestamp = checkoutResult.sdkTimestamp;
2606
+ if (!subscriptionCreditToken) {
2607
+ console.error("[Recur SDK] Missing creditToken from checkout for subscription");
2608
+ throw new Error("Missing creditToken for subscription payment");
2609
+ }
2610
+ paymentBody = {
2611
+ creditToken: subscriptionCreditToken,
2612
+ timestamp: subscriptionTimestamp || timestamp
2613
+ };
2614
+ console.log("[Recur SDK] Using creditToken from checkout:", subscriptionCreditToken.substring(0, 30) + "...");
2615
+ console.log("[Recur SDK] Using timestamp:", subscriptionTimestamp ? "from checkout (sdkTimestamp)" : "from tradeResult");
2606
2616
  }
2607
- const paymentBody = checkoutResult.checkout.productType === "SUBSCRIPTION" ? { creditToken, timestamp } : {};
2608
2617
  const paymentResponse = await fetch(
2609
2618
  `${baseUrl}/v1/checkouts/${checkoutResult.checkout.id}/pay`,
2610
2619
  {
@@ -2686,13 +2695,17 @@ function RecurProvider({ children, config: initialConfig = {} }) {
2686
2695
  },
2687
2696
  [config]
2688
2697
  );
2689
- const fetchPlans = React.useCallback(
2690
- async () => {
2698
+ const fetchProducts = React.useCallback(
2699
+ async (options) => {
2691
2700
  if (!config.publishableKey) {
2692
2701
  throw new Error("publishableKey is required");
2693
2702
  }
2694
2703
  const baseUrl = config.baseUrl || "https://api.recur.tw";
2695
- const response = await fetch(`${baseUrl}/v1/plans`, {
2704
+ const url = new URL(`${baseUrl}/v1/products`);
2705
+ if (options?.type) {
2706
+ url.searchParams.set("type", options.type);
2707
+ }
2708
+ const response = await fetch(url.toString(), {
2696
2709
  method: "GET",
2697
2710
  headers: {
2698
2711
  "Content-Type": "application/json",
@@ -2701,21 +2714,29 @@ function RecurProvider({ children, config: initialConfig = {} }) {
2701
2714
  });
2702
2715
  if (!response.ok) {
2703
2716
  const errorData = await response.json().catch(() => ({}));
2704
- throw new Error(errorData.error || "Failed to fetch plans");
2717
+ throw new Error(errorData.error || "Failed to fetch products");
2705
2718
  }
2706
2719
  return await response.json();
2707
2720
  },
2708
2721
  [config]
2709
2722
  );
2723
+ const fetchPlans = React.useCallback(
2724
+ async () => {
2725
+ const result = await fetchProducts({ type: "SUBSCRIPTION" });
2726
+ return { plans: result.products };
2727
+ },
2728
+ [fetchProducts]
2729
+ );
2710
2730
  const value = React.useMemo(
2711
2731
  () => ({
2712
2732
  config,
2713
2733
  checkout,
2734
+ fetchProducts,
2714
2735
  fetchPlans,
2715
2736
  isCheckingOut,
2716
2737
  updateConfig
2717
2738
  }),
2718
- [config, checkout, fetchPlans, isCheckingOut, updateConfig]
2739
+ [config, checkout, fetchProducts, fetchPlans, isCheckingOut, updateConfig]
2719
2740
  );
2720
2741
  return /* @__PURE__ */ jsxRuntime.jsx(RecurContext.Provider, { value, children });
2721
2742
  }
@@ -2726,9 +2747,9 @@ function useRecur() {
2726
2747
  }
2727
2748
  return context;
2728
2749
  }
2729
- function usePlans(options = {}) {
2730
- const { enabled = true, onSuccess, onError } = options;
2731
- const { fetchPlans } = useRecur();
2750
+ function useProducts(options = {}) {
2751
+ const { enabled = true, type, onSuccess, onError } = options;
2752
+ const { fetchProducts } = useRecur();
2732
2753
  const [data, setData] = React.useState(void 0);
2733
2754
  const [isLoading2, setIsLoading] = React.useState(false);
2734
2755
  const [error, setError] = React.useState(null);
@@ -2737,17 +2758,17 @@ function usePlans(options = {}) {
2737
2758
  setIsLoading(true);
2738
2759
  setError(null);
2739
2760
  try {
2740
- const result = await fetchPlans();
2741
- setData(result.plans);
2742
- onSuccess?.(result.plans);
2761
+ const result = await fetchProducts({ type });
2762
+ setData(result.products);
2763
+ onSuccess?.(result.products);
2743
2764
  } catch (err) {
2744
- const error2 = err instanceof Error ? err : new Error("Failed to fetch plans");
2765
+ const error2 = err instanceof Error ? err : new Error("Failed to fetch products");
2745
2766
  setError(error2);
2746
2767
  onError?.(error2);
2747
2768
  } finally {
2748
2769
  setIsLoading(false);
2749
2770
  }
2750
- }, [enabled, fetchPlans, onSuccess, onError]);
2771
+ }, [enabled, type, fetchProducts, onSuccess, onError]);
2751
2772
  React.useEffect(() => {
2752
2773
  fetchData();
2753
2774
  }, [fetchData]);
@@ -2806,6 +2827,7 @@ function useSubscribe(options = {}) {
2806
2827
  }
2807
2828
 
2808
2829
  exports.RecurProvider = RecurProvider;
2809
- exports.usePlans = usePlans;
2830
+ exports.usePlans = useProducts;
2831
+ exports.useProducts = useProducts;
2810
2832
  exports.useRecur = useRecur;
2811
2833
  exports.useSubscribe = useSubscribe;
package/dist/index.d.cts CHANGED
@@ -325,24 +325,34 @@ interface ProductMetadata {
325
325
  /** Whether this is the primary/default variant */
326
326
  isMainVariant?: boolean;
327
327
  }
328
- interface Plan {
328
+ interface Product {
329
329
  id: string;
330
330
  name: string;
331
331
  slug: string;
332
332
  description: string | null;
333
- billingPeriod: 'WEEKLY' | 'MONTHLY' | 'YEARLY' | 'CUSTOM';
333
+ type: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION';
334
+ billingPeriod: 'WEEKLY' | 'MONTHLY' | 'YEARLY' | 'ONE_TIME' | 'CUSTOM' | null;
334
335
  price: number;
335
- trialDays: number;
336
- /** Customer-facing features (e.g., storage limits, support level) */
337
- features: Record<string, unknown> | null;
336
+ currency: string;
337
+ trialDays: number | null;
338
338
  /** System metadata for grouping and organization */
339
339
  metadata: ProductMetadata | null;
340
340
  /** Convenience field: extracted from metadata.productFamily for easier access */
341
341
  productFamily?: string | null;
342
342
  displayOrder: number;
343
343
  }
344
+ interface ProductsResult {
345
+ products: Product[];
346
+ }
347
+ type Plan = Product;
344
348
  interface PlansResult {
345
- plans: Plan[];
349
+ plans: Product[];
350
+ }
351
+ interface FetchProductsOptions {
352
+ /**
353
+ * Filter by product type
354
+ */
355
+ type?: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION';
346
356
  }
347
357
  interface RecurContextValue {
348
358
  /**
@@ -354,7 +364,12 @@ interface RecurContextValue {
354
364
  */
355
365
  checkout: (options: CheckoutOptions) => Promise<void>;
356
366
  /**
357
- * Fetch available subscription plans
367
+ * Fetch available products
368
+ */
369
+ fetchProducts: (options?: FetchProductsOptions) => Promise<ProductsResult>;
370
+ /**
371
+ * @deprecated Use fetchProducts instead
372
+ * Fetch available subscription plans (backward compatibility)
358
373
  */
359
374
  fetchPlans: () => Promise<PlansResult>;
360
375
  /**
@@ -558,28 +573,32 @@ declare function RecurProvider({ children, config: initialConfig }: RecurProvide
558
573
  */
559
574
  declare function useRecur(): RecurContextValue;
560
575
 
561
- interface UsePlansOptions {
576
+ interface UseProductsOptions {
562
577
  /**
563
- * Whether to fetch plans immediately on mount
578
+ * Whether to fetch products immediately on mount
564
579
  * @default true
565
580
  */
566
581
  enabled?: boolean;
567
582
  /**
568
- * Callback when plans are fetched successfully
583
+ * Filter by product type
569
584
  */
570
- onSuccess?: (plans: Plan[]) => void;
585
+ type?: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION';
586
+ /**
587
+ * Callback when products are fetched successfully
588
+ */
589
+ onSuccess?: (products: Product[]) => void;
571
590
  /**
572
591
  * Callback when an error occurs
573
592
  */
574
593
  onError?: (error: Error) => void;
575
594
  }
576
- interface UsePlansResult {
595
+ interface UseProductsResult {
577
596
  /**
578
- * The fetched plans data
597
+ * The fetched products data
579
598
  */
580
- data: Plan[] | undefined;
599
+ data: Product[] | undefined;
581
600
  /**
582
- * Whether the plans are currently being fetched
601
+ * Whether the products are currently being fetched
583
602
  */
584
603
  isLoading: boolean;
585
604
  /**
@@ -592,29 +611,38 @@ interface UsePlansResult {
592
611
  refetch: () => Promise<void>;
593
612
  }
594
613
  /**
595
- * usePlans Hook
614
+ * useProducts Hook
596
615
  *
597
- * Fetch available subscription plans with automatic state management
616
+ * Fetch available products with automatic state management
598
617
  *
599
618
  * @example
600
619
  * ```tsx
601
- * function PlansPage() {
602
- * const { data: plans, isLoading, error } = usePlans();
620
+ * function ProductsPage() {
621
+ * const { data: products, isLoading, error } = useProducts();
603
622
  *
604
- * if (isLoading) return <PlansSkeletonLoader />;
623
+ * if (isLoading) return <ProductsSkeletonLoader />;
605
624
  * if (error) return <ErrorMessage error={error} />;
606
625
  *
607
626
  * return (
608
627
  * <div>
609
- * {plans?.map(plan => (
610
- * <PlanCard key={plan.id} plan={plan} />
628
+ * {products?.map(product => (
629
+ * <ProductCard key={product.id} product={product} />
611
630
  * ))}
612
631
  * </div>
613
632
  * );
614
633
  * }
615
634
  * ```
635
+ *
636
+ * @example Filter by type
637
+ * ```tsx
638
+ * // Only fetch subscription products
639
+ * const { data: subscriptions } = useProducts({ type: 'SUBSCRIPTION' });
640
+ *
641
+ * // Only fetch one-time products
642
+ * const { data: oneTimeProducts } = useProducts({ type: 'ONE_TIME' });
643
+ * ```
616
644
  */
617
- declare function usePlans(options?: UsePlansOptions): UsePlansResult;
645
+ declare function useProducts(options?: UseProductsOptions): UseProductsResult;
618
646
 
619
647
  interface UseSubscribeOptions {
620
648
  /**
@@ -696,4 +724,4 @@ interface UseSubscribeResult {
696
724
  */
697
725
  declare function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult;
698
726
 
699
- export { type CheckoutError, type CheckoutOptions, type CheckoutResult, type Plan, type PlansResult, type ProductMetadata, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type SubscriptionResult, type UsePlansOptions, type UsePlansResult, type UseSubscribeOptions, type UseSubscribeResult, usePlans, useRecur, useSubscribe };
727
+ export { type CheckoutError, type CheckoutOptions, type CheckoutResult, type Plan, type PlansResult, type Product, type ProductMetadata, type ProductsResult, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type SubscriptionResult, type UseProductsOptions as UsePlansOptions, type UseProductsResult as UsePlansResult, type UseProductsOptions, type UseProductsResult, type UseSubscribeOptions, type UseSubscribeResult, useProducts as usePlans, useProducts, useRecur, useSubscribe };
package/dist/index.d.ts CHANGED
@@ -325,24 +325,34 @@ interface ProductMetadata {
325
325
  /** Whether this is the primary/default variant */
326
326
  isMainVariant?: boolean;
327
327
  }
328
- interface Plan {
328
+ interface Product {
329
329
  id: string;
330
330
  name: string;
331
331
  slug: string;
332
332
  description: string | null;
333
- billingPeriod: 'WEEKLY' | 'MONTHLY' | 'YEARLY' | 'CUSTOM';
333
+ type: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION';
334
+ billingPeriod: 'WEEKLY' | 'MONTHLY' | 'YEARLY' | 'ONE_TIME' | 'CUSTOM' | null;
334
335
  price: number;
335
- trialDays: number;
336
- /** Customer-facing features (e.g., storage limits, support level) */
337
- features: Record<string, unknown> | null;
336
+ currency: string;
337
+ trialDays: number | null;
338
338
  /** System metadata for grouping and organization */
339
339
  metadata: ProductMetadata | null;
340
340
  /** Convenience field: extracted from metadata.productFamily for easier access */
341
341
  productFamily?: string | null;
342
342
  displayOrder: number;
343
343
  }
344
+ interface ProductsResult {
345
+ products: Product[];
346
+ }
347
+ type Plan = Product;
344
348
  interface PlansResult {
345
- plans: Plan[];
349
+ plans: Product[];
350
+ }
351
+ interface FetchProductsOptions {
352
+ /**
353
+ * Filter by product type
354
+ */
355
+ type?: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION';
346
356
  }
347
357
  interface RecurContextValue {
348
358
  /**
@@ -354,7 +364,12 @@ interface RecurContextValue {
354
364
  */
355
365
  checkout: (options: CheckoutOptions) => Promise<void>;
356
366
  /**
357
- * Fetch available subscription plans
367
+ * Fetch available products
368
+ */
369
+ fetchProducts: (options?: FetchProductsOptions) => Promise<ProductsResult>;
370
+ /**
371
+ * @deprecated Use fetchProducts instead
372
+ * Fetch available subscription plans (backward compatibility)
358
373
  */
359
374
  fetchPlans: () => Promise<PlansResult>;
360
375
  /**
@@ -558,28 +573,32 @@ declare function RecurProvider({ children, config: initialConfig }: RecurProvide
558
573
  */
559
574
  declare function useRecur(): RecurContextValue;
560
575
 
561
- interface UsePlansOptions {
576
+ interface UseProductsOptions {
562
577
  /**
563
- * Whether to fetch plans immediately on mount
578
+ * Whether to fetch products immediately on mount
564
579
  * @default true
565
580
  */
566
581
  enabled?: boolean;
567
582
  /**
568
- * Callback when plans are fetched successfully
583
+ * Filter by product type
569
584
  */
570
- onSuccess?: (plans: Plan[]) => void;
585
+ type?: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION';
586
+ /**
587
+ * Callback when products are fetched successfully
588
+ */
589
+ onSuccess?: (products: Product[]) => void;
571
590
  /**
572
591
  * Callback when an error occurs
573
592
  */
574
593
  onError?: (error: Error) => void;
575
594
  }
576
- interface UsePlansResult {
595
+ interface UseProductsResult {
577
596
  /**
578
- * The fetched plans data
597
+ * The fetched products data
579
598
  */
580
- data: Plan[] | undefined;
599
+ data: Product[] | undefined;
581
600
  /**
582
- * Whether the plans are currently being fetched
601
+ * Whether the products are currently being fetched
583
602
  */
584
603
  isLoading: boolean;
585
604
  /**
@@ -592,29 +611,38 @@ interface UsePlansResult {
592
611
  refetch: () => Promise<void>;
593
612
  }
594
613
  /**
595
- * usePlans Hook
614
+ * useProducts Hook
596
615
  *
597
- * Fetch available subscription plans with automatic state management
616
+ * Fetch available products with automatic state management
598
617
  *
599
618
  * @example
600
619
  * ```tsx
601
- * function PlansPage() {
602
- * const { data: plans, isLoading, error } = usePlans();
620
+ * function ProductsPage() {
621
+ * const { data: products, isLoading, error } = useProducts();
603
622
  *
604
- * if (isLoading) return <PlansSkeletonLoader />;
623
+ * if (isLoading) return <ProductsSkeletonLoader />;
605
624
  * if (error) return <ErrorMessage error={error} />;
606
625
  *
607
626
  * return (
608
627
  * <div>
609
- * {plans?.map(plan => (
610
- * <PlanCard key={plan.id} plan={plan} />
628
+ * {products?.map(product => (
629
+ * <ProductCard key={product.id} product={product} />
611
630
  * ))}
612
631
  * </div>
613
632
  * );
614
633
  * }
615
634
  * ```
635
+ *
636
+ * @example Filter by type
637
+ * ```tsx
638
+ * // Only fetch subscription products
639
+ * const { data: subscriptions } = useProducts({ type: 'SUBSCRIPTION' });
640
+ *
641
+ * // Only fetch one-time products
642
+ * const { data: oneTimeProducts } = useProducts({ type: 'ONE_TIME' });
643
+ * ```
616
644
  */
617
- declare function usePlans(options?: UsePlansOptions): UsePlansResult;
645
+ declare function useProducts(options?: UseProductsOptions): UseProductsResult;
618
646
 
619
647
  interface UseSubscribeOptions {
620
648
  /**
@@ -696,4 +724,4 @@ interface UseSubscribeResult {
696
724
  */
697
725
  declare function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult;
698
726
 
699
- export { type CheckoutError, type CheckoutOptions, type CheckoutResult, type Plan, type PlansResult, type ProductMetadata, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type SubscriptionResult, type UsePlansOptions, type UsePlansResult, type UseSubscribeOptions, type UseSubscribeResult, usePlans, useRecur, useSubscribe };
727
+ export { type CheckoutError, type CheckoutOptions, type CheckoutResult, type Plan, type PlansResult, type Product, type ProductMetadata, type ProductsResult, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type SubscriptionResult, type UseProductsOptions as UsePlansOptions, type UseProductsResult as UsePlansResult, type UseProductsOptions, type UseProductsResult, type UseSubscribeOptions, type UseSubscribeResult, useProducts as usePlans, useProducts, useRecur, useSubscribe };
package/dist/index.js CHANGED
@@ -2592,13 +2592,22 @@ function RecurProvider({ children, config: initialConfig = {} }) {
2592
2592
  const tradeResult = await paymentSession.getTradeResult();
2593
2593
  console.log("[Recur SDK] Trade result received:", tradeResult);
2594
2594
  console.log("[Recur SDK] Step 8: Executing payment...");
2595
- const creditToken = tradeResult.EncryptInfo || tradeResult.creditToken;
2596
2595
  const timestamp = tradeResult.HashTimestamp || tradeResult.timestamp;
2597
- if (!creditToken) {
2598
- console.error("[Recur SDK] Missing payment token. Available fields:", Object.keys(tradeResult));
2599
- throw new Error("Missing payment token from PAYUNi SDK");
2596
+ let paymentBody = {};
2597
+ if (checkoutResult.checkout.productType === "SUBSCRIPTION") {
2598
+ const subscriptionCreditToken = checkoutResult.creditToken;
2599
+ const subscriptionTimestamp = checkoutResult.sdkTimestamp;
2600
+ if (!subscriptionCreditToken) {
2601
+ console.error("[Recur SDK] Missing creditToken from checkout for subscription");
2602
+ throw new Error("Missing creditToken for subscription payment");
2603
+ }
2604
+ paymentBody = {
2605
+ creditToken: subscriptionCreditToken,
2606
+ timestamp: subscriptionTimestamp || timestamp
2607
+ };
2608
+ console.log("[Recur SDK] Using creditToken from checkout:", subscriptionCreditToken.substring(0, 30) + "...");
2609
+ console.log("[Recur SDK] Using timestamp:", subscriptionTimestamp ? "from checkout (sdkTimestamp)" : "from tradeResult");
2600
2610
  }
2601
- const paymentBody = checkoutResult.checkout.productType === "SUBSCRIPTION" ? { creditToken, timestamp } : {};
2602
2611
  const paymentResponse = await fetch(
2603
2612
  `${baseUrl}/v1/checkouts/${checkoutResult.checkout.id}/pay`,
2604
2613
  {
@@ -2680,13 +2689,17 @@ function RecurProvider({ children, config: initialConfig = {} }) {
2680
2689
  },
2681
2690
  [config]
2682
2691
  );
2683
- const fetchPlans = useCallback(
2684
- async () => {
2692
+ const fetchProducts = useCallback(
2693
+ async (options) => {
2685
2694
  if (!config.publishableKey) {
2686
2695
  throw new Error("publishableKey is required");
2687
2696
  }
2688
2697
  const baseUrl = config.baseUrl || "https://api.recur.tw";
2689
- const response = await fetch(`${baseUrl}/v1/plans`, {
2698
+ const url = new URL(`${baseUrl}/v1/products`);
2699
+ if (options?.type) {
2700
+ url.searchParams.set("type", options.type);
2701
+ }
2702
+ const response = await fetch(url.toString(), {
2690
2703
  method: "GET",
2691
2704
  headers: {
2692
2705
  "Content-Type": "application/json",
@@ -2695,21 +2708,29 @@ function RecurProvider({ children, config: initialConfig = {} }) {
2695
2708
  });
2696
2709
  if (!response.ok) {
2697
2710
  const errorData = await response.json().catch(() => ({}));
2698
- throw new Error(errorData.error || "Failed to fetch plans");
2711
+ throw new Error(errorData.error || "Failed to fetch products");
2699
2712
  }
2700
2713
  return await response.json();
2701
2714
  },
2702
2715
  [config]
2703
2716
  );
2717
+ const fetchPlans = useCallback(
2718
+ async () => {
2719
+ const result = await fetchProducts({ type: "SUBSCRIPTION" });
2720
+ return { plans: result.products };
2721
+ },
2722
+ [fetchProducts]
2723
+ );
2704
2724
  const value = useMemo(
2705
2725
  () => ({
2706
2726
  config,
2707
2727
  checkout,
2728
+ fetchProducts,
2708
2729
  fetchPlans,
2709
2730
  isCheckingOut,
2710
2731
  updateConfig
2711
2732
  }),
2712
- [config, checkout, fetchPlans, isCheckingOut, updateConfig]
2733
+ [config, checkout, fetchProducts, fetchPlans, isCheckingOut, updateConfig]
2713
2734
  );
2714
2735
  return /* @__PURE__ */ jsx(RecurContext.Provider, { value, children });
2715
2736
  }
@@ -2720,9 +2741,9 @@ function useRecur() {
2720
2741
  }
2721
2742
  return context;
2722
2743
  }
2723
- function usePlans(options = {}) {
2724
- const { enabled = true, onSuccess, onError } = options;
2725
- const { fetchPlans } = useRecur();
2744
+ function useProducts(options = {}) {
2745
+ const { enabled = true, type, onSuccess, onError } = options;
2746
+ const { fetchProducts } = useRecur();
2726
2747
  const [data, setData] = useState(void 0);
2727
2748
  const [isLoading2, setIsLoading] = useState(false);
2728
2749
  const [error, setError] = useState(null);
@@ -2731,17 +2752,17 @@ function usePlans(options = {}) {
2731
2752
  setIsLoading(true);
2732
2753
  setError(null);
2733
2754
  try {
2734
- const result = await fetchPlans();
2735
- setData(result.plans);
2736
- onSuccess?.(result.plans);
2755
+ const result = await fetchProducts({ type });
2756
+ setData(result.products);
2757
+ onSuccess?.(result.products);
2737
2758
  } catch (err) {
2738
- const error2 = err instanceof Error ? err : new Error("Failed to fetch plans");
2759
+ const error2 = err instanceof Error ? err : new Error("Failed to fetch products");
2739
2760
  setError(error2);
2740
2761
  onError?.(error2);
2741
2762
  } finally {
2742
2763
  setIsLoading(false);
2743
2764
  }
2744
- }, [enabled, fetchPlans, onSuccess, onError]);
2765
+ }, [enabled, type, fetchProducts, onSuccess, onError]);
2745
2766
  useEffect(() => {
2746
2767
  fetchData();
2747
2768
  }, [fetchData]);
@@ -2799,4 +2820,4 @@ function useSubscribe(options = {}) {
2799
2820
  };
2800
2821
  }
2801
2822
 
2802
- export { RecurProvider, usePlans, useRecur, useSubscribe };
2823
+ export { RecurProvider, useProducts as usePlans, useProducts, useRecur, useSubscribe };
package/dist/recur.umd.js CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";var RecurCheckout=(()=>{var C=Object.defineProperty;var ce=Object.getOwnPropertyDescriptor;var le=Object.getOwnPropertyNames;var de=Object.prototype.hasOwnProperty;var ue=(c,e,t)=>e in c?C(c,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):c[e]=t;var g=(c,e)=>()=>(c&&(e=c(c=0)),e);var h=(c,e)=>{for(var t in e)C(c,t,{get:e[t],enumerable:!0})},me=(c,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of le(e))!de.call(c,i)&&i!==t&&C(c,i,{get:()=>e[i],enumerable:!(r=ce(e,i))||r.enumerable});return c};var pe=c=>me(C({},"__esModule",{value:!0}),c);var a=(c,e,t)=>ue(c,typeof e!="symbol"?e+"":e,t);var N={};h(N,{RecurLoadingSpinner:()=>S});var S,B=g(()=>{"use strict";S=class extends HTMLElement{static get observedAttributes(){return["message","size"]}constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}attributeChangedCallback(e,t,r){t!==r&&this.render()}get message(){return this.getAttribute("message")||"\u6B63\u5728\u8655\u7406\u8A02\u95B1..."}get size(){let e=this.getAttribute("size");return e==="small"||e==="large"?e:"medium"}getSizeValue(){return{small:24,medium:40,large:56}[this.size]}render(){let e=this.getSizeValue();this.shadowRoot.innerHTML=`
1
+ "use strict";var RecurCheckout=(()=>{var C=Object.defineProperty;var ce=Object.getOwnPropertyDescriptor;var le=Object.getOwnPropertyNames;var de=Object.prototype.hasOwnProperty;var ue=(c,e,t)=>e in c?C(c,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):c[e]=t;var b=(c,e)=>()=>(c&&(e=c(c=0)),e);var h=(c,e)=>{for(var t in e)C(c,t,{get:e[t],enumerable:!0})},me=(c,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of le(e))!de.call(c,i)&&i!==t&&C(c,i,{get:()=>e[i],enumerable:!(r=ce(e,i))||r.enumerable});return c};var pe=c=>me(C({},"__esModule",{value:!0}),c);var a=(c,e,t)=>ue(c,typeof e!="symbol"?e+"":e,t);var O={};h(O,{RecurLoadingSpinner:()=>S});var S,F=b(()=>{"use strict";S=class extends HTMLElement{static get observedAttributes(){return["message","size"]}constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}attributeChangedCallback(e,t,r){t!==r&&this.render()}get message(){return this.getAttribute("message")||"\u6B63\u5728\u8655\u7406\u8A02\u95B1..."}get size(){let e=this.getAttribute("size");return e==="small"||e==="large"?e:"medium"}getSizeValue(){return{small:24,medium:40,large:56}[this.size]}render(){let e=this.getSizeValue();this.shadowRoot.innerHTML=`
2
2
  <style>
3
3
  :host {
4
4
  display: block;
@@ -40,7 +40,7 @@
40
40
 
41
41
  <div class="recur-sdk__spinner"></div>
42
42
  ${this.message?`<p class="recur-sdk__loading-text">${this.message}</p>`:""}
43
- `}};typeof window<"u"&&!customElements.get("recur-loading-spinner")&&customElements.define("recur-loading-spinner",S)});var K={};h(K,{RecurSuccessMessage:()=>T});var T,O=g(()=>{"use strict";T=class extends HTMLElement{static get observedAttributes(){return["title","message","icon"]}constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}attributeChangedCallback(e,t,r){t!==r&&this.render()}get successTitle(){return this.getAttribute("title")||"Subscription Complete!"}get successMessage(){return this.getAttribute("message")||"Thank you for subscribing. Your payment has been processed successfully."}get showIcon(){return this.getAttribute("icon")!=="false"}render(){this.shadowRoot.innerHTML=`
43
+ `}};typeof window<"u"&&!customElements.get("recur-loading-spinner")&&customElements.define("recur-loading-spinner",S)});var j={};h(j,{RecurSuccessMessage:()=>T});var T,Y=b(()=>{"use strict";T=class extends HTMLElement{static get observedAttributes(){return["title","message","icon"]}constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}attributeChangedCallback(e,t,r){t!==r&&this.render()}get successTitle(){return this.getAttribute("title")||"Subscription Complete!"}get successMessage(){return this.getAttribute("message")||"Thank you for subscribing. Your payment has been processed successfully."}get showIcon(){return this.getAttribute("icon")!=="false"}render(){this.shadowRoot.innerHTML=`
44
44
  <style>
45
45
  :host {
46
46
  display: block;
@@ -121,7 +121,7 @@
121
121
  <p class="recur-sdk__success-message">${this.successMessage}</p>
122
122
  <slot></slot>
123
123
  </div>
124
- `}};typeof window<"u"&&!customElements.get("recur-success-message")&&customElements.define("recur-success-message",T)});var F={};h(F,{RecurErrorDisplay:()=>I});var I,j=g(()=>{"use strict";I=class extends HTMLElement{static get observedAttributes(){return["error","dismissible"]}constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}attributeChangedCallback(e,t,r){t!==r&&this.render()}get error(){return this.getAttribute("error")||""}get isDismissible(){return this.getAttribute("dismissible")==="true"}handleDismiss(){this.dispatchEvent(new CustomEvent("dismiss",{bubbles:!0,composed:!0})),this.remove()}render(){if(!this.error){this.shadowRoot.innerHTML="";return}this.shadowRoot.innerHTML=`
124
+ `}};typeof window<"u"&&!customElements.get("recur-success-message")&&customElements.define("recur-success-message",T)});var q={};h(q,{RecurErrorDisplay:()=>I});var I,V=b(()=>{"use strict";I=class extends HTMLElement{static get observedAttributes(){return["error","dismissible"]}constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}attributeChangedCallback(e,t,r){t!==r&&this.render()}get error(){return this.getAttribute("error")||""}get isDismissible(){return this.getAttribute("dismissible")==="true"}handleDismiss(){this.dispatchEvent(new CustomEvent("dismiss",{bubbles:!0,composed:!0})),this.remove()}render(){if(!this.error){this.shadowRoot.innerHTML="";return}this.shadowRoot.innerHTML=`
125
125
  <style>
126
126
  :host {
127
127
  display: block;
@@ -211,7 +211,7 @@
211
211
  </button>
212
212
  `:""}
213
213
  </div>
214
- `,this.isDismissible&&this.shadowRoot.querySelector(".recur-sdk__error-dismiss")?.addEventListener("click",()=>this.handleDismiss())}};typeof window<"u"&&!customElements.get("recur-error-display")&&customElements.define("recur-error-display",I)});var Y={};h(Y,{RecurSkeletonLoader:()=>R});var R,q=g(()=>{"use strict";R=class extends HTMLElement{static get observedAttributes(){return["type"]}constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}attributeChangedCallback(e,t,r){t!==r&&this.render()}get type(){let e=this.getAttribute("type");return e==="list"||e==="card"?e:"payment-form"}renderPaymentFormSkeleton(){return`
214
+ `,this.isDismissible&&this.shadowRoot.querySelector(".recur-sdk__error-dismiss")?.addEventListener("click",()=>this.handleDismiss())}};typeof window<"u"&&!customElements.get("recur-error-display")&&customElements.define("recur-error-display",I)});var X={};h(X,{RecurSkeletonLoader:()=>R});var R,W=b(()=>{"use strict";R=class extends HTMLElement{static get observedAttributes(){return["type"]}constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}attributeChangedCallback(e,t,r){t!==r&&this.render()}get type(){let e=this.getAttribute("type");return e==="list"||e==="card"?e:"payment-form"}renderPaymentFormSkeleton(){return`
215
215
  <div class="skeleton-field">
216
216
  <div class="skeleton-label"></div>
217
217
  <div class="skeleton-input"></div>
@@ -377,7 +377,7 @@
377
377
  </style>
378
378
 
379
379
  ${e}
380
- `}};typeof window<"u"&&!customElements.get("recur-skeleton-loader")&&customElements.define("recur-skeleton-loader",R)});var V={};h(V,{RecurPaymentFormSkeleton:()=>M});var M,X=g(()=>{"use strict";M=class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}render(){this.shadowRoot.innerHTML=`
380
+ `}};typeof window<"u"&&!customElements.get("recur-skeleton-loader")&&customElements.define("recur-skeleton-loader",R)});var J={};h(J,{RecurPaymentFormSkeleton:()=>P});var P,Z=b(()=>{"use strict";P=class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}render(){this.shadowRoot.innerHTML=`
381
381
  <style>
382
382
  :host {
383
383
  display: block;
@@ -644,7 +644,7 @@
644
644
  <p class="security-text">\u60A8\u7684\u4ED8\u6B3E\u8CC7\u8A0A\u7D93\u904E\u52A0\u5BC6\u4FDD\u8B77</p>
645
645
  </div>
646
646
  </div>
647
- `}};typeof window<"u"&&!customElements.get("recur-payment-form-skeleton")&&customElements.define("recur-payment-form-skeleton",M)});var W={};h(W,{RecurToast:()=>P,RecurToastContainer:()=>k});var P,f,k,J=g(()=>{"use strict";P=class extends HTMLElement{static get observedAttributes(){return["message","type","duration"]}constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render(),this.setupAutoDismiss()}get message(){return this.getAttribute("message")||"Notification"}get type(){let e=this.getAttribute("type");return e==="success"||e==="error"?e:"info"}get duration(){let e=this.getAttribute("duration");return e?parseInt(e,10):5e3}setupAutoDismiss(){let e=this.duration;e>0&&setTimeout(()=>this.dismiss(),e)}dismiss(){this.style.animation="recur-toast-slide-out 0.3s ease-in-out",setTimeout(()=>this.remove(),300)}getTypeIcon(){switch(this.type){case"success":return`<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
647
+ `}};typeof window<"u"&&!customElements.get("recur-payment-form-skeleton")&&customElements.define("recur-payment-form-skeleton",P)});var G={};h(G,{RecurToast:()=>M,RecurToastContainer:()=>x});var M,f,x,Q=b(()=>{"use strict";M=class extends HTMLElement{static get observedAttributes(){return["message","type","duration"]}constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render(),this.setupAutoDismiss()}get message(){return this.getAttribute("message")||"Notification"}get type(){let e=this.getAttribute("type");return e==="success"||e==="error"?e:"info"}get duration(){let e=this.getAttribute("duration");return e?parseInt(e,10):5e3}setupAutoDismiss(){let e=this.duration;e>0&&setTimeout(()=>this.dismiss(),e)}dismiss(){this.style.animation="recur-toast-slide-out 0.3s ease-in-out",setTimeout(()=>this.remove(),300)}getTypeIcon(){switch(this.type){case"success":return`<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
648
648
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
649
649
  </svg>`;case"error":return`<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
650
650
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
@@ -764,7 +764,7 @@
764
764
  </svg>
765
765
  </button>
766
766
  </div>
767
- `,this.shadowRoot.querySelector(".toast-close")?.addEventListener("click",i=>{i.stopPropagation(),this.dismiss()}),this.shadowRoot.querySelector(".toast")?.addEventListener("click",()=>this.dismiss())}static show(e,t="info",r=5e3){let i=k.getInstance(),s=document.createElement("recur-toast");return s.setAttribute("message",e),s.setAttribute("type",t),s.setAttribute("duration",r.toString()),i.appendChild(s),s}},f=class f extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}render(){this.shadowRoot.innerHTML=`
767
+ `,this.shadowRoot.querySelector(".toast-close")?.addEventListener("click",i=>{i.stopPropagation(),this.dismiss()}),this.shadowRoot.querySelector(".toast")?.addEventListener("click",()=>this.dismiss())}static show(e,t="info",r=5e3){let i=x.getInstance(),s=document.createElement("recur-toast");return s.setAttribute("message",e),s.setAttribute("type",t),s.setAttribute("duration",r.toString()),i.appendChild(s),s}},f=class f extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render()}render(){this.shadowRoot.innerHTML=`
768
768
  <style>
769
769
  :host {
770
770
  position: fixed;
@@ -791,7 +791,7 @@
791
791
  </style>
792
792
 
793
793
  <slot></slot>
794
- `}static getInstance(){return f.instance||(f.instance=document.querySelector("recur-toast-container"),f.instance||(f.instance=document.createElement("recur-toast-container"),document.body.appendChild(f.instance))),f.instance}};a(f,"instance",null);k=f;typeof window<"u"&&!customElements.get("recur-toast")&&customElements.define("recur-toast",P);typeof window<"u"&&!customElements.get("recur-toast-container")&&customElements.define("recur-toast-container",k)});var Z={};h(Z,{RecurPaymentForm:()=>L});var L,G=g(()=>{"use strict";L=class extends HTMLElement{constructor(){super();a(this,"containerId");a(this,"customStyles");a(this,"_isInitializing",!1);a(this,"_initializationAborted",!1);this.containerId=this.getAttribute("container-id")||`recur-${Date.now()}`,this.customStyles=this.getAttribute("custom-styles")||"",this.attachShadow({mode:"open"})}connectedCallback(){this.render()}disconnectedCallback(){console.log("[PaymentForm] Component disconnected, cleaning up..."),this._initializationAborted=!0,this._paymentSession=null;let t=document.getElementById(`${this.containerId}-submit-btn`);if(t){let r=t.cloneNode(!0);t.parentNode?.replaceChild(r,t)}}static get observedAttributes(){return["custom-styles","customer-name","customer-email","plan-name","amount","billing-period"]}attributeChangedCallback(t,r,i){t==="custom-styles"&&r!==i?(this.customStyles=i||"",this.updateCustomStyles()):r!==i&&this.updateCustomerInfoSection()}render(){this.shadowRoot.innerHTML=`
794
+ `}static getInstance(){return f.instance||(f.instance=document.querySelector("recur-toast-container"),f.instance||(f.instance=document.createElement("recur-toast-container"),document.body.appendChild(f.instance))),f.instance}};a(f,"instance",null);x=f;typeof window<"u"&&!customElements.get("recur-toast")&&customElements.define("recur-toast",M);typeof window<"u"&&!customElements.get("recur-toast-container")&&customElements.define("recur-toast-container",x)});var ee={};h(ee,{RecurPaymentForm:()=>L});var L,te=b(()=>{"use strict";L=class extends HTMLElement{constructor(){super();a(this,"containerId");a(this,"customStyles");a(this,"_isInitializing",!1);a(this,"_initializationAborted",!1);this.containerId=this.getAttribute("container-id")||`recur-${Date.now()}`,this.customStyles=this.getAttribute("custom-styles")||"",this.attachShadow({mode:"open"})}connectedCallback(){this.render()}disconnectedCallback(){console.log("[PaymentForm] Component disconnected, cleaning up..."),this._initializationAborted=!0,this._paymentSession=null;let t=document.getElementById(`${this.containerId}-submit-btn`);if(t){let r=t.cloneNode(!0);t.parentNode?.replaceChild(r,t)}}static get observedAttributes(){return["custom-styles","customer-name","customer-email","plan-name","amount","billing-period"]}attributeChangedCallback(t,r,i){t==="custom-styles"&&r!==i?(this.customStyles=i||"",this.updateCustomStyles()):r!==i&&this.updateCustomerInfoSection()}render(){this.shadowRoot.innerHTML=`
795
795
  <style>
796
796
  /* \u9019\u4E9B\u6A23\u5F0F\u88AB\u9694\u96E2\u5728 Shadow DOM \u5167\uFF0C\u4E0D\u6703\u5F71\u97FF\u5916\u90E8 */
797
797
  :host {
@@ -1217,10 +1217,10 @@
1217
1217
  >
1218
1218
  <span id="${this.containerId}-submit-text">\u78BA\u8A8D\u4ED8\u6B3E</span>
1219
1219
  </button>
1220
- `,t}async initializePayment(t,r){if(this._initializationAborted)return console.log("[PaymentForm] Initialization aborted before start"),null;if(this._isInitializing=!0,await this.updateComplete(),this._initializationAborted)return console.log("[PaymentForm] Initialization aborted after updateComplete"),this._isInitializing=!1,null;if(!window.UniPayment)throw this._isInitializing=!1,new Error("PAYUNi SDK not loaded");try{let i=document.getElementById(`${this.containerId}-card-no`),s=document.getElementById(`${this.containerId}-card-exp`),n=document.getElementById(`${this.containerId}-card-cvc`);if(!i||!s||!n)return console.log("[PaymentForm] DOM elements not found, likely disconnected"),this._isInitializing=!1,null;if(this._initializationAborted)return console.log("[PaymentForm] Initialization aborted before createSession"),this._isInitializing=!1,null;let o=window.UniPayment.createSession(t,{env:r==="SANDBOX"?"S":"P",elements:{CardNo:`${this.containerId}-card-no`,CardExp:`${this.containerId}-card-exp`,CardCvc:`${this.containerId}-card-cvc`}});if(this._initializationAborted)return console.log("[PaymentForm] Initialization aborted before paymentSession.start()"),this._isInitializing=!1,null;try{await o.start()}catch(l){if((l?.message?.includes("1008")||l?.message?.includes("timeout")||l?.code===1008)&&this._initializationAborted)return console.log("[PaymentForm] Caught 1008 timeout error after component disconnect - ignoring"),this._isInitializing=!1,null;throw l}return this._initializationAborted?(console.log("[PaymentForm] Initialization aborted after paymentSession.start()"),this._isInitializing=!1,null):(this.hideCardSkeletons(),o.onUpdate?.(l=>{if(this._initializationAborted){console.log("[PaymentForm] Ignoring onUpdate event - component disconnected");return}console.log("[PaymentForm] PAYUNi update:",l);let d=l.status&&l.status.CardNo===!0&&l.status.CardExp===!0&&l.status.CardCvc===!0,v=document.getElementById(`${this.containerId}-submit-btn`);v&&(v.disabled=!d,console.log("[PaymentForm] Submit button disabled:",!d))}),this._initializationAborted?(console.log("[PaymentForm] Initialization aborted before saving session"),this._isInitializing=!1,null):(this._paymentSession=o,this.setupFormSubmission(),this._isInitializing=!1,o))}catch(i){throw this._isInitializing=!1,console.error("[PaymentForm] Failed to initialize payment:",i),i}}setupFormSubmission(){let t=document.getElementById(`${this.containerId}-submit-btn`);t&&t.addEventListener("click",async r=>{if(r.preventDefault(),t.classList.contains("loading"))return;let i,s,n=document.getElementById(`${this.containerId}-email`),o=document.getElementById(`${this.containerId}-name`);if(n&&o){if(i=n.value,s=o.value,!i||!s){this.showError("\u8ACB\u586B\u5BEB\u59D3\u540D\u548C\u96FB\u5B50\u90F5\u4EF6");return}}else if(i=this.getAttribute("customer-email")||void 0,s=this.getAttribute("customer-name")||void 0,!i||!s){this.showError("\u5BA2\u6236\u8CC7\u8A0A\u4E0D\u5B8C\u6574");return}this.clearError(),this.setButtonLoading(!0),this.dispatchEvent(new CustomEvent("submit",{detail:{customerEmail:i,customerName:s,paymentSession:this._paymentSession},bubbles:!0,composed:!0}))})}setButtonLoading(t){let r=document.getElementById(`${this.containerId}-submit-btn`);r&&(t?(r.classList.add("loading"),r.disabled=!0,r.innerHTML=`
1220
+ `,t}async initializePayment(t,r){if(this._initializationAborted)return console.log("[PaymentForm] Initialization aborted before start"),null;if(this._isInitializing=!0,await this.updateComplete(),this._initializationAborted)return console.log("[PaymentForm] Initialization aborted after updateComplete"),this._isInitializing=!1,null;if(!window.UniPayment)throw this._isInitializing=!1,new Error("PAYUNi SDK not loaded");try{let i=document.getElementById(`${this.containerId}-card-no`),s=document.getElementById(`${this.containerId}-card-exp`),n=document.getElementById(`${this.containerId}-card-cvc`);if(!i||!s||!n)return console.log("[PaymentForm] DOM elements not found, likely disconnected"),this._isInitializing=!1,null;if(this._initializationAborted)return console.log("[PaymentForm] Initialization aborted before createSession"),this._isInitializing=!1,null;let o=window.UniPayment.createSession(t,{env:r==="SANDBOX"?"S":"P",elements:{CardNo:`${this.containerId}-card-no`,CardExp:`${this.containerId}-card-exp`,CardCvc:`${this.containerId}-card-cvc`}});if(this._initializationAborted)return console.log("[PaymentForm] Initialization aborted before paymentSession.start()"),this._isInitializing=!1,null;try{await o.start()}catch(d){if((d?.message?.includes("1008")||d?.message?.includes("timeout")||d?.code===1008)&&this._initializationAborted)return console.log("[PaymentForm] Caught 1008 timeout error after component disconnect - ignoring"),this._isInitializing=!1,null;throw d}return this._initializationAborted?(console.log("[PaymentForm] Initialization aborted after paymentSession.start()"),this._isInitializing=!1,null):(this.hideCardSkeletons(),o.onUpdate?.(d=>{if(this._initializationAborted){console.log("[PaymentForm] Ignoring onUpdate event - component disconnected");return}console.log("[PaymentForm] PAYUNi update:",d);let l=d.status&&d.status.CardNo===!0&&d.status.CardExp===!0&&d.status.CardCvc===!0,v=document.getElementById(`${this.containerId}-submit-btn`);v&&(v.disabled=!l,console.log("[PaymentForm] Submit button disabled:",!l))}),this._initializationAborted?(console.log("[PaymentForm] Initialization aborted before saving session"),this._isInitializing=!1,null):(this._paymentSession=o,this.setupFormSubmission(),this._isInitializing=!1,o))}catch(i){throw this._isInitializing=!1,console.error("[PaymentForm] Failed to initialize payment:",i),i}}setupFormSubmission(){let t=document.getElementById(`${this.containerId}-submit-btn`);t&&t.addEventListener("click",async r=>{if(r.preventDefault(),t.classList.contains("loading"))return;let i,s,n=document.getElementById(`${this.containerId}-email`),o=document.getElementById(`${this.containerId}-name`);if(n&&o){if(i=n.value,s=o.value,!i||!s){this.showError("\u8ACB\u586B\u5BEB\u59D3\u540D\u548C\u96FB\u5B50\u90F5\u4EF6");return}}else if(i=this.getAttribute("customer-email")||void 0,s=this.getAttribute("customer-name")||void 0,!i||!s){this.showError("\u5BA2\u6236\u8CC7\u8A0A\u4E0D\u5B8C\u6574");return}this.clearError(),this.setButtonLoading(!0),this.dispatchEvent(new CustomEvent("submit",{detail:{customerEmail:i,customerName:s,paymentSession:this._paymentSession},bubbles:!0,composed:!0}))})}setButtonLoading(t){let r=document.getElementById(`${this.containerId}-submit-btn`);r&&(t?(r.classList.add("loading"),r.disabled=!0,r.innerHTML=`
1221
1221
  <span class="recur-loading-spinner"></span>
1222
1222
  <span>\u8655\u7406\u4E2D...</span>
1223
- `):(r.classList.remove("loading"),r.disabled=!1,r.innerHTML='<span id="'+this.containerId+'-submit-text">\u78BA\u8A8D\u4ED8\u6B3E</span>'))}resetButton(){this.setButtonLoading(!1)}showError(t){let r=this.querySelector(".recur-sdk__error-container");r||(r=document.createElement("div"),r.className="recur-sdk__error-container",r.style.cssText="margin-bottom: 16px;",this.insertBefore(r,this.firstChild));let i=document.createElement("recur-error-display");i.setAttribute("error",t),i.setAttribute("dismissible","true"),r.innerHTML="",r.appendChild(i),i.scrollIntoView({behavior:"smooth",block:"center"})}clearError(){let t=this.querySelector(".recur-sdk__error-container");t&&(t.innerHTML="")}updateComplete(){return new Promise(t=>{if(this.isConnected)setTimeout(t,0);else{let r=new MutationObserver(()=>{this.isConnected&&(r.disconnect(),setTimeout(t,0))});r.observe(document.body,{childList:!0,subtree:!0})}})}getFormData(){return{email:document.getElementById(`${this.containerId}-email`)?.value,name:document.getElementById(`${this.containerId}-name`)?.value}}setFormData(t){if(t.email){let r=document.getElementById(`${this.containerId}-email`);r&&(r.value=t.email)}if(t.name){let r=document.getElementById(`${this.containerId}-name`);r&&(r.value=t.name)}}};customElements.get("recur-payment-form")||customElements.define("recur-payment-form",L)});var Q={};h(Q,{RecurCheckoutButton:()=>U});var U,ee=g(()=>{"use strict";U=class extends HTMLElement{constructor(){super();a(this,"_isLoading",!1);a(this,"handleClick",async t=>{if(t.preventDefault(),this._isLoading||this.hasAttribute("disabled"))return;let r=this.getAttribute("publishable-key"),i=this.getAttribute("product-id"),s=this.getAttribute("success-url"),n=this.getAttribute("cancel-url");if(!r){this.dispatchError("Missing required attribute: publishable-key");return}if(!i){this.dispatchError("Missing required attribute: product-id");return}if(!s){this.dispatchError("Missing required attribute: success-url");return}if(!n){this.dispatchError("Missing required attribute: cancel-url");return}this._isLoading=!0,this.render(),this.setupEventListeners();try{let o=await this.createCheckoutSession({publishableKey:r,productId:i,successUrl:this.resolveUrl(s),cancelUrl:this.resolveUrl(n),customerEmail:this.getAttribute("customer-email")||void 0,mode:this.getAttribute("mode")});this.dispatchEvent(new CustomEvent("checkout-started",{detail:{sessionId:o.id,url:o.url},bubbles:!0,composed:!0})),window.location.href=o.url}catch(o){this._isLoading=!1,this.render(),this.setupEventListeners(),this.dispatchError(o.message||"Failed to create checkout session")}});this.attachShadow({mode:"open"})}static get observedAttributes(){return["publishable-key","product-id","success-url","cancel-url","customer-email","mode","button-text","button-style","disabled"]}connectedCallback(){this.render(),this.setupEventListeners()}disconnectedCallback(){let t=this.shadowRoot?.querySelector("button");t&&t.removeEventListener("click",this.handleClick)}attributeChangedCallback(t,r,i){r!==i&&this.render()}render(){let t=this.getAttribute("button-text")||this.textContent?.trim()||"\u8A02\u95B1",r=this.getAttribute("button-style")||"primary",i=this.hasAttribute("disabled")||this._isLoading;this.shadowRoot.innerHTML=`
1223
+ `):(r.classList.remove("loading"),r.disabled=!1,r.innerHTML='<span id="'+this.containerId+'-submit-text">\u78BA\u8A8D\u4ED8\u6B3E</span>'))}resetButton(){this.setButtonLoading(!1)}showError(t){let r=this.querySelector(".recur-sdk__error-container");r||(r=document.createElement("div"),r.className="recur-sdk__error-container",r.style.cssText="margin-bottom: 16px;",this.insertBefore(r,this.firstChild));let i=document.createElement("recur-error-display");i.setAttribute("error",t),i.setAttribute("dismissible","true"),r.innerHTML="",r.appendChild(i),i.scrollIntoView({behavior:"smooth",block:"center"})}clearError(){let t=this.querySelector(".recur-sdk__error-container");t&&(t.innerHTML="")}updateComplete(){return new Promise(t=>{if(this.isConnected)setTimeout(t,0);else{let r=new MutationObserver(()=>{this.isConnected&&(r.disconnect(),setTimeout(t,0))});r.observe(document.body,{childList:!0,subtree:!0})}})}getFormData(){return{email:document.getElementById(`${this.containerId}-email`)?.value,name:document.getElementById(`${this.containerId}-name`)?.value}}setFormData(t){if(t.email){let r=document.getElementById(`${this.containerId}-email`);r&&(r.value=t.email)}if(t.name){let r=document.getElementById(`${this.containerId}-name`);r&&(r.value=t.name)}}};customElements.get("recur-payment-form")||customElements.define("recur-payment-form",L)});var re={};h(re,{RecurCheckoutButton:()=>U});var U,ie=b(()=>{"use strict";U=class extends HTMLElement{constructor(){super();a(this,"_isLoading",!1);a(this,"handleClick",async t=>{if(t.preventDefault(),this._isLoading||this.hasAttribute("disabled"))return;let r=this.getAttribute("publishable-key"),i=this.getAttribute("product-id"),s=this.getAttribute("success-url"),n=this.getAttribute("cancel-url");if(!r){this.dispatchError("Missing required attribute: publishable-key");return}if(!i){this.dispatchError("Missing required attribute: product-id");return}if(!s){this.dispatchError("Missing required attribute: success-url");return}if(!n){this.dispatchError("Missing required attribute: cancel-url");return}this._isLoading=!0,this.render(),this.setupEventListeners();try{let o=await this.createCheckoutSession({publishableKey:r,productId:i,successUrl:this.resolveUrl(s),cancelUrl:this.resolveUrl(n),customerEmail:this.getAttribute("customer-email")||void 0,mode:this.getAttribute("mode")});this.dispatchEvent(new CustomEvent("checkout-started",{detail:{sessionId:o.id,url:o.url},bubbles:!0,composed:!0})),window.location.href=o.url}catch(o){this._isLoading=!1,this.render(),this.setupEventListeners(),this.dispatchError(o.message||"Failed to create checkout session")}});this.attachShadow({mode:"open"})}static get observedAttributes(){return["publishable-key","product-id","success-url","cancel-url","customer-email","mode","button-text","button-style","disabled"]}connectedCallback(){this.render(),this.setupEventListeners()}disconnectedCallback(){let t=this.shadowRoot?.querySelector("button");t&&t.removeEventListener("click",this.handleClick)}attributeChangedCallback(t,r,i){r!==i&&this.render()}render(){let t=this.getAttribute("button-text")||this.textContent?.trim()||"\u8A02\u95B1",r=this.getAttribute("button-style")||"primary",i=this.hasAttribute("disabled")||this._isLoading;this.shadowRoot.innerHTML=`
1224
1224
  <style>
1225
1225
  :host {
1226
1226
  display: inline-block;
@@ -1319,7 +1319,7 @@
1319
1319
  ${this._isLoading?'<span class="spinner"></span>':""}
1320
1320
  <span>${this._isLoading?"\u8655\u7406\u4E2D...":t}</span>
1321
1321
  </button>
1322
- `}setupEventListeners(){let t=this.shadowRoot?.querySelector("button");t&&t.addEventListener("click",this.handleClick)}async createCheckoutSession(t){let r=this.getApiBaseUrl(),i={productId:t.productId,successUrl:t.successUrl,cancelUrl:t.cancelUrl};t.mode&&(i.mode=t.mode),t.customerEmail&&(i.customerEmail=t.customerEmail);let s=await fetch(`${r}/v1/checkout/sessions`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":t.publishableKey},body:JSON.stringify(i)});if(!s.ok){let n=await s.json().catch(()=>({}));throw new Error(n.error?.message||`HTTP ${s.status}: Failed to create checkout session`)}return s.json()}getApiBaseUrl(){let t=this.getAttribute("api-base-url");if(t)return t;if(typeof window<"u"){let r=window.location.hostname;if(r==="localhost"||r.includes(".test")||r.includes(".local")||r==="127.0.0.1")return`${window.location.protocol}//${window.location.host}/api`}return"https://api.recur.tw"}resolveUrl(t){return t.startsWith("/")?`${window.location.origin}${t}`:t}dispatchError(t){console.error("[recur-checkout]",t),this.dispatchEvent(new CustomEvent("checkout-error",{detail:{message:t},bubbles:!0,composed:!0}))}};typeof window<"u"&&!customElements.get("recur-checkout")&&customElements.define("recur-checkout",U)});var ye={};h(ye,{RecurCheckout:()=>w,RecurElements:()=>y,createElements:()=>$,default:()=>ge,init:()=>ie});async function he(){if(typeof window>"u"){console.warn("[Recur SDK] Cannot register Web Components: window is not defined");return}await Promise.all([Promise.resolve().then(()=>(B(),N)),Promise.resolve().then(()=>(O(),K)),Promise.resolve().then(()=>(j(),F)),Promise.resolve().then(()=>(q(),Y)),Promise.resolve().then(()=>(X(),V)),Promise.resolve().then(()=>(J(),W)),Promise.resolve().then(()=>(G(),Z)),Promise.resolve().then(()=>(ee(),Q))]);let e=["recur-loading-spinner","recur-success-message","recur-error-display","recur-skeleton-loader","recur-payment-form-skeleton","recur-toast","recur-toast-container","recur-payment-form","recur-checkout"].filter(t=>!customElements.get(t));e.length>0&&console.warn("[Recur SDK] The following components are not registered:",e)}typeof window<"u"&&he();var _=class{constructor(e){a(this,"config");if(!e.publishableKey)throw new Error("publishableKey is required");this.config={baseUrl:"https://api.recur.tw",...e}}async createSubscription(e){let{planId:t,customerName:r,customerEmail:i}=e;if(!t)throw new Error("planId is required");let s=await fetch(`${this.config.baseUrl}/v1/subscriptions`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":this.config.publishableKey},body:JSON.stringify({planId:t,customerName:r,customerEmail:i})});if(!s.ok){let n=await s.json().catch(()=>({}));throw{code:n.error||"CHECKOUT_FAILED",message:n.message||"Failed to initiate checkout",details:n}}return await s.json()}async fetchPlans(){let e=await fetch(`${this.config.baseUrl}/v1/plans`,{method:"GET",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":this.config.publishableKey}});if(!e.ok){let t=await e.json().catch(()=>({}));throw{code:t.error||"FETCH_PLANS_FAILED",message:t.message||"Failed to fetch plans",details:t}}return await e.json()}getConfig(){return{...this.config}}};var z=class{constructor(e,t){a(this,"config");a(this,"options");a(this,"container");a(this,"checkoutId",null);a(this,"sdkToken",null);a(this,"sdkEnv","S");a(this,"payuniSDK",null);a(this,"isFormValid",!1);if(this.config=e,this.options=t,typeof t.container=="string"){let r=document.querySelector(t.container);if(!r)throw new Error(`Container not found: ${t.container}`);this.container=r}else this.container=t.container}async render(){try{await this.initCheckout(),this.renderHTML(),await this.loadPayUniSDK(),await this.initPayUniSDK(),this.setupFormSubmit()}catch(e){this.handleError(e)}}getBaseUrl(){if(this.config.baseUrl)return this.config.baseUrl;if(typeof window<"u"){let e=window.location.hostname;if(e==="localhost"||e.includes(".test")||e.includes(".local")||e==="127.0.0.1")return`${window.location.protocol}//${window.location.host}/api`}return"https://api.recur.tw"}async initCheckout(){let e=this.getBaseUrl(),t=await fetch(`${e}/v1/checkouts`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":this.config.publishableKey},body:JSON.stringify({productId:this.options.planId,customerEmail:this.options.customerEmail,customerName:this.options.customerName})});if(!t.ok){let i=await t.json().catch(()=>({}));throw new Error(i.error||"Failed to initialize checkout")}let r=await t.json();this.checkoutId=r.checkout.id,this.sdkToken=r.sdkToken,this.sdkEnv=this.config.publishableKey.startsWith("pk_test_")?"S":"P"}renderHTML(){this.container.innerHTML=`
1322
+ `}setupEventListeners(){let t=this.shadowRoot?.querySelector("button");t&&t.addEventListener("click",this.handleClick)}async createCheckoutSession(t){let r=this.getApiBaseUrl(),i={productId:t.productId,successUrl:t.successUrl,cancelUrl:t.cancelUrl};t.mode&&(i.mode=t.mode),t.customerEmail&&(i.customerEmail=t.customerEmail);let s=await fetch(`${r}/v1/checkout/sessions`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":t.publishableKey},body:JSON.stringify(i)});if(!s.ok){let n=await s.json().catch(()=>({}));throw new Error(n.error?.message||`HTTP ${s.status}: Failed to create checkout session`)}return s.json()}getApiBaseUrl(){let t=this.getAttribute("api-base-url");if(t)return t;if(typeof window<"u"){let r=window.location.hostname;if(r==="localhost"||r.includes(".test")||r.includes(".local")||r==="127.0.0.1")return`${window.location.protocol}//${window.location.host}/api`}return"https://api.recur.tw"}resolveUrl(t){return t.startsWith("/")?`${window.location.origin}${t}`:t}dispatchError(t){console.error("[recur-checkout]",t),this.dispatchEvent(new CustomEvent("checkout-error",{detail:{message:t},bubbles:!0,composed:!0}))}};typeof window<"u"&&!customElements.get("recur-checkout")&&customElements.define("recur-checkout",U)});var ye={};h(ye,{RecurCheckout:()=>E,RecurElements:()=>y,createElements:()=>N,default:()=>ge,init:()=>ne});async function he(){if(typeof window>"u"){console.warn("[Recur SDK] Cannot register Web Components: window is not defined");return}await Promise.all([Promise.resolve().then(()=>(F(),O)),Promise.resolve().then(()=>(Y(),j)),Promise.resolve().then(()=>(V(),q)),Promise.resolve().then(()=>(W(),X)),Promise.resolve().then(()=>(Z(),J)),Promise.resolve().then(()=>(Q(),G)),Promise.resolve().then(()=>(te(),ee)),Promise.resolve().then(()=>(ie(),re))]);let e=["recur-loading-spinner","recur-success-message","recur-error-display","recur-skeleton-loader","recur-payment-form-skeleton","recur-toast","recur-toast-container","recur-payment-form","recur-checkout"].filter(t=>!customElements.get(t));e.length>0&&console.warn("[Recur SDK] The following components are not registered:",e)}typeof window<"u"&&he();var D=class{constructor(e){a(this,"config");if(!e.publishableKey)throw new Error("publishableKey is required");this.config={baseUrl:"https://api.recur.tw",...e}}async createSubscription(e){let{planId:t,customerName:r,customerEmail:i}=e;if(!t)throw new Error("planId is required");let s=await fetch(`${this.config.baseUrl}/v1/subscriptions`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":this.config.publishableKey},body:JSON.stringify({planId:t,customerName:r,customerEmail:i})});if(!s.ok){let n=await s.json().catch(()=>({}));throw{code:n.error||"CHECKOUT_FAILED",message:n.message||"Failed to initiate checkout",details:n}}return await s.json()}async fetchProducts(e){let t=new URL(`${this.config.baseUrl}/v1/products`);e?.type&&t.searchParams.set("type",e.type);let r=await fetch(t.toString(),{method:"GET",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":this.config.publishableKey}});if(!r.ok){let i=await r.json().catch(()=>({}));throw{code:i.error||"FETCH_PRODUCTS_FAILED",message:i.message||"Failed to fetch products",details:i}}return await r.json()}async fetchPlans(){return{plans:(await this.fetchProducts({type:"SUBSCRIPTION"})).products}}getConfig(){return{...this.config}}};var _=class{constructor(e,t){a(this,"config");a(this,"options");a(this,"container");a(this,"checkoutId",null);a(this,"sdkToken",null);a(this,"sdkEnv","S");a(this,"payuniSDK",null);a(this,"isFormValid",!1);if(this.config=e,this.options=t,typeof t.container=="string"){let r=document.querySelector(t.container);if(!r)throw new Error(`Container not found: ${t.container}`);this.container=r}else this.container=t.container}async render(){try{await this.initCheckout(),this.renderHTML(),await this.loadPayUniSDK(),await this.initPayUniSDK(),this.setupFormSubmit()}catch(e){this.handleError(e)}}getBaseUrl(){if(this.config.baseUrl)return this.config.baseUrl;if(typeof window<"u"){let e=window.location.hostname;if(e==="localhost"||e.includes(".test")||e.includes(".local")||e==="127.0.0.1")return`${window.location.protocol}//${window.location.host}/api`}return"https://api.recur.tw"}async initCheckout(){let e=this.getBaseUrl(),t=await fetch(`${e}/v1/checkouts`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":this.config.publishableKey},body:JSON.stringify({productId:this.options.planId,customerEmail:this.options.customerEmail,customerName:this.options.customerName})});if(!t.ok){let i=await t.json().catch(()=>({}));throw new Error(i.error||"Failed to initialize checkout")}let r=await t.json();this.checkoutId=r.checkout.id,this.sdkToken=r.sdkToken,this.sdkEnv=this.config.publishableKey.startsWith("pk_test_")?"S":"P"}renderHTML(){this.container.innerHTML=`
1323
1323
  <div class="recur-embedded-checkout" style="max-width: 400px; margin: 0 auto; padding: 20px; font-family: system-ui, -apple-system, sans-serif;">
1324
1324
  <div class="recur-checkout-header" style="margin-bottom: 24px;">
1325
1325
  <h2 style="margin: 0 0 8px 0; font-size: 20px; font-weight: 600; color: #111;">Subscribe</h2>
@@ -1389,21 +1389,21 @@
1389
1389
  </p>
1390
1390
  </form>
1391
1391
  </div>
1392
- `}async loadPayUniSDK(){return new Promise((e,t)=>{if(window.UniPayment){e();return}let r=document.createElement("script");r.src=this.sdkEnv==="P"?"https://vendor.payuni.com.tw/sdk/uni-payment.js":"https://sandbox-vendor.payuni.com.tw/sdk/uni-payment.js",r.async=!0,r.onload=()=>e(),r.onerror=()=>t(new Error("Failed to load PAYUNi SDK")),document.head.appendChild(r)})}async initPayUniSDK(){if(!this.sdkToken)throw new Error("SDK token not initialized");let e=window.UniPayment;if(!e)throw new Error("PAYUNi SDK not loaded");let t={env:this.sdkEnv,useInst:!1,elements:{CardNo:"recur-card-number",CardExp:"recur-card-exp",CardCvc:"recur-card-cvc"},style:{color:"#111827",errorColor:"#dc2626",fontSize:"14px",fontWeight:"400",lineHeight:"24px"}};this.payuniSDK=e.createSession(this.sdkToken,t),await this.payuniSDK.start(),this.payuniSDK.onUpdate(r=>{let i=r?.status;i&&(this.isFormValid=i.CardNo===!0&&i.CardExp===!0&&i.CardCvc===!0,this.updateSubmitButton())})}updateSubmitButton(){let e=document.getElementById("recur-submit-btn");e&&(e.disabled=!this.isFormValid,e.style.opacity=this.isFormValid?"1":"0.5",e.style.cursor=this.isFormValid?"pointer":"not-allowed")}setupFormSubmit(){let e=document.getElementById("recur-checkout-form");e&&e.addEventListener("submit",async t=>{t.preventDefault(),await this.handleSubmit()})}async handleSubmit(){try{this.showLoading(!0),this.hideError();let e=document.getElementById("recur-email").value,t=document.getElementById("recur-name").value;if(!e||!t)throw new Error("Please fill in all required fields");if(!this.checkoutId)throw new Error("Checkout session not initialized");if(!this.payuniSDK)throw new Error("Payment system not initialized");let r=await this.payuniSDK.getTradeResult();if(r.Status!=="SUCCESS")throw new Error(r.Message||"Card validation failed");let i=r.EncryptInfo||r.creditToken;if(!i)throw console.error("[Recur SDK] Missing payment token. Available fields:",Object.keys(r)),new Error("Missing payment token");let s=this.getBaseUrl(),n=await fetch(`${s}/v1/checkouts/${this.checkoutId}/pay`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":this.config.publishableKey},body:JSON.stringify({creditToken:i,timestamp:r.HashTimestamp||r.timestamp})});if(!n.ok){let d=await n.json().catch(()=>({}));throw new Error(d.error||"Failed to process payment")}let o=await n.json(),l={subscription:{id:o.subscription?.id||o.charge?.id||"",status:o.success?"active":"failed",planId:this.options.planId,planName:"",amount:o.charge?.amount||0,billingPeriod:o.subscription?.billingPeriod||"MONTHLY",trialDays:null},subscriber:{id:"",email:document.getElementById("recur-email")?.value||"",name:document.getElementById("recur-name")?.value||""},nextSteps:{getSdkToken:"",completeSubscription:""}};this.options.onSuccess&&this.options.onSuccess(l),this.showSuccess()}catch(e){this.handleError(e)}finally{this.showLoading(!1)}}showLoading(e){let t=document.getElementById("recur-submit-btn");t&&(t.disabled=e,t.textContent=e?"Processing...":"Subscribe Now")}showError(e){let t=document.getElementById("recur-error");t&&(t.textContent=e,t.style.display="block")}hideError(){let e=document.getElementById("recur-error");e&&(e.style.display="none")}showSuccess(){this.container.innerHTML="";let e=document.createElement("recur-success-message");e.setAttribute("title","Subscription Complete!"),e.setAttribute("message","Thank you for subscribing. You will receive a confirmation email shortly."),this.container.appendChild(e)}handleError(e){let t=e?.message||"An error occurred";this.showError(t);let r={code:"CHECKOUT_ERROR",message:t};this.options.onError&&this.options.onError(r)}};var y=class{constructor(e){a(this,"publishableKey");a(this,"baseUrl");a(this,"embedUrl");a(this,"iframe",null);a(this,"container",null);a(this,"sessionId",null);a(this,"timestamp",null);a(this,"creditToken",null);a(this,"cardToken",null);a(this,"cardTimestamp",null);a(this,"eventHandlers",new Map);typeof e=="string"?(this.publishableKey=e,this.baseUrl=this.getDefaultBaseUrl(),this.embedUrl=this.getDefaultEmbedUrl()):(this.publishableKey=e.publishableKey,this.baseUrl=e.baseUrl||this.getDefaultBaseUrl(),this.embedUrl=e.embedUrl||this.getDefaultEmbedUrl()),window.addEventListener("message",this.handleMessage.bind(this))}async mount(e){let t=typeof e=="string"?document.querySelector(e):e;if(!t)throw new Error(`Container not found: ${e}`);return this.container=t,this.iframe=document.createElement("iframe"),this.iframe.src=`${this.embedUrl}/elements?key=${encodeURIComponent(this.publishableKey)}`,this.iframe.style.cssText=`
1392
+ `}async loadPayUniSDK(){return new Promise((e,t)=>{if(window.UniPayment){e();return}let r=document.createElement("script");r.src=this.sdkEnv==="P"?"https://vendor.payuni.com.tw/sdk/uni-payment.js":"https://sandbox-vendor.payuni.com.tw/sdk/uni-payment.js",r.async=!0,r.onload=()=>e(),r.onerror=()=>t(new Error("Failed to load PAYUNi SDK")),document.head.appendChild(r)})}async initPayUniSDK(){if(!this.sdkToken)throw new Error("SDK token not initialized");let e=window.UniPayment;if(!e)throw new Error("PAYUNi SDK not loaded");let t={env:this.sdkEnv,useInst:!1,elements:{CardNo:"recur-card-number",CardExp:"recur-card-exp",CardCvc:"recur-card-cvc"},style:{color:"#111827",errorColor:"#dc2626",fontSize:"14px",fontWeight:"400",lineHeight:"24px"}};this.payuniSDK=e.createSession(this.sdkToken,t),await this.payuniSDK.start(),this.payuniSDK.onUpdate(r=>{let i=r?.status;i&&(this.isFormValid=i.CardNo===!0&&i.CardExp===!0&&i.CardCvc===!0,this.updateSubmitButton())})}updateSubmitButton(){let e=document.getElementById("recur-submit-btn");e&&(e.disabled=!this.isFormValid,e.style.opacity=this.isFormValid?"1":"0.5",e.style.cursor=this.isFormValid?"pointer":"not-allowed")}setupFormSubmit(){let e=document.getElementById("recur-checkout-form");e&&e.addEventListener("submit",async t=>{t.preventDefault(),await this.handleSubmit()})}async handleSubmit(){try{this.showLoading(!0),this.hideError();let e=document.getElementById("recur-email").value,t=document.getElementById("recur-name").value;if(!e||!t)throw new Error("Please fill in all required fields");if(!this.checkoutId)throw new Error("Checkout session not initialized");if(!this.payuniSDK)throw new Error("Payment system not initialized");let r=await this.payuniSDK.getTradeResult();if(r.Status!=="SUCCESS")throw new Error(r.Message||"Card validation failed");let i=r.EncryptInfo||r.creditToken;if(!i)throw console.error("[Recur SDK] Missing payment token. Available fields:",Object.keys(r)),new Error("Missing payment token");let s=this.getBaseUrl(),n=await fetch(`${s}/v1/checkouts/${this.checkoutId}/pay`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":this.config.publishableKey},body:JSON.stringify({creditToken:i,timestamp:r.HashTimestamp||r.timestamp})});if(!n.ok){let l=await n.json().catch(()=>({}));throw new Error(l.error||"Failed to process payment")}let o=await n.json(),d={subscription:{id:o.subscription?.id||o.charge?.id||"",status:o.success?"active":"failed",planId:this.options.planId,planName:"",amount:o.charge?.amount||0,billingPeriod:o.subscription?.billingPeriod||"MONTHLY",trialDays:null},subscriber:{id:"",email:document.getElementById("recur-email")?.value||"",name:document.getElementById("recur-name")?.value||""},nextSteps:{getSdkToken:"",completeSubscription:""}};this.options.onSuccess&&this.options.onSuccess(d),this.showSuccess()}catch(e){this.handleError(e)}finally{this.showLoading(!1)}}showLoading(e){let t=document.getElementById("recur-submit-btn");t&&(t.disabled=e,t.textContent=e?"Processing...":"Subscribe Now")}showError(e){let t=document.getElementById("recur-error");t&&(t.textContent=e,t.style.display="block")}hideError(){let e=document.getElementById("recur-error");e&&(e.style.display="none")}showSuccess(){this.container.innerHTML="";let e=document.createElement("recur-success-message");e.setAttribute("title","Subscription Complete!"),e.setAttribute("message","Thank you for subscribing. You will receive a confirmation email shortly."),this.container.appendChild(e)}handleError(e){let t=e?.message||"An error occurred";this.showError(t);let r={code:"CHECKOUT_ERROR",message:t};this.options.onError&&this.options.onError(r)}};var y=class{constructor(e){a(this,"publishableKey");a(this,"baseUrl");a(this,"embedUrl");a(this,"iframe",null);a(this,"container",null);a(this,"sessionId",null);a(this,"timestamp",null);a(this,"creditToken",null);a(this,"cardToken",null);a(this,"cardTimestamp",null);a(this,"eventHandlers",new Map);typeof e=="string"?(this.publishableKey=e,this.baseUrl=this.getDefaultBaseUrl(),this.embedUrl=this.getDefaultEmbedUrl()):(this.publishableKey=e.publishableKey,this.baseUrl=e.baseUrl||this.getDefaultBaseUrl(),this.embedUrl=e.embedUrl||this.getDefaultEmbedUrl()),window.addEventListener("message",this.handleMessage.bind(this))}async mount(e){let t=typeof e=="string"?document.querySelector(e):e;if(!t)throw new Error(`Container not found: ${e}`);return this.container=t,this.iframe=document.createElement("iframe"),this.iframe.src=`${this.embedUrl}/elements?key=${encodeURIComponent(this.publishableKey)}`,this.iframe.style.cssText=`
1393
1393
  width: 100%;
1394
1394
  border: none;
1395
1395
  min-height: 200px;
1396
1396
  display: block;
1397
1397
  user-select: none;
1398
1398
  transition: height 0.35s ease, opacity 0.4s ease 0.1s;
1399
- `.replace(/\s+/g," ").trim(),this.iframe.allow="payment *",this.iframe.setAttribute("title","Secure payment input frame"),this.iframe.setAttribute("role","presentation"),this.iframe.setAttribute("scrolling","no"),this.iframe.setAttribute("frameBorder","0"),this.container.innerHTML="",this.container.appendChild(this.iframe),new Promise((r,i)=>{let s=setTimeout(()=>{i(new Error("Elements initialization timeout"))},3e4),n=()=>{clearTimeout(s),this.off("ready",n),r()},o=l=>{clearTimeout(s),this.off("error",o),i(new Error(l.message||"Elements initialization failed"))};this.on("ready",n),this.on("error",o)})}on(e,t){this.eventHandlers.has(e)||this.eventHandlers.set(e,new Set),this.eventHandlers.get(e).add(t)}off(e,t){let r=this.eventHandlers.get(e);r&&r.delete(t)}emit(e,t){let r=this.eventHandlers.get(e);r&&r.forEach(i=>i(t))}handleMessage(e){if(!this.isValidOrigin(e.origin))return;let{type:t,data:r}=e.data||{};switch(t){case"RECUR_ELEMENTS_READY":this.sessionId=r.sessionId,this.timestamp=r.timestamp,this.creditToken=r.creditToken,this.emit("ready",r);break;case"RECUR_CARD_VALID":this.emit("change",{valid:r.valid});break;case"RECUR_CARD_TOKENIZED":this.cardToken=r.cardToken,this.cardTimestamp=r.timestamp,this.emit("tokenized",r);break;case"RECUR_PAYMENT_ERROR":this.emit("error",r);break;case"RECUR_RESIZE":this.iframe&&r?.height&&(this.iframe.style.height=`${r.height}px`);break}}isValidOrigin(e){try{let t=new URL(this.embedUrl).origin;return e===t}catch{return!1}}async tokenize(){if(!this.iframe||!this.iframe.contentWindow)throw new Error("Elements not mounted");let e=new URL(this.embedUrl).origin;return this.iframe.contentWindow.postMessage({type:"RECUR_TOKENIZE"},e),new Promise((t,r)=>{let i=setTimeout(()=>{r(new Error("Tokenization timeout"))},6e4),s=o=>{clearTimeout(i),this.off("tokenized",s),t(o)},n=o=>{clearTimeout(i),this.off("error",n),r(new Error(o.message||"Tokenization failed"))};this.on("tokenized",s),this.on("error",n)})}async confirmPayment(e){if(!this.sessionId||!this.cardToken)throw new Error("Card not tokenized. Call tokenize() first.");let t=await fetch(`${this.baseUrl}/api/v1/elements/confirm`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":this.publishableKey},body:JSON.stringify({sessionId:this.sessionId,sdkToken:this.cardToken,timestamp:this.cardTimestamp,creditToken:this.creditToken,productId:e.productId,email:e.email,name:e.name,metadata:e.metadata})}),r=await t.json();return t.ok?r:{status:"failed",message:r.error||r.message||"Payment failed",canRetry:r.canRetry??!0}}getDefaultBaseUrl(){if(typeof window>"u")return"https://app.recur.tw";let e=window.location.hostname;return e==="localhost"||e.includes(".test")||e.includes(".local")||e==="127.0.0.1"?`${window.location.protocol}//${e}:3000`:"https://app.recur.tw"}getDefaultEmbedUrl(){if(typeof window>"u")return"https://embed.recur.tw";let e=window.location.hostname;return e==="localhost"||e.includes(".test")||e.includes(".local")||e==="127.0.0.1"?`${window.location.protocol}//${e}:3002`:"https://embed.recur.tw"}destroy(){this.iframe&&this.iframe.parentNode&&this.iframe.parentNode.removeChild(this.iframe),this.iframe=null,this.container=null,this.sessionId=null,this.timestamp=null,this.creditToken=null,this.cardToken=null,this.cardTimestamp=null,this.eventHandlers.clear(),window.removeEventListener("message",this.handleMessage.bind(this))}};function $(c){return new y(c)}var fe="https://vendor.payuni.com.tw/sdk/uni-payment.js",be="https://sandbox-vendor.payuni.com.tw/sdk/uni-payment.js",te=!1,D=!1,x=null;async function re(c=!1){return te&&window.UniPayment?Promise.resolve():(D&&x||(D=!0,x=new Promise((e,t)=>{let r=document.createElement("script");r.src=c?be:fe,r.async=!0,r.onload=()=>{te=!0,D=!1,e()},r.onerror=()=>{D=!1,x=null,t(new Error("Failed to load PAYUNi SDK"))},document.head.appendChild(r)})),x)}var w=class{constructor(e){a(this,"core");a(this,"currentModal",null);a(this,"currentIframe",null);a(this,"currentModalOverlay",null);this.core=new _(e)}async fetchPlans(){return await this.core.fetchPlans()}async createEmbeddedCheckout(e){let t=this.core.getConfig();await new z(t,e).render()}async redirectToCheckout(e){let t=this.core.getConfig(),r=()=>{if(typeof window<"u"){let l=window.location.hostname;if(l==="localhost"||l.includes(".test")||l.includes(".local")||l==="127.0.0.1")return`${window.location.protocol}//${window.location.host}/api`}return"https://api.recur.tw"},i=t.baseUrl||r(),s={productId:e.productId,successUrl:e.successUrl,cancelUrl:e.cancelUrl};e.mode&&(s.mode=e.mode),e.customerEmail&&(s.customerEmail=e.customerEmail),e.externalCustomerId&&(s.externalCustomerId=e.externalCustomerId);let n=await fetch(`${i}/v1/checkout/sessions`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":t.publishableKey},body:JSON.stringify(s)});if(!n.ok){let l=await n.json().catch(()=>({}));throw new Error(l.error?.message||"Failed to create checkout session")}let o=await n.json();window.location.href=o.url}async createCheckoutSession(e){let t=this.core.getConfig(),r=()=>{if(typeof window<"u"){let o=window.location.hostname;if(o==="localhost"||o.includes(".test")||o.includes(".local")||o==="127.0.0.1")return`${window.location.protocol}//${window.location.host}/api`}return"https://api.recur.tw"},i=t.baseUrl||r(),s={productId:e.productId,successUrl:e.successUrl,cancelUrl:e.cancelUrl};e.mode&&(s.mode=e.mode),e.customerEmail&&(s.customerEmail=e.customerEmail),e.externalCustomerId&&(s.externalCustomerId=e.externalCustomerId);let n=await fetch(`${i}/v1/checkout/sessions`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":t.publishableKey},body:JSON.stringify(s)});if(!n.ok){let o=await n.json().catch(()=>({}));throw new Error(o.error?.message||"Failed to create checkout session")}return n.json()}async checkout(e){let t=this.core.getConfig(),r=null;try{if(console.log("[Recur SDK] Starting checkout flow...",{planId:e.planId,mode:e.mode}),!e.planId)throw new Error("planId is required");if(!t.publishableKey)throw new Error("publishableKey is required");if(!e.customerName)throw new Error("customerName is required");if(!e.customerEmail)throw new Error("customerEmail is required");let i=this.getBaseUrl();console.log("[Recur SDK] Base URL:",i);let s={"Content-Type":"application/json","X-Recur-Publishable-Key":t.publishableKey},n=e.mode||"modal",o=null;if(n==="modal"){let u=this.createModalWithSkeleton(e.onClose);r=u.overlay,o=u.container}else if(n==="iframe"){if(o=this.getEmbeddedContainer(e.container),!o)throw new Error("Container is required for iframe mode");o.innerHTML="";let u=document.createElement("recur-payment-form-skeleton");o.appendChild(u)}console.log("[Recur SDK] Step 1: Creating checkout session...");let l=await fetch(`${i}/v1/checkouts`,{method:"POST",headers:s,body:JSON.stringify({productId:e.planId,customerName:e.customerName,customerEmail:e.customerEmail,externalCustomerId:e.externalCustomerId})});if(!l.ok){let u=await l.json().catch(()=>({}));console.error("[Recur SDK] Failed to create checkout:",u);let A=u.details||u.error||"Failed to create checkout";throw new Error(A)}let d=await l.json();if(console.log("[Recur SDK] Checkout created successfully:",d),e.onSuccess?.(d),n==="redirect"){let u=`https://checkout.recur.tw/${d.checkout.id}`;console.log("[Recur SDK] Redirecting to hosted checkout:",u),window.location.href=u;return}if(console.log("[Recur SDK] Step 2: Extracting SDK token..."),!d.sdkToken)throw new Error("SDK token missing from checkout response");console.log("[Recur SDK] Step 3: Loading PAYUNi SDK...");let v=!0;if(await re(v),console.log("[Recur SDK] PAYUNi SDK loaded successfully"),!window.UniPayment)throw new Error("PAYUNi SDK failed to load");if(console.log("[Recur SDK] Step 4: Rendering payment form..."),!o)throw new Error("Payment container not available");o.innerHTML="";let m=document.createElement("recur-payment-form");if(m.setAttribute("container-id",o.id||"recur-payment-container"),e.customerName&&m.setAttribute("customer-name",e.customerName),e.customerEmail&&m.setAttribute("customer-email",e.customerEmail),d.plan?.name&&m.setAttribute("plan-name",d.plan.name),d.checkout?.amount&&m.setAttribute("amount",d.checkout.amount.toString()),d.plan?.billingPeriod&&m.setAttribute("billing-period",d.plan.billingPeriod),m.setAttribute("custom-styles",`
1399
+ `.replace(/\s+/g," ").trim(),this.iframe.allow="payment *",this.iframe.setAttribute("title","Secure payment input frame"),this.iframe.setAttribute("role","presentation"),this.iframe.setAttribute("scrolling","no"),this.iframe.setAttribute("frameBorder","0"),this.container.innerHTML="",this.container.appendChild(this.iframe),new Promise((r,i)=>{let s=setTimeout(()=>{i(new Error("Elements initialization timeout"))},3e4),n=()=>{clearTimeout(s),this.off("ready",n),r()},o=d=>{clearTimeout(s),this.off("error",o),i(new Error(d.message||"Elements initialization failed"))};this.on("ready",n),this.on("error",o)})}on(e,t){this.eventHandlers.has(e)||this.eventHandlers.set(e,new Set),this.eventHandlers.get(e).add(t)}off(e,t){let r=this.eventHandlers.get(e);r&&r.delete(t)}emit(e,t){let r=this.eventHandlers.get(e);r&&r.forEach(i=>i(t))}handleMessage(e){if(!this.isValidOrigin(e.origin))return;let{type:t,data:r}=e.data||{};switch(t){case"RECUR_ELEMENTS_READY":this.sessionId=r.sessionId,this.timestamp=r.timestamp,this.creditToken=r.creditToken,this.emit("ready",r);break;case"RECUR_CARD_VALID":this.emit("change",{valid:r.valid});break;case"RECUR_CARD_TOKENIZED":this.cardToken=r.cardToken,this.cardTimestamp=r.timestamp,this.emit("tokenized",r);break;case"RECUR_PAYMENT_ERROR":this.emit("error",r);break;case"RECUR_RESIZE":this.iframe&&r?.height&&(this.iframe.style.height=`${r.height}px`);break}}isValidOrigin(e){try{let t=new URL(this.embedUrl).origin;return e===t}catch{return!1}}async tokenize(){if(!this.iframe||!this.iframe.contentWindow)throw new Error("Elements not mounted");let e=new URL(this.embedUrl).origin;return this.iframe.contentWindow.postMessage({type:"RECUR_TOKENIZE"},e),new Promise((t,r)=>{let i=setTimeout(()=>{r(new Error("Tokenization timeout"))},6e4),s=o=>{clearTimeout(i),this.off("tokenized",s),t(o)},n=o=>{clearTimeout(i),this.off("error",n),r(new Error(o.message||"Tokenization failed"))};this.on("tokenized",s),this.on("error",n)})}async confirmPayment(e){if(!this.sessionId||!this.cardToken)throw new Error("Card not tokenized. Call tokenize() first.");let t=await fetch(`${this.baseUrl}/api/v1/elements/confirm`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":this.publishableKey},body:JSON.stringify({sessionId:this.sessionId,sdkToken:this.cardToken,timestamp:this.cardTimestamp,creditToken:this.creditToken,productId:e.productId,email:e.email,name:e.name,metadata:e.metadata})}),r=await t.json();return t.ok?r:{status:"failed",message:r.error||r.message||"Payment failed",canRetry:r.canRetry??!0}}getDefaultBaseUrl(){if(typeof window>"u")return"https://app.recur.tw";let e=window.location.hostname;return e==="localhost"||e.includes(".test")||e.includes(".local")||e==="127.0.0.1"?`${window.location.protocol}//${e}:3000`:"https://app.recur.tw"}getDefaultEmbedUrl(){if(typeof window>"u")return"https://embed.recur.tw";let e=window.location.hostname;return e==="localhost"||e.includes(".test")||e.includes(".local")||e==="127.0.0.1"?`${window.location.protocol}//${e}:3002`:"https://embed.recur.tw"}destroy(){this.iframe&&this.iframe.parentNode&&this.iframe.parentNode.removeChild(this.iframe),this.iframe=null,this.container=null,this.sessionId=null,this.timestamp=null,this.creditToken=null,this.cardToken=null,this.cardTimestamp=null,this.eventHandlers.clear(),window.removeEventListener("message",this.handleMessage.bind(this))}};function N(c){return new y(c)}var fe="https://vendor.payuni.com.tw/sdk/uni-payment.js",be="https://sandbox-vendor.payuni.com.tw/sdk/uni-payment.js",se=!1,z=!1,w=null;async function oe(c=!1){return se&&window.UniPayment?Promise.resolve():(z&&w||(z=!0,w=new Promise((e,t)=>{let r=document.createElement("script");r.src=c?be:fe,r.async=!0,r.onload=()=>{se=!0,z=!1,e()},r.onerror=()=>{z=!1,w=null,t(new Error("Failed to load PAYUNi SDK"))},document.head.appendChild(r)})),w)}var E=class{constructor(e){a(this,"core");a(this,"currentModal",null);a(this,"currentIframe",null);a(this,"currentModalOverlay",null);this.core=new D(e)}async fetchProducts(e){return await this.core.fetchProducts(e)}async fetchPlans(){return await this.core.fetchPlans()}async createEmbeddedCheckout(e){let t=this.core.getConfig();await new _(t,e).render()}async redirectToCheckout(e){let t=this.core.getConfig(),r=()=>{if(typeof window<"u"){let d=window.location.hostname;if(d==="localhost"||d.includes(".test")||d.includes(".local")||d==="127.0.0.1")return`${window.location.protocol}//${window.location.host}/api`}return"https://api.recur.tw"},i=t.baseUrl||r(),s={productId:e.productId,successUrl:e.successUrl,cancelUrl:e.cancelUrl};e.mode&&(s.mode=e.mode),e.customerEmail&&(s.customerEmail=e.customerEmail),e.externalCustomerId&&(s.externalCustomerId=e.externalCustomerId);let n=await fetch(`${i}/v1/checkout/sessions`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":t.publishableKey},body:JSON.stringify(s)});if(!n.ok){let d=await n.json().catch(()=>({}));throw new Error(d.error?.message||"Failed to create checkout session")}let o=await n.json();window.location.href=o.url}async createCheckoutSession(e){let t=this.core.getConfig(),r=()=>{if(typeof window<"u"){let o=window.location.hostname;if(o==="localhost"||o.includes(".test")||o.includes(".local")||o==="127.0.0.1")return`${window.location.protocol}//${window.location.host}/api`}return"https://api.recur.tw"},i=t.baseUrl||r(),s={productId:e.productId,successUrl:e.successUrl,cancelUrl:e.cancelUrl};e.mode&&(s.mode=e.mode),e.customerEmail&&(s.customerEmail=e.customerEmail),e.externalCustomerId&&(s.externalCustomerId=e.externalCustomerId);let n=await fetch(`${i}/v1/checkout/sessions`,{method:"POST",headers:{"Content-Type":"application/json","X-Recur-Publishable-Key":t.publishableKey},body:JSON.stringify(s)});if(!n.ok){let o=await n.json().catch(()=>({}));throw new Error(o.error?.message||"Failed to create checkout session")}return n.json()}async checkout(e){let t=this.core.getConfig(),r=null;try{if(console.log("[Recur SDK] Starting checkout flow...",{planId:e.planId,mode:e.mode}),!e.planId)throw new Error("planId is required");if(!t.publishableKey)throw new Error("publishableKey is required");if(!e.customerName)throw new Error("customerName is required");if(!e.customerEmail)throw new Error("customerEmail is required");let i=this.getBaseUrl();console.log("[Recur SDK] Base URL:",i);let s={"Content-Type":"application/json","X-Recur-Publishable-Key":t.publishableKey},n=e.mode||"modal",o=null;if(n==="modal"){let u=this.createModalWithSkeleton(e.onClose);r=u.overlay,o=u.container}else if(n==="iframe"){if(o=this.getEmbeddedContainer(e.container),!o)throw new Error("Container is required for iframe mode");o.innerHTML="";let u=document.createElement("recur-payment-form-skeleton");o.appendChild(u)}console.log("[Recur SDK] Step 1: Creating checkout session...");let d=await fetch(`${i}/v1/checkouts`,{method:"POST",headers:s,body:JSON.stringify({productId:e.planId,customerName:e.customerName,customerEmail:e.customerEmail,externalCustomerId:e.externalCustomerId})});if(!d.ok){let u=await d.json().catch(()=>({}));console.error("[Recur SDK] Failed to create checkout:",u);let A=u.details||u.error||"Failed to create checkout";throw new Error(A)}let l=await d.json();if(console.log("[Recur SDK] Checkout created successfully:",l),e.onSuccess?.(l),n==="redirect"){let u=`https://checkout.recur.tw/${l.checkout.id}`;console.log("[Recur SDK] Redirecting to hosted checkout:",u),window.location.href=u;return}if(console.log("[Recur SDK] Step 2: Extracting SDK token..."),!l.sdkToken)throw new Error("SDK token missing from checkout response");console.log("[Recur SDK] Step 3: Loading PAYUNi SDK...");let v=!0;if(await oe(v),console.log("[Recur SDK] PAYUNi SDK loaded successfully"),!window.UniPayment)throw new Error("PAYUNi SDK failed to load");if(console.log("[Recur SDK] Step 4: Rendering payment form..."),!o)throw new Error("Payment container not available");o.innerHTML="";let m=document.createElement("recur-payment-form");if(m.setAttribute("container-id",o.id||"recur-payment-container"),e.customerName&&m.setAttribute("customer-name",e.customerName),e.customerEmail&&m.setAttribute("customer-email",e.customerEmail),l.plan?.name&&m.setAttribute("plan-name",l.plan.name),l.checkout?.amount&&m.setAttribute("amount",l.checkout.amount.toString()),l.plan?.billingPeriod&&m.setAttribute("billing-period",l.plan.billingPeriod),m.setAttribute("custom-styles",`
1400
1400
  .form-input-focus {
1401
1401
  border-color: var(--ring, hsl(215 16% 47%)) !important;
1402
1402
  outline: 0 !important;
1403
1403
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent) !important;
1404
1404
  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out !important;
1405
1405
  }
1406
- `),o.appendChild(m),await new Promise(u=>setTimeout(u,100)),console.log("[Recur SDK] Step 5: Initializing PAYUNi SDK..."),await m.initializePayment(d.sdkToken,v?"SANDBOX":"PRODUCTION")===null){console.log("[Recur SDK] Initialization aborted");return}console.log("[Recur SDK] PAYUNi SDK initialized successfully"),console.log("[Recur SDK] Step 6: Setting up submit handler..."),m.addEventListener("submit",(async u=>{console.log("[Recur SDK] Form submitted");let A=u,{paymentSession:se}=A.detail;try{let b=await se.getTradeResult();console.log("[Recur SDK] Trade result received"),console.log("[Recur SDK] Executing payment...");let E=b.EncryptInfo||b.creditToken,oe=b.HashTimestamp||b.timestamp;if(!E)throw new Error("Missing payment token from PAYUNi SDK");let ne=d.checkout.productType==="SUBSCRIPTION"?{creditToken:E,timestamp:oe}:{},H=await fetch(`${i}/v1/checkouts/${d.checkout.id}/pay`,{method:"POST",headers:s,body:JSON.stringify(ne)});if(!H.ok){let ae=await H.json().catch(()=>({}));throw new Error(ae.error||"Failed to execute payment")}let p=await H.json();if(console.log("[Recur SDK] Payment executed:",p),p.requires3D&&p.redirectUrl){console.log("[Recur SDK] 3D verification required"),window.location.href=p.redirectUrl;return}e.onPaymentComplete&&(p.subscription?e.onPaymentComplete({id:p.subscription.id,status:p.subscription.status,planId:d.checkout.productId,amount:d.checkout.amount,billingPeriod:p.subscription.billingPeriod,currentPeriodStart:p.subscription.currentPeriodStart,currentPeriodEnd:p.subscription.currentPeriodEnd}):e.onPaymentComplete({id:p.checkout.id,status:p.checkout.status,planId:d.checkout.productId,amount:d.checkout.amount,billingPeriod:d.checkout.productType})),console.log("[Recur SDK] Checkout flow completed successfully!"),m.resetButton?.(),r&&r.remove()}catch(b){console.error("[Recur SDK] Payment error:",b);let E={code:"PAYMENT_FAILED",message:b instanceof Error?b.message:"Payment failed"};e.onError?.(E),m.resetButton?.()}})),console.log("[Recur SDK] Checkout flow initialized, waiting for user input...")}catch(i){console.error("[Recur SDK] Checkout error:",i),r&&r.remove();let s={code:"CHECKOUT_ERROR",message:i instanceof Error?i.message:"An unknown error occurred"};throw e.onError?.(s),i}}getBaseUrl(){let e=this.core.getConfig();if(e.baseUrl)return e.baseUrl;if(typeof window<"u"){let t=window.location.hostname;if(t==="localhost"||t.includes(".test")||t.includes(".local")||t==="127.0.0.1")return`${window.location.protocol}//${window.location.host}/api`}return"https://api.recur.tw"}createModalWithSkeleton(e){this.closeModal();let t=document.createElement("div");t.id="recur-modal-overlay",t.style.cssText=`
1406
+ `),o.appendChild(m),await new Promise(u=>setTimeout(u,100)),console.log("[Recur SDK] Step 5: Initializing PAYUNi SDK..."),await m.initializePayment(l.sdkToken,v?"SANDBOX":"PRODUCTION")===null){console.log("[Recur SDK] Initialization aborted");return}console.log("[Recur SDK] PAYUNi SDK initialized successfully"),console.log("[Recur SDK] Step 6: Setting up submit handler..."),m.addEventListener("submit",(async u=>{console.log("[Recur SDK] Form submitted");let A=u,{paymentSession:ae}=A.detail;try{let g=await ae.getTradeResult();console.log("[Recur SDK] Trade result received"),console.log("[Recur SDK] Executing payment...");let H=g.HashTimestamp||g.timestamp,B={};if(l.checkout.productType==="SUBSCRIPTION"){let k=l.creditToken,K=l.sdkTimestamp;if(!k)throw console.error("[Recur SDK] Missing creditToken from checkout for subscription"),new Error("Missing creditToken for subscription payment");B={creditToken:k,timestamp:K||H},console.log("[Recur SDK] Using creditToken from checkout:",k.substring(0,30)+"..."),console.log("[Recur SDK] Using timestamp:",K?"from checkout (sdkTimestamp)":"from tradeResult")}let $=await fetch(`${i}/v1/checkouts/${l.checkout.id}/pay`,{method:"POST",headers:s,body:JSON.stringify(B)});if(!$.ok){let k=await $.json().catch(()=>({}));throw new Error(k.error||"Failed to execute payment")}let p=await $.json();if(console.log("[Recur SDK] Payment executed:",p),p.requires3D&&p.redirectUrl){console.log("[Recur SDK] 3D verification required"),window.location.href=p.redirectUrl;return}e.onPaymentComplete&&(p.subscription?e.onPaymentComplete({id:p.subscription.id,status:p.subscription.status,planId:l.checkout.productId,amount:l.checkout.amount,billingPeriod:p.subscription.billingPeriod,currentPeriodStart:p.subscription.currentPeriodStart,currentPeriodEnd:p.subscription.currentPeriodEnd}):e.onPaymentComplete({id:p.checkout.id,status:p.checkout.status,planId:l.checkout.productId,amount:l.checkout.amount,billingPeriod:l.checkout.productType})),console.log("[Recur SDK] Checkout flow completed successfully!"),m.resetButton?.(),r&&r.remove()}catch(g){console.error("[Recur SDK] Payment error:",g);let H={code:"PAYMENT_FAILED",message:g instanceof Error?g.message:"Payment failed"};e.onError?.(H),m.resetButton?.()}})),console.log("[Recur SDK] Checkout flow initialized, waiting for user input...")}catch(i){console.error("[Recur SDK] Checkout error:",i),r&&r.remove();let s={code:"CHECKOUT_ERROR",message:i instanceof Error?i.message:"An unknown error occurred"};throw e.onError?.(s),i}}getBaseUrl(){let e=this.core.getConfig();if(e.baseUrl)return e.baseUrl;if(typeof window<"u"){let t=window.location.hostname;if(t==="localhost"||t.includes(".test")||t.includes(".local")||t==="127.0.0.1")return`${window.location.protocol}//${window.location.host}/api`}return"https://api.recur.tw"}createModalWithSkeleton(e){this.closeModal();let t=document.createElement("div");t.id="recur-modal-overlay",t.style.cssText=`
1407
1407
  position: fixed;
1408
1408
  top: 0;
1409
1409
  left: 0;
@@ -1442,7 +1442,7 @@
1442
1442
  background: white;
1443
1443
  border-radius: 12px;
1444
1444
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1445
- `;let n=document.createElement("recur-payment-form-skeleton");return s.appendChild(n),r.appendChild(i),r.appendChild(s),t.appendChild(r),document.body.appendChild(t),this.currentModalOverlay=t,{overlay:t,container:s}}getEmbeddedContainer(e){return e?typeof e=="string"?document.getElementById(e)||document.querySelector(e):e:null}closeModal(){this.currentModal&&(this.currentModal.close(),this.currentModal=null),this.currentModalOverlay&&(this.currentModalOverlay.remove(),this.currentModalOverlay=null)}removeIframe(){this.currentIframe&&(this.currentIframe.remove(),this.currentIframe=null)}close(){this.closeModal(),this.removeIframe()}};function ie(c){return new w(c)}var ge={init:ie,RecurCheckout:w,RecurElements:y,createElements:$};return pe(ye);})();
1445
+ `;let n=document.createElement("recur-payment-form-skeleton");return s.appendChild(n),r.appendChild(i),r.appendChild(s),t.appendChild(r),document.body.appendChild(t),this.currentModalOverlay=t,{overlay:t,container:s}}getEmbeddedContainer(e){return e?typeof e=="string"?document.getElementById(e)||document.querySelector(e):e:null}closeModal(){this.currentModal&&(this.currentModal.close(),this.currentModal=null),this.currentModalOverlay&&(this.currentModalOverlay.remove(),this.currentModalOverlay=null)}removeIframe(){this.currentIframe&&(this.currentIframe.remove(),this.currentIframe=null)}close(){this.closeModal(),this.removeIframe()}};function ne(c){return new E(c)}var ge={init:ne,RecurCheckout:E,RecurElements:y,createElements:N};return pe(ye);})();
1446
1446
  if (typeof window !== "undefined") {
1447
1447
  window.RecurCheckout = RecurCheckout.default;
1448
1448
  window.RecurElements = RecurCheckout.RecurElements;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recur-tw",
3
- "version": "0.4.4",
3
+ "version": "0.6.1",
4
4
  "description": "React & Vanilla JS SDK for embedding subscription checkout flows (Taiwan / PAYUNi)",
5
5
  "type": "module",
6
6
  "private": false,