@umituz/web-polar-payment 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -118,7 +118,7 @@ declare function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdap
118
118
  * Billing Constants
119
119
  * @description Standardized subscription states and plan names
120
120
  */
121
- declare const SUBSCRIPTION_STATUS: {
121
+ declare const SUBSCRIPTION_STATUS: Readonly<{
122
122
  ACTIVE: "active";
123
123
  CANCELED: "canceled";
124
124
  REVOKED: "revoked";
@@ -128,7 +128,7 @@ declare const SUBSCRIPTION_STATUS: {
128
128
  INCOMPLETE_EXPIRED: "incomplete_expired";
129
129
  UNPAID: "unpaid";
130
130
  NONE: "none";
131
- };
131
+ }>;
132
132
  declare const FREE_PLAN = "free";
133
133
 
134
134
  /**
package/dist/index.d.ts CHANGED
@@ -118,7 +118,7 @@ declare function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdap
118
118
  * Billing Constants
119
119
  * @description Standardized subscription states and plan names
120
120
  */
121
- declare const SUBSCRIPTION_STATUS: {
121
+ declare const SUBSCRIPTION_STATUS: Readonly<{
122
122
  ACTIVE: "active";
123
123
  CANCELED: "canceled";
124
124
  REVOKED: "revoked";
@@ -128,7 +128,7 @@ declare const SUBSCRIPTION_STATUS: {
128
128
  INCOMPLETE_EXPIRED: "incomplete_expired";
129
129
  UNPAID: "unpaid";
130
130
  NONE: "none";
131
- };
131
+ }>;
132
132
  declare const FREE_PLAN = "free";
133
133
 
134
134
  /**
package/dist/index.js CHANGED
@@ -43,21 +43,21 @@ __export(index_exports, {
43
43
  module.exports = __toCommonJS(index_exports);
44
44
 
45
45
  // src/infrastructure/utils/normalization.util.ts
46
+ var STATUS_MAP = {
47
+ active: "active",
48
+ trialing: "trialing",
49
+ past_due: "past_due",
50
+ incomplete: "incomplete",
51
+ incomplete_expired: "incomplete_expired",
52
+ unpaid: "unpaid",
53
+ canceled: "canceled",
54
+ cancelled: "canceled",
55
+ revoked: "revoked",
56
+ none: "none"
57
+ };
46
58
  function normalizeStatus(raw) {
47
- const map = {
48
- active: "active",
49
- trialing: "trialing",
50
- past_due: "past_due",
51
- incomplete: "incomplete",
52
- incomplete_expired: "incomplete_expired",
53
- unpaid: "unpaid",
54
- canceled: "canceled",
55
- cancelled: "canceled",
56
- revoked: "revoked",
57
- none: "none"
58
- };
59
59
  if (typeof raw !== "string") return "none";
60
- return map[raw.toLowerCase()] ?? "none";
60
+ return STATUS_MAP[raw.toLowerCase()] ?? "none";
61
61
  }
62
62
  function normalizeBillingCycle(interval) {
63
63
  if (typeof interval !== "string") return "monthly";
@@ -99,15 +99,31 @@ function createFirebaseAdapter(config) {
99
99
  cancelAtPeriodEnd: config.db?.cancelAtPeriodEndField ?? "cancelAtPeriodEnd",
100
100
  currentPeriodEnd: config.db?.currentPeriodEndField ?? "currentPeriodEnd"
101
101
  };
102
+ let httpsCallableCache = null;
103
+ let firestoreCache = null;
104
+ async function getHttpsCallable() {
105
+ if (!httpsCallableCache) {
106
+ const mod = await import("firebase/functions");
107
+ httpsCallableCache = mod.httpsCallable;
108
+ }
109
+ return httpsCallableCache;
110
+ }
111
+ async function getFirestore() {
112
+ if (!firestoreCache) {
113
+ const mod = await import("firebase/firestore");
114
+ firestoreCache = { doc: mod.doc, getDoc: mod.getDoc };
115
+ }
116
+ return firestoreCache;
117
+ }
102
118
  async function callable(name, data) {
103
- const { httpsCallable } = await import("firebase/functions");
119
+ const httpsCallable = await getHttpsCallable();
104
120
  const fn = httpsCallable(functions, name);
105
121
  const result = await fn(data);
106
122
  return result.data;
107
123
  }
108
124
  return {
109
125
  async getStatus(userId) {
110
- const { doc, getDoc } = await import("firebase/firestore");
126
+ const { doc, getDoc } = await getFirestore();
111
127
  const snap = await getDoc(doc(firestore, db.collection, userId));
112
128
  if (!snap.exists()) {
113
129
  return { plan: "free", subscriptionStatus: "none" };
@@ -161,7 +177,7 @@ function createFirebaseAdapter(config) {
161
177
  }
162
178
 
163
179
  // src/infrastructure/constants/billing.constants.ts
164
- var SUBSCRIPTION_STATUS = {
180
+ var SUBSCRIPTION_STATUS = Object.freeze({
165
181
  ACTIVE: "active",
166
182
  CANCELED: "canceled",
167
183
  REVOKED: "revoked",
@@ -171,7 +187,7 @@ var SUBSCRIPTION_STATUS = {
171
187
  INCOMPLETE_EXPIRED: "incomplete_expired",
172
188
  UNPAID: "unpaid",
173
189
  NONE: "none"
174
- };
190
+ });
175
191
  var FREE_PLAN = "free";
176
192
 
177
193
  // src/presentation/components/PolarProvider.tsx
@@ -202,31 +218,36 @@ function PolarProvider({ adapter, userId, children }) {
202
218
  const [status, setStatus] = (0, import_react2.useState)(FREE_STATUS);
203
219
  const [loading, setLoading] = (0, import_react2.useState)(true);
204
220
  const adapterRef = (0, import_react2.useRef)(adapter);
205
- adapterRef.current = adapter;
221
+ const statusRef = (0, import_react2.useRef)({ setStatus, setLoading });
222
+ const userIdRef = (0, import_react2.useRef)(normalizeUserId(userId));
206
223
  const refreshAbortRef = (0, import_react2.useRef)(null);
224
+ adapterRef.current = adapter;
225
+ statusRef.current = { setStatus, setLoading };
226
+ userIdRef.current = normalizeUserId(userId);
207
227
  const refresh = (0, import_react2.useCallback)(async () => {
208
- const uid = normalizeUserId(userId);
228
+ const uid = userIdRef.current;
229
+ const { setStatus: setStatus2, setLoading: setLoading2 } = statusRef.current;
209
230
  if (!uid) {
210
- setStatus(FREE_STATUS);
211
- setLoading(false);
231
+ setStatus2(FREE_STATUS);
232
+ setLoading2(false);
212
233
  return;
213
234
  }
214
235
  refreshAbortRef.current?.abort();
215
236
  const ctrl = new AbortController();
216
237
  refreshAbortRef.current = ctrl;
217
238
  try {
218
- setLoading(true);
239
+ setLoading2(true);
219
240
  const s = await adapterRef.current.getStatus(uid);
220
- if (!ctrl.signal.aborted) setStatus(s);
241
+ if (!ctrl.signal.aborted) setStatus2(s);
221
242
  } catch (err) {
222
243
  if (!ctrl.signal.aborted) {
223
244
  console.error("[polar-billing] getStatus failed:", err);
224
- setStatus(FREE_STATUS);
245
+ setStatus2(FREE_STATUS);
225
246
  }
226
247
  } finally {
227
- if (!ctrl.signal.aborted) setLoading(false);
248
+ if (!ctrl.signal.aborted) setLoading2(false);
228
249
  }
229
- }, [userId]);
250
+ }, []);
230
251
  (0, import_react2.useEffect)(() => {
231
252
  refresh();
232
253
  return () => {
@@ -234,26 +255,26 @@ function PolarProvider({ adapter, userId, children }) {
234
255
  };
235
256
  }, [refresh]);
236
257
  const startCheckout = (0, import_react2.useCallback)(async (params) => {
237
- const uid = normalizeUserId(userId);
238
- const result = await adapterRef.current.createCheckout({ ...params, userId: uid });
258
+ const uid = userIdRef.current;
259
+ const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? void 0 });
239
260
  if (!result.url.startsWith("https://")) {
240
261
  throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https://");
241
262
  }
242
263
  window.location.href = result.url;
243
- }, [userId]);
264
+ }, []);
244
265
  const syncSubscription = (0, import_react2.useCallback)(async () => {
245
- const uid = normalizeUserId(userId);
266
+ const uid = userIdRef.current;
246
267
  if (!uid) return { synced: false };
247
268
  const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
248
269
  const result = await adapterRef.current.syncSubscription(uid, checkoutId);
249
270
  if (result.synced) await refresh();
250
271
  return result;
251
- }, [userId, refresh]);
272
+ }, [refresh]);
252
273
  const getBillingHistory = (0, import_react2.useCallback)(async () => {
253
- const uid = normalizeUserId(userId);
274
+ const uid = userIdRef.current;
254
275
  if (!uid) return [];
255
276
  return adapterRef.current.getBillingHistory(uid);
256
- }, [userId]);
277
+ }, []);
257
278
  const cancelSubscription = (0, import_react2.useCallback)(
258
279
  async (reason) => {
259
280
  const result = await adapterRef.current.cancelSubscription(reason);
@@ -261,12 +282,13 @@ function PolarProvider({ adapter, userId, children }) {
261
282
  return result;
262
283
  },
263
284
  [refresh]
285
+ // Only depends on stable refresh
264
286
  );
265
287
  const getPortalUrl = (0, import_react2.useCallback)(async () => {
266
- const uid = normalizeUserId(userId);
288
+ const uid = userIdRef.current;
267
289
  if (!uid) throw new Error("[polar-billing] Cannot get portal URL: No authenticated user");
268
290
  return adapterRef.current.getPortalUrl(uid);
269
- }, [userId]);
291
+ }, []);
270
292
  const value = (0, import_react2.useMemo)(
271
293
  () => ({
272
294
  status,
@@ -278,7 +300,8 @@ function PolarProvider({ adapter, userId, children }) {
278
300
  cancelSubscription,
279
301
  getPortalUrl
280
302
  }),
281
- [status, loading, refresh, startCheckout, syncSubscription, getBillingHistory, cancelSubscription, getPortalUrl]
303
+ [status, loading, refresh, syncSubscription]
304
+ // startCheckout, getBillingHistory, cancelSubscription, getPortalUrl are stable
282
305
  );
283
306
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PolarContext.Provider, { value, children });
284
307
  }
package/dist/index.mjs CHANGED
@@ -1,19 +1,19 @@
1
1
  // src/infrastructure/utils/normalization.util.ts
2
+ var STATUS_MAP = {
3
+ active: "active",
4
+ trialing: "trialing",
5
+ past_due: "past_due",
6
+ incomplete: "incomplete",
7
+ incomplete_expired: "incomplete_expired",
8
+ unpaid: "unpaid",
9
+ canceled: "canceled",
10
+ cancelled: "canceled",
11
+ revoked: "revoked",
12
+ none: "none"
13
+ };
2
14
  function normalizeStatus(raw) {
3
- const map = {
4
- active: "active",
5
- trialing: "trialing",
6
- past_due: "past_due",
7
- incomplete: "incomplete",
8
- incomplete_expired: "incomplete_expired",
9
- unpaid: "unpaid",
10
- canceled: "canceled",
11
- cancelled: "canceled",
12
- revoked: "revoked",
13
- none: "none"
14
- };
15
15
  if (typeof raw !== "string") return "none";
16
- return map[raw.toLowerCase()] ?? "none";
16
+ return STATUS_MAP[raw.toLowerCase()] ?? "none";
17
17
  }
18
18
  function normalizeBillingCycle(interval) {
19
19
  if (typeof interval !== "string") return "monthly";
@@ -55,15 +55,31 @@ function createFirebaseAdapter(config) {
55
55
  cancelAtPeriodEnd: config.db?.cancelAtPeriodEndField ?? "cancelAtPeriodEnd",
56
56
  currentPeriodEnd: config.db?.currentPeriodEndField ?? "currentPeriodEnd"
57
57
  };
58
+ let httpsCallableCache = null;
59
+ let firestoreCache = null;
60
+ async function getHttpsCallable() {
61
+ if (!httpsCallableCache) {
62
+ const mod = await import("firebase/functions");
63
+ httpsCallableCache = mod.httpsCallable;
64
+ }
65
+ return httpsCallableCache;
66
+ }
67
+ async function getFirestore() {
68
+ if (!firestoreCache) {
69
+ const mod = await import("firebase/firestore");
70
+ firestoreCache = { doc: mod.doc, getDoc: mod.getDoc };
71
+ }
72
+ return firestoreCache;
73
+ }
58
74
  async function callable(name, data) {
59
- const { httpsCallable } = await import("firebase/functions");
75
+ const httpsCallable = await getHttpsCallable();
60
76
  const fn = httpsCallable(functions, name);
61
77
  const result = await fn(data);
62
78
  return result.data;
63
79
  }
64
80
  return {
65
81
  async getStatus(userId) {
66
- const { doc, getDoc } = await import("firebase/firestore");
82
+ const { doc, getDoc } = await getFirestore();
67
83
  const snap = await getDoc(doc(firestore, db.collection, userId));
68
84
  if (!snap.exists()) {
69
85
  return { plan: "free", subscriptionStatus: "none" };
@@ -117,7 +133,7 @@ function createFirebaseAdapter(config) {
117
133
  }
118
134
 
119
135
  // src/infrastructure/constants/billing.constants.ts
120
- var SUBSCRIPTION_STATUS = {
136
+ var SUBSCRIPTION_STATUS = Object.freeze({
121
137
  ACTIVE: "active",
122
138
  CANCELED: "canceled",
123
139
  REVOKED: "revoked",
@@ -127,7 +143,7 @@ var SUBSCRIPTION_STATUS = {
127
143
  INCOMPLETE_EXPIRED: "incomplete_expired",
128
144
  UNPAID: "unpaid",
129
145
  NONE: "none"
130
- };
146
+ });
131
147
  var FREE_PLAN = "free";
132
148
 
133
149
  // src/presentation/components/PolarProvider.tsx
@@ -164,31 +180,36 @@ function PolarProvider({ adapter, userId, children }) {
164
180
  const [status, setStatus] = useState(FREE_STATUS);
165
181
  const [loading, setLoading] = useState(true);
166
182
  const adapterRef = useRef(adapter);
167
- adapterRef.current = adapter;
183
+ const statusRef = useRef({ setStatus, setLoading });
184
+ const userIdRef = useRef(normalizeUserId(userId));
168
185
  const refreshAbortRef = useRef(null);
186
+ adapterRef.current = adapter;
187
+ statusRef.current = { setStatus, setLoading };
188
+ userIdRef.current = normalizeUserId(userId);
169
189
  const refresh = useCallback(async () => {
170
- const uid = normalizeUserId(userId);
190
+ const uid = userIdRef.current;
191
+ const { setStatus: setStatus2, setLoading: setLoading2 } = statusRef.current;
171
192
  if (!uid) {
172
- setStatus(FREE_STATUS);
173
- setLoading(false);
193
+ setStatus2(FREE_STATUS);
194
+ setLoading2(false);
174
195
  return;
175
196
  }
176
197
  refreshAbortRef.current?.abort();
177
198
  const ctrl = new AbortController();
178
199
  refreshAbortRef.current = ctrl;
179
200
  try {
180
- setLoading(true);
201
+ setLoading2(true);
181
202
  const s = await adapterRef.current.getStatus(uid);
182
- if (!ctrl.signal.aborted) setStatus(s);
203
+ if (!ctrl.signal.aborted) setStatus2(s);
183
204
  } catch (err) {
184
205
  if (!ctrl.signal.aborted) {
185
206
  console.error("[polar-billing] getStatus failed:", err);
186
- setStatus(FREE_STATUS);
207
+ setStatus2(FREE_STATUS);
187
208
  }
188
209
  } finally {
189
- if (!ctrl.signal.aborted) setLoading(false);
210
+ if (!ctrl.signal.aborted) setLoading2(false);
190
211
  }
191
- }, [userId]);
212
+ }, []);
192
213
  useEffect(() => {
193
214
  refresh();
194
215
  return () => {
@@ -196,26 +217,26 @@ function PolarProvider({ adapter, userId, children }) {
196
217
  };
197
218
  }, [refresh]);
198
219
  const startCheckout = useCallback(async (params) => {
199
- const uid = normalizeUserId(userId);
200
- const result = await adapterRef.current.createCheckout({ ...params, userId: uid });
220
+ const uid = userIdRef.current;
221
+ const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? void 0 });
201
222
  if (!result.url.startsWith("https://")) {
202
223
  throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https://");
203
224
  }
204
225
  window.location.href = result.url;
205
- }, [userId]);
226
+ }, []);
206
227
  const syncSubscription = useCallback(async () => {
207
- const uid = normalizeUserId(userId);
228
+ const uid = userIdRef.current;
208
229
  if (!uid) return { synced: false };
209
230
  const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
210
231
  const result = await adapterRef.current.syncSubscription(uid, checkoutId);
211
232
  if (result.synced) await refresh();
212
233
  return result;
213
- }, [userId, refresh]);
234
+ }, [refresh]);
214
235
  const getBillingHistory = useCallback(async () => {
215
- const uid = normalizeUserId(userId);
236
+ const uid = userIdRef.current;
216
237
  if (!uid) return [];
217
238
  return adapterRef.current.getBillingHistory(uid);
218
- }, [userId]);
239
+ }, []);
219
240
  const cancelSubscription = useCallback(
220
241
  async (reason) => {
221
242
  const result = await adapterRef.current.cancelSubscription(reason);
@@ -223,12 +244,13 @@ function PolarProvider({ adapter, userId, children }) {
223
244
  return result;
224
245
  },
225
246
  [refresh]
247
+ // Only depends on stable refresh
226
248
  );
227
249
  const getPortalUrl = useCallback(async () => {
228
- const uid = normalizeUserId(userId);
250
+ const uid = userIdRef.current;
229
251
  if (!uid) throw new Error("[polar-billing] Cannot get portal URL: No authenticated user");
230
252
  return adapterRef.current.getPortalUrl(uid);
231
- }, [userId]);
253
+ }, []);
232
254
  const value = useMemo(
233
255
  () => ({
234
256
  status,
@@ -240,7 +262,8 @@ function PolarProvider({ adapter, userId, children }) {
240
262
  cancelSubscription,
241
263
  getPortalUrl
242
264
  }),
243
- [status, loading, refresh, startCheckout, syncSubscription, getBillingHistory, cancelSubscription, getPortalUrl]
265
+ [status, loading, refresh, syncSubscription]
266
+ // startCheckout, getBillingHistory, cancelSubscription, getPortalUrl are stable
244
267
  );
245
268
  return /* @__PURE__ */ jsx(PolarContext.Provider, { value, children });
246
269
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-polar-payment",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Universal Polar.sh subscription billing — Firebase adapter",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -3,7 +3,8 @@
3
3
  * @description Standardized subscription states and plan names
4
4
  */
5
5
 
6
- export const SUBSCRIPTION_STATUS = {
6
+ // Object.freeze() prevents accidental mutations and enables V8 optimizations
7
+ export const SUBSCRIPTION_STATUS = Object.freeze({
7
8
  ACTIVE: 'active' as const,
8
9
  CANCELED: 'canceled' as const,
9
10
  REVOKED: 'revoked' as const,
@@ -13,6 +14,16 @@ export const SUBSCRIPTION_STATUS = {
13
14
  INCOMPLETE_EXPIRED: 'incomplete_expired' as const,
14
15
  UNPAID: 'unpaid' as const,
15
16
  NONE: 'none' as const,
16
- };
17
+ }) as Readonly<{
18
+ ACTIVE: 'active';
19
+ CANCELED: 'canceled';
20
+ REVOKED: 'revoked';
21
+ TRIALING: 'trialing';
22
+ PAST_DUE: 'past_due';
23
+ INCOMPLETE: 'incomplete';
24
+ INCOMPLETE_EXPIRED: 'incomplete_expired';
25
+ UNPAID: 'unpaid';
26
+ NONE: 'none';
27
+ }>;
17
28
 
18
29
  export const FREE_PLAN = 'free';
@@ -95,8 +95,29 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
95
95
  currentPeriodEnd: config.db?.currentPeriodEndField ?? 'currentPeriodEnd',
96
96
  };
97
97
 
98
+ // Cache imports to avoid repeated dynamic import overhead
99
+ // Reduces latency on subsequent calls and GC pressure
100
+ let httpsCallableCache: typeof import('firebase/functions')['httpsCallable'] | null = null;
101
+ let firestoreCache: { doc: any; getDoc: any } | null = null;
102
+
103
+ async function getHttpsCallable() {
104
+ if (!httpsCallableCache) {
105
+ const mod = await import('firebase/functions');
106
+ httpsCallableCache = mod.httpsCallable;
107
+ }
108
+ return httpsCallableCache;
109
+ }
110
+
111
+ async function getFirestore() {
112
+ if (!firestoreCache) {
113
+ const mod = await import('firebase/firestore');
114
+ firestoreCache = { doc: mod.doc, getDoc: mod.getDoc };
115
+ }
116
+ return firestoreCache;
117
+ }
118
+
98
119
  async function callable<T = unknown, R = unknown>(name: string, data?: T): Promise<R> {
99
- const { httpsCallable } = await import('firebase/functions');
120
+ const httpsCallable = await getHttpsCallable();
100
121
  const fn = (httpsCallable as any)(functions, name) as (data?: T) => Promise<{ data: R }>;
101
122
  const result = await fn(data);
102
123
  return result.data;
@@ -104,7 +125,7 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
104
125
 
105
126
  return {
106
127
  async getStatus(userId: string): Promise<SubscriptionStatus> {
107
- const { doc, getDoc } = await import('firebase/firestore');
128
+ const { doc, getDoc } = await getFirestore();
108
129
  const snap = await getDoc((doc as any)(firestore, db.collection, userId));
109
130
 
110
131
  if (!snap.exists()) {
@@ -1,25 +1,27 @@
1
1
  import type { SubscriptionStatusValue, BillingCycle } from '../../domain/entities';
2
2
 
3
+ // Static map outside function scope - created once, reused forever
4
+ // Reduces GC pressure from repeated object allocations
5
+ const STATUS_MAP: Readonly<Record<string, SubscriptionStatusValue>> = {
6
+ active: 'active',
7
+ trialing: 'trialing',
8
+ past_due: 'past_due',
9
+ incomplete: 'incomplete',
10
+ incomplete_expired: 'incomplete_expired',
11
+ unpaid: 'unpaid',
12
+ canceled: 'canceled',
13
+ cancelled: 'canceled',
14
+ revoked: 'revoked',
15
+ none: 'none',
16
+ };
17
+
3
18
  /**
4
19
  * Normalize a raw Polar status string to a known value.
5
20
  * @description Defaults to 'none' for unknown statuses or non-string input.
6
21
  */
7
22
  export function normalizeStatus(raw: string): SubscriptionStatusValue {
8
- const map: Record<string, SubscriptionStatusValue> = {
9
- active: 'active',
10
- trialing: 'trialing',
11
- past_due: 'past_due',
12
- incomplete: 'incomplete',
13
- incomplete_expired: 'incomplete_expired',
14
- unpaid: 'unpaid',
15
- canceled: 'canceled',
16
- cancelled: 'canceled',
17
- revoked: 'revoked',
18
- none: 'none',
19
- };
20
-
21
23
  if (typeof raw !== 'string') return 'none';
22
- return map[raw.toLowerCase()] ?? 'none';
24
+ return STATUS_MAP[raw.toLowerCase()] ?? 'none';
23
25
  }
24
26
 
25
27
  /**
@@ -42,12 +42,23 @@ function normalizeUserId(userId: string | undefined): string | undefined {
42
42
  export function PolarProvider({ adapter, userId, children }: PolarProviderProps) {
43
43
  const [status, setStatus] = useState<SubscriptionStatus>(FREE_STATUS);
44
44
  const [loading, setLoading] = useState(true);
45
+
46
+ // Store adapter and setters in refs to create stable callbacks
45
47
  const adapterRef = useRef(adapter);
46
- adapterRef.current = adapter;
48
+ const statusRef = useRef({ setStatus, setLoading });
49
+ const userIdRef = useRef(normalizeUserId(userId));
47
50
  const refreshAbortRef = useRef<AbortController | null>(null);
48
51
 
52
+ // Keep refs in sync
53
+ adapterRef.current = adapter;
54
+ statusRef.current = { setStatus, setLoading };
55
+ userIdRef.current = normalizeUserId(userId);
56
+
57
+ // Stable refresh function with no dependencies - prevents cascading re-renders
49
58
  const refresh = useCallback(async () => {
50
- const uid = normalizeUserId(userId);
59
+ const uid = userIdRef.current;
60
+ const { setStatus, setLoading } = statusRef.current;
61
+
51
62
  if (!uid) {
52
63
  setStatus(FREE_STATUS);
53
64
  setLoading(false);
@@ -70,37 +81,37 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
70
81
  } finally {
71
82
  if (!ctrl.signal.aborted) setLoading(false);
72
83
  }
73
- }, [userId]);
84
+ }, []); // No dependencies - completely stable
74
85
 
75
86
  useEffect(() => {
76
87
  refresh();
77
88
  return () => { refreshAbortRef.current?.abort(); };
78
- }, [refresh]);
89
+ }, [refresh]); // Only re-run if refresh identity changes (never)
79
90
 
80
91
  const startCheckout = useCallback(async (params: CheckoutParams) => {
81
- const uid = normalizeUserId(userId);
82
- const result = await adapterRef.current.createCheckout({ ...params, userId: uid });
92
+ const uid = userIdRef.current;
93
+ const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? undefined });
83
94
  if (!result.url.startsWith('https://')) {
84
95
  throw new Error('[polar-billing] Invalid checkout URL returned: URL must start with https://');
85
96
  }
86
97
  window.location.href = result.url;
87
- }, [userId]);
98
+ }, []); // No dependencies - stable
88
99
 
89
100
  const syncSubscription = useCallback(async (): Promise<SyncResult> => {
90
- const uid = normalizeUserId(userId);
101
+ const uid = userIdRef.current;
91
102
  if (!uid) return { synced: false };
92
103
 
93
104
  const checkoutId = new URLSearchParams(window.location.search).get('checkout_id') ?? undefined;
94
105
  const result = await adapterRef.current.syncSubscription(uid, checkoutId);
95
106
  if (result.synced) await refresh();
96
107
  return result;
97
- }, [userId, refresh]);
108
+ }, [refresh]); // Only depends on stable refresh
98
109
 
99
110
  const getBillingHistory = useCallback(async (): Promise<OrderItem[]> => {
100
- const uid = normalizeUserId(userId);
111
+ const uid = userIdRef.current;
101
112
  if (!uid) return [];
102
113
  return adapterRef.current.getBillingHistory(uid);
103
- }, [userId]);
114
+ }, []); // No dependencies - stable
104
115
 
105
116
  const cancelSubscription = useCallback(
106
117
  async (reason?: CancellationReason): Promise<CancelResult> => {
@@ -108,15 +119,17 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
108
119
  if (result.success) await refresh();
109
120
  return result;
110
121
  },
111
- [refresh],
122
+ [refresh], // Only depends on stable refresh
112
123
  );
113
124
 
114
125
  const getPortalUrl = useCallback(async (): Promise<string> => {
115
- const uid = normalizeUserId(userId);
126
+ const uid = userIdRef.current;
116
127
  if (!uid) throw new Error('[polar-billing] Cannot get portal URL: No authenticated user');
117
128
  return adapterRef.current.getPortalUrl(uid);
118
- }, [userId]);
129
+ }, []); // No dependencies - stable
119
130
 
131
+ // Memoized context value - only recreates when status/loading changes
132
+ // All functions are stable, so they don't trigger re-creation
120
133
  const value = useMemo(
121
134
  () => ({
122
135
  status,
@@ -128,7 +141,7 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
128
141
  cancelSubscription,
129
142
  getPortalUrl,
130
143
  }),
131
- [status, loading, refresh, startCheckout, syncSubscription, getBillingHistory, cancelSubscription, getPortalUrl],
144
+ [status, loading, refresh, syncSubscription], // startCheckout, getBillingHistory, cancelSubscription, getPortalUrl are stable
132
145
  );
133
146
 
134
147
  return <PolarContext.Provider value={value}>{children}</PolarContext.Provider>;