@umituz/web-polar-payment 1.0.13 → 1.0.15

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.
@@ -16,11 +16,7 @@ import type {
16
16
  OrderItem,
17
17
  } from '../../domain/entities';
18
18
  import { PolarContext } from '../hooks/usePolarBilling';
19
-
20
- /**
21
- * PolarProvider Component
22
- * @description Context provider for Polar billing management.
23
- */
19
+ import { normalizeUserId, isValidProductId, isValidCheckoutUrl, isProductionInsecureUrl } from '../../infrastructure/utils/validations.util';
24
20
 
25
21
  interface PolarProviderProps {
26
22
  adapter: PolarAdapter;
@@ -33,74 +29,85 @@ const FREE_STATUS: SubscriptionStatus = {
33
29
  subscriptionStatus: 'none',
34
30
  };
35
31
 
36
- function normalizeUserId(userId: string | undefined): string | undefined {
37
- if (typeof userId !== 'string') return undefined;
38
- const trimmed = userId.trim();
39
- return trimmed.length > 0 ? trimmed : undefined;
40
- }
41
-
42
32
  export function PolarProvider({ adapter, userId, children }: PolarProviderProps) {
43
33
  const [status, setStatus] = useState<SubscriptionStatus>(FREE_STATUS);
44
34
  const [loading, setLoading] = useState(true);
35
+
45
36
  const adapterRef = useRef(adapter);
37
+ const statusRef = useRef({ setStatus, setLoading });
38
+ const userIdRef = useRef(normalizeUserId(userId));
39
+ const refreshMountedRef = useRef(true);
40
+
46
41
  adapterRef.current = adapter;
47
- const refreshAbortRef = useRef<AbortController | null>(null);
42
+ statusRef.current = { setStatus, setLoading };
43
+ userIdRef.current = normalizeUserId(userId);
48
44
 
49
45
  const refresh = useCallback(async () => {
50
- const uid = normalizeUserId(userId);
46
+ const uid = userIdRef.current;
47
+ const { setStatus, setLoading } = statusRef.current;
48
+
51
49
  if (!uid) {
52
50
  setStatus(FREE_STATUS);
53
51
  setLoading(false);
54
52
  return;
55
53
  }
56
54
 
57
- refreshAbortRef.current?.abort();
58
- const ctrl = new AbortController();
59
- refreshAbortRef.current = ctrl;
55
+ refreshMountedRef.current = true;
60
56
 
61
57
  try {
62
58
  setLoading(true);
63
59
  const s = await adapterRef.current.getStatus(uid);
64
- if (!ctrl.signal.aborted) setStatus(s);
60
+ if (refreshMountedRef.current) setStatus(s);
65
61
  } catch (err) {
66
- if (!ctrl.signal.aborted) {
62
+ if (refreshMountedRef.current) {
67
63
  console.error('[polar-billing] getStatus failed:', err);
68
64
  setStatus(FREE_STATUS);
69
65
  }
70
66
  } finally {
71
- if (!ctrl.signal.aborted) setLoading(false);
67
+ if (refreshMountedRef.current) setLoading(false);
72
68
  }
73
- }, [userId]);
69
+ }, []);
74
70
 
75
71
  useEffect(() => {
72
+ refreshMountedRef.current = true;
76
73
  refresh();
77
- return () => { refreshAbortRef.current?.abort(); };
78
- }, [refresh]);
74
+ return () => { refreshMountedRef.current = false; };
75
+ }, [userId]);
79
76
 
80
77
  const startCheckout = useCallback(async (params: CheckoutParams) => {
81
- const uid = normalizeUserId(userId);
82
- const result = await adapterRef.current.createCheckout({ ...params, userId: uid });
83
- if (!result.url.startsWith('https://')) {
84
- throw new Error('[polar-billing] Invalid checkout URL returned: URL must start with https://');
78
+ if (!isValidProductId(params.productId)) {
79
+ throw new Error('[polar-billing] Invalid productId: must be a non-empty string');
85
80
  }
81
+
82
+ const uid = userIdRef.current;
83
+ const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? undefined });
84
+
85
+ if (!isValidCheckoutUrl(result.url)) {
86
+ throw new Error('[polar-billing] Invalid checkout URL returned: URL must start with https:// or http://');
87
+ }
88
+
89
+ if (isProductionInsecureUrl(result.url)) {
90
+ console.warn('[polar-billing] WARNING: Using insecure http:// URL in production environment');
91
+ }
92
+
86
93
  window.location.href = result.url;
87
- }, [userId]);
94
+ }, []);
88
95
 
89
96
  const syncSubscription = useCallback(async (): Promise<SyncResult> => {
90
- const uid = normalizeUserId(userId);
97
+ const uid = userIdRef.current;
91
98
  if (!uid) return { synced: false };
92
99
 
93
100
  const checkoutId = new URLSearchParams(window.location.search).get('checkout_id') ?? undefined;
94
101
  const result = await adapterRef.current.syncSubscription(uid, checkoutId);
95
102
  if (result.synced) await refresh();
96
103
  return result;
97
- }, [userId, refresh]);
104
+ }, [refresh]);
98
105
 
99
106
  const getBillingHistory = useCallback(async (): Promise<OrderItem[]> => {
100
- const uid = normalizeUserId(userId);
107
+ const uid = userIdRef.current;
101
108
  if (!uid) return [];
102
109
  return adapterRef.current.getBillingHistory(uid);
103
- }, [userId]);
110
+ }, []);
104
111
 
105
112
  const cancelSubscription = useCallback(
106
113
  async (reason?: CancellationReason): Promise<CancelResult> => {
@@ -112,10 +119,10 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
112
119
  );
113
120
 
114
121
  const getPortalUrl = useCallback(async (): Promise<string> => {
115
- const uid = normalizeUserId(userId);
122
+ const uid = userIdRef.current;
116
123
  if (!uid) throw new Error('[polar-billing] Cannot get portal URL: No authenticated user');
117
124
  return adapterRef.current.getPortalUrl(uid);
118
- }, [userId]);
125
+ }, []);
119
126
 
120
127
  const value = useMemo(
121
128
  () => ({
@@ -128,7 +135,7 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
128
135
  cancelSubscription,
129
136
  getPortalUrl,
130
137
  }),
131
- [status, loading, refresh, startCheckout, syncSubscription, getBillingHistory, cancelSubscription, getPortalUrl],
138
+ [status, loading, refresh, syncSubscription],
132
139
  );
133
140
 
134
141
  return <PolarContext.Provider value={value}>{children}</PolarContext.Provider>;
@@ -21,10 +21,6 @@ export interface PolarContextValue {
21
21
 
22
22
  export const PolarContext = createContext<PolarContextValue | undefined>(undefined);
23
23
 
24
- /**
25
- * usePolarBilling Hook
26
- * @description Hook to access Polar billing context
27
- */
28
24
  export function usePolarBilling(): PolarContextValue {
29
25
  const ctx = useContext(PolarContext);
30
26
  if (!ctx) throw new Error('usePolarBilling must be used within <PolarProvider>');
@@ -1,7 +1,2 @@
1
- /**
2
- * Presentation Layer
3
- * Subpath: @umituz/web-polar-payment/presentation
4
- */
5
-
6
1
  export * from './components/PolarProvider';
7
2
  export * from './hooks/usePolarBilling';