@umituz/web-polar-payment 1.0.14 → 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.
package/dist/index.d.mts CHANGED
@@ -2,10 +2,6 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
4
 
5
- /**
6
- * Subscription Entity
7
- * @description Types for subscription status and billing cycles
8
- */
9
5
  type SubscriptionStatusValue = 'active' | 'canceled' | 'revoked' | 'trialing' | 'past_due' | 'incomplete' | 'incomplete_expired' | 'unpaid' | 'none';
10
6
  type BillingCycle = 'monthly' | 'yearly';
11
7
  interface SubscriptionStatus {
@@ -16,14 +12,9 @@ interface SubscriptionStatus {
16
12
  currentPeriodEnd?: string;
17
13
  billingCycle?: BillingCycle;
18
14
  polarCustomerId?: string;
19
- /** Token balance (for token-based projects like Aria) */
20
15
  tokens?: number;
21
16
  }
22
17
 
23
- /**
24
- * Order Entity
25
- * @description Types for billing history items
26
- */
27
18
  interface OrderItem {
28
19
  id: string;
29
20
  createdAt: string;
@@ -35,16 +26,11 @@ interface OrderItem {
35
26
  invoiceUrl?: string;
36
27
  }
37
28
 
38
- /**
39
- * Checkout Entity
40
- * @description Types for initiating and following process of checkouts
41
- */
42
29
  interface CheckoutParams {
43
30
  productId: string;
44
31
  planKey?: string;
45
32
  billingCycle?: 'monthly' | 'yearly';
46
33
  successUrl?: string;
47
- /** Injected automatically by PolarProvider — do not pass manually */
48
34
  userId?: string;
49
35
  }
50
36
  interface CheckoutResult {
@@ -52,20 +38,12 @@ interface CheckoutResult {
52
38
  id: string;
53
39
  }
54
40
 
55
- /**
56
- * Cancellation Entity
57
- * @description Types for subscription cancellation reasons and outcomes
58
- */
59
41
  type CancellationReason = 'too_expensive' | 'missing_features' | 'switched_service' | 'unused' | 'customer_service' | 'low_quality' | 'too_complex' | 'other';
60
42
  interface CancelResult {
61
43
  success: boolean;
62
44
  endsAt?: string;
63
45
  }
64
46
 
65
- /**
66
- * Sync Entity
67
- * @description Type for subscription synchronization results
68
- */
69
47
  interface SyncResult {
70
48
  synced: boolean;
71
49
  plan?: string;
@@ -108,16 +86,8 @@ interface FirebaseAdapterConfig {
108
86
  currentPeriodEndField?: string;
109
87
  };
110
88
  }
111
- /**
112
- * Firebase Billing Service
113
- * @description Implementation of PolarAdapter for Firebase Functions and Firestore.
114
- */
115
89
  declare function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapter;
116
90
 
117
- /**
118
- * Billing Constants
119
- * @description Standardized subscription states and plan names
120
- */
121
91
  declare const SUBSCRIPTION_STATUS: Readonly<{
122
92
  ACTIVE: "active";
123
93
  CANCELED: "canceled";
@@ -131,21 +101,20 @@ declare const SUBSCRIPTION_STATUS: Readonly<{
131
101
  }>;
132
102
  declare const FREE_PLAN = "free";
133
103
 
134
- /**
135
- * Normalize a raw Polar status string to a known value.
136
- * @description Defaults to 'none' for unknown statuses or non-string input.
137
- */
138
104
  declare function normalizeStatus(raw: string): SubscriptionStatusValue;
139
- /**
140
- * Normalize billing interval
141
- * @description Maps 'month'/'year' to 'monthly'/'yearly'. Defaults to 'monthly' for unknown values or non-string input.
142
- */
143
105
  declare function normalizeBillingCycle(interval: string): BillingCycle;
144
106
 
145
- /**
146
- * PolarProvider Component
147
- * @description Context provider for Polar billing management.
148
- */
107
+ declare function normalizeUserId(userId: string | undefined): string | undefined;
108
+ declare function isValidProductId(productId: unknown): productId is string;
109
+ declare function isValidCheckoutUrl(url: string): boolean;
110
+ declare function isProductionInsecureUrl(url: string): boolean;
111
+
112
+ declare function asString(value: unknown): string | undefined;
113
+ declare function asBoolean(value: unknown): boolean | undefined;
114
+ declare function isTimestamp(value: unknown): value is {
115
+ toDate(): Date;
116
+ };
117
+
149
118
  interface PolarProviderProps {
150
119
  adapter: PolarAdapter;
151
120
  userId?: string;
@@ -164,11 +133,7 @@ interface PolarContextValue {
164
133
  getPortalUrl: () => Promise<string>;
165
134
  }
166
135
  declare const PolarContext: react.Context<PolarContextValue | undefined>;
167
- /**
168
- * usePolarBilling Hook
169
- * @description Hook to access Polar billing context
170
- */
171
136
  declare function usePolarBilling(): PolarContextValue;
172
137
  declare const useSubscription: typeof usePolarBilling;
173
138
 
174
- export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult, FREE_PLAN, type FirebaseAdapterConfig, type OrderItem, type PolarAdapter, PolarContext, type PolarContextValue, PolarProvider, SUBSCRIPTION_STATUS, type SubscriptionStatus, type SubscriptionStatusValue, type SyncResult, createFirebaseAdapter, normalizeBillingCycle, normalizeStatus, usePolarBilling, useSubscription };
139
+ export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult, FREE_PLAN, type FirebaseAdapterConfig, type OrderItem, type PolarAdapter, PolarContext, type PolarContextValue, PolarProvider, SUBSCRIPTION_STATUS, type SubscriptionStatus, type SubscriptionStatusValue, type SyncResult, asBoolean, asString, createFirebaseAdapter, isProductionInsecureUrl, isTimestamp, isValidCheckoutUrl, isValidProductId, normalizeBillingCycle, normalizeStatus, normalizeUserId, usePolarBilling, useSubscription };
package/dist/index.d.ts CHANGED
@@ -2,10 +2,6 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
4
 
5
- /**
6
- * Subscription Entity
7
- * @description Types for subscription status and billing cycles
8
- */
9
5
  type SubscriptionStatusValue = 'active' | 'canceled' | 'revoked' | 'trialing' | 'past_due' | 'incomplete' | 'incomplete_expired' | 'unpaid' | 'none';
10
6
  type BillingCycle = 'monthly' | 'yearly';
11
7
  interface SubscriptionStatus {
@@ -16,14 +12,9 @@ interface SubscriptionStatus {
16
12
  currentPeriodEnd?: string;
17
13
  billingCycle?: BillingCycle;
18
14
  polarCustomerId?: string;
19
- /** Token balance (for token-based projects like Aria) */
20
15
  tokens?: number;
21
16
  }
22
17
 
23
- /**
24
- * Order Entity
25
- * @description Types for billing history items
26
- */
27
18
  interface OrderItem {
28
19
  id: string;
29
20
  createdAt: string;
@@ -35,16 +26,11 @@ interface OrderItem {
35
26
  invoiceUrl?: string;
36
27
  }
37
28
 
38
- /**
39
- * Checkout Entity
40
- * @description Types for initiating and following process of checkouts
41
- */
42
29
  interface CheckoutParams {
43
30
  productId: string;
44
31
  planKey?: string;
45
32
  billingCycle?: 'monthly' | 'yearly';
46
33
  successUrl?: string;
47
- /** Injected automatically by PolarProvider — do not pass manually */
48
34
  userId?: string;
49
35
  }
50
36
  interface CheckoutResult {
@@ -52,20 +38,12 @@ interface CheckoutResult {
52
38
  id: string;
53
39
  }
54
40
 
55
- /**
56
- * Cancellation Entity
57
- * @description Types for subscription cancellation reasons and outcomes
58
- */
59
41
  type CancellationReason = 'too_expensive' | 'missing_features' | 'switched_service' | 'unused' | 'customer_service' | 'low_quality' | 'too_complex' | 'other';
60
42
  interface CancelResult {
61
43
  success: boolean;
62
44
  endsAt?: string;
63
45
  }
64
46
 
65
- /**
66
- * Sync Entity
67
- * @description Type for subscription synchronization results
68
- */
69
47
  interface SyncResult {
70
48
  synced: boolean;
71
49
  plan?: string;
@@ -108,16 +86,8 @@ interface FirebaseAdapterConfig {
108
86
  currentPeriodEndField?: string;
109
87
  };
110
88
  }
111
- /**
112
- * Firebase Billing Service
113
- * @description Implementation of PolarAdapter for Firebase Functions and Firestore.
114
- */
115
89
  declare function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapter;
116
90
 
117
- /**
118
- * Billing Constants
119
- * @description Standardized subscription states and plan names
120
- */
121
91
  declare const SUBSCRIPTION_STATUS: Readonly<{
122
92
  ACTIVE: "active";
123
93
  CANCELED: "canceled";
@@ -131,21 +101,20 @@ declare const SUBSCRIPTION_STATUS: Readonly<{
131
101
  }>;
132
102
  declare const FREE_PLAN = "free";
133
103
 
134
- /**
135
- * Normalize a raw Polar status string to a known value.
136
- * @description Defaults to 'none' for unknown statuses or non-string input.
137
- */
138
104
  declare function normalizeStatus(raw: string): SubscriptionStatusValue;
139
- /**
140
- * Normalize billing interval
141
- * @description Maps 'month'/'year' to 'monthly'/'yearly'. Defaults to 'monthly' for unknown values or non-string input.
142
- */
143
105
  declare function normalizeBillingCycle(interval: string): BillingCycle;
144
106
 
145
- /**
146
- * PolarProvider Component
147
- * @description Context provider for Polar billing management.
148
- */
107
+ declare function normalizeUserId(userId: string | undefined): string | undefined;
108
+ declare function isValidProductId(productId: unknown): productId is string;
109
+ declare function isValidCheckoutUrl(url: string): boolean;
110
+ declare function isProductionInsecureUrl(url: string): boolean;
111
+
112
+ declare function asString(value: unknown): string | undefined;
113
+ declare function asBoolean(value: unknown): boolean | undefined;
114
+ declare function isTimestamp(value: unknown): value is {
115
+ toDate(): Date;
116
+ };
117
+
149
118
  interface PolarProviderProps {
150
119
  adapter: PolarAdapter;
151
120
  userId?: string;
@@ -164,11 +133,7 @@ interface PolarContextValue {
164
133
  getPortalUrl: () => Promise<string>;
165
134
  }
166
135
  declare const PolarContext: react.Context<PolarContextValue | undefined>;
167
- /**
168
- * usePolarBilling Hook
169
- * @description Hook to access Polar billing context
170
- */
171
136
  declare function usePolarBilling(): PolarContextValue;
172
137
  declare const useSubscription: typeof usePolarBilling;
173
138
 
174
- export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult, FREE_PLAN, type FirebaseAdapterConfig, type OrderItem, type PolarAdapter, PolarContext, type PolarContextValue, PolarProvider, SUBSCRIPTION_STATUS, type SubscriptionStatus, type SubscriptionStatusValue, type SyncResult, createFirebaseAdapter, normalizeBillingCycle, normalizeStatus, usePolarBilling, useSubscription };
139
+ export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult, FREE_PLAN, type FirebaseAdapterConfig, type OrderItem, type PolarAdapter, PolarContext, type PolarContextValue, PolarProvider, SUBSCRIPTION_STATUS, type SubscriptionStatus, type SubscriptionStatusValue, type SyncResult, asBoolean, asString, createFirebaseAdapter, isProductionInsecureUrl, isTimestamp, isValidCheckoutUrl, isValidProductId, normalizeBillingCycle, normalizeStatus, normalizeUserId, usePolarBilling, useSubscription };
package/dist/index.js CHANGED
@@ -34,29 +34,49 @@ __export(index_exports, {
34
34
  PolarContext: () => PolarContext,
35
35
  PolarProvider: () => PolarProvider,
36
36
  SUBSCRIPTION_STATUS: () => SUBSCRIPTION_STATUS,
37
+ asBoolean: () => asBoolean,
38
+ asString: () => asString,
37
39
  createFirebaseAdapter: () => createFirebaseAdapter,
40
+ isProductionInsecureUrl: () => isProductionInsecureUrl,
41
+ isTimestamp: () => isTimestamp,
42
+ isValidCheckoutUrl: () => isValidCheckoutUrl,
43
+ isValidProductId: () => isValidProductId,
38
44
  normalizeBillingCycle: () => normalizeBillingCycle,
39
45
  normalizeStatus: () => normalizeStatus,
46
+ normalizeUserId: () => normalizeUserId,
40
47
  usePolarBilling: () => usePolarBilling,
41
48
  useSubscription: () => useSubscription
42
49
  });
43
50
  module.exports = __toCommonJS(index_exports);
44
51
 
52
+ // src/infrastructure/constants/billing.constants.ts
53
+ var SUBSCRIPTION_STATUS = Object.freeze({
54
+ ACTIVE: "active",
55
+ CANCELED: "canceled",
56
+ REVOKED: "revoked",
57
+ TRIALING: "trialing",
58
+ PAST_DUE: "past_due",
59
+ INCOMPLETE: "incomplete",
60
+ INCOMPLETE_EXPIRED: "incomplete_expired",
61
+ UNPAID: "unpaid",
62
+ NONE: "none"
63
+ });
64
+ var FREE_PLAN = "free";
65
+
45
66
  // 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
- };
67
+ var STATUS_MAP = Object.freeze({
68
+ [SUBSCRIPTION_STATUS.ACTIVE]: SUBSCRIPTION_STATUS.ACTIVE,
69
+ [SUBSCRIPTION_STATUS.CANCELED]: SUBSCRIPTION_STATUS.CANCELED,
70
+ [SUBSCRIPTION_STATUS.REVOKED]: SUBSCRIPTION_STATUS.REVOKED,
71
+ [SUBSCRIPTION_STATUS.TRIALING]: SUBSCRIPTION_STATUS.TRIALING,
72
+ [SUBSCRIPTION_STATUS.PAST_DUE]: SUBSCRIPTION_STATUS.PAST_DUE,
73
+ [SUBSCRIPTION_STATUS.INCOMPLETE]: SUBSCRIPTION_STATUS.INCOMPLETE,
74
+ [SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED]: SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED,
75
+ [SUBSCRIPTION_STATUS.UNPAID]: SUBSCRIPTION_STATUS.UNPAID,
76
+ [SUBSCRIPTION_STATUS.NONE]: SUBSCRIPTION_STATUS.NONE
77
+ });
58
78
  function normalizeStatus(raw) {
59
- if (typeof raw !== "string") return "none";
79
+ if (raw == null || typeof raw !== "string") return "none";
60
80
  return STATUS_MAP[raw.toLowerCase()] ?? "none";
61
81
  }
62
82
  function normalizeBillingCycle(interval) {
@@ -67,7 +87,7 @@ function normalizeBillingCycle(interval) {
67
87
  return "monthly";
68
88
  }
69
89
 
70
- // src/infrastructure/services/firebase-billing.service.ts
90
+ // src/infrastructure/utils/firebase-helpers.util.ts
71
91
  function asString(value) {
72
92
  if (typeof value === "string") return value;
73
93
  return void 0;
@@ -79,6 +99,8 @@ function asBoolean(value) {
79
99
  function isTimestamp(value) {
80
100
  return typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate === "function";
81
101
  }
102
+
103
+ // src/infrastructure/services/firebase-billing.service.ts
82
104
  function createFirebaseAdapter(config) {
83
105
  const functions = config.functions;
84
106
  const firestore = config.firestore;
@@ -169,26 +191,26 @@ function createFirebaseAdapter(config) {
169
191
  callables.portal,
170
192
  {}
171
193
  );
172
- const url = result.url ?? result.customerPortalUrl;
173
- if (!url) throw new Error("No portal URL returned from Cloud Function");
174
- return url;
194
+ return result.url;
175
195
  }
176
196
  };
177
197
  }
178
198
 
179
- // src/infrastructure/constants/billing.constants.ts
180
- var SUBSCRIPTION_STATUS = Object.freeze({
181
- ACTIVE: "active",
182
- CANCELED: "canceled",
183
- REVOKED: "revoked",
184
- TRIALING: "trialing",
185
- PAST_DUE: "past_due",
186
- INCOMPLETE: "incomplete",
187
- INCOMPLETE_EXPIRED: "incomplete_expired",
188
- UNPAID: "unpaid",
189
- NONE: "none"
190
- });
191
- var FREE_PLAN = "free";
199
+ // src/infrastructure/utils/validations.util.ts
200
+ function normalizeUserId(userId) {
201
+ if (typeof userId !== "string") return void 0;
202
+ const trimmed = userId.trim();
203
+ return trimmed.length > 0 ? trimmed : void 0;
204
+ }
205
+ function isValidProductId(productId) {
206
+ return typeof productId === "string" && productId.trim().length > 0;
207
+ }
208
+ function isValidCheckoutUrl(url) {
209
+ return url.startsWith("https://") || url.startsWith("http://");
210
+ }
211
+ function isProductionInsecureUrl(url) {
212
+ return url.startsWith("http://") && !url.includes("localhost") && !url.includes("127.0.0.1");
213
+ }
192
214
 
193
215
  // src/presentation/components/PolarProvider.tsx
194
216
  var import_react2 = require("react");
@@ -209,18 +231,13 @@ var FREE_STATUS = {
209
231
  plan: "free",
210
232
  subscriptionStatus: "none"
211
233
  };
212
- function normalizeUserId(userId) {
213
- if (typeof userId !== "string") return void 0;
214
- const trimmed = userId.trim();
215
- return trimmed.length > 0 ? trimmed : void 0;
216
- }
217
234
  function PolarProvider({ adapter, userId, children }) {
218
235
  const [status, setStatus] = (0, import_react2.useState)(FREE_STATUS);
219
236
  const [loading, setLoading] = (0, import_react2.useState)(true);
220
237
  const adapterRef = (0, import_react2.useRef)(adapter);
221
238
  const statusRef = (0, import_react2.useRef)({ setStatus, setLoading });
222
239
  const userIdRef = (0, import_react2.useRef)(normalizeUserId(userId));
223
- const refreshAbortRef = (0, import_react2.useRef)(null);
240
+ const refreshMountedRef = (0, import_react2.useRef)(true);
224
241
  adapterRef.current = adapter;
225
242
  statusRef.current = { setStatus, setLoading };
226
243
  userIdRef.current = normalizeUserId(userId);
@@ -232,33 +249,38 @@ function PolarProvider({ adapter, userId, children }) {
232
249
  setLoading2(false);
233
250
  return;
234
251
  }
235
- refreshAbortRef.current?.abort();
236
- const ctrl = new AbortController();
237
- refreshAbortRef.current = ctrl;
252
+ refreshMountedRef.current = true;
238
253
  try {
239
254
  setLoading2(true);
240
255
  const s = await adapterRef.current.getStatus(uid);
241
- if (!ctrl.signal.aborted) setStatus2(s);
256
+ if (refreshMountedRef.current) setStatus2(s);
242
257
  } catch (err) {
243
- if (!ctrl.signal.aborted) {
258
+ if (refreshMountedRef.current) {
244
259
  console.error("[polar-billing] getStatus failed:", err);
245
260
  setStatus2(FREE_STATUS);
246
261
  }
247
262
  } finally {
248
- if (!ctrl.signal.aborted) setLoading2(false);
263
+ if (refreshMountedRef.current) setLoading2(false);
249
264
  }
250
265
  }, []);
251
266
  (0, import_react2.useEffect)(() => {
267
+ refreshMountedRef.current = true;
252
268
  refresh();
253
269
  return () => {
254
- refreshAbortRef.current?.abort();
270
+ refreshMountedRef.current = false;
255
271
  };
256
- }, [refresh]);
272
+ }, [userId]);
257
273
  const startCheckout = (0, import_react2.useCallback)(async (params) => {
274
+ if (!isValidProductId(params.productId)) {
275
+ throw new Error("[polar-billing] Invalid productId: must be a non-empty string");
276
+ }
258
277
  const uid = userIdRef.current;
259
278
  const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? void 0 });
260
- if (!result.url.startsWith("https://")) {
261
- throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https://");
279
+ if (!isValidCheckoutUrl(result.url)) {
280
+ throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https:// or http://");
281
+ }
282
+ if (isProductionInsecureUrl(result.url)) {
283
+ console.warn("[polar-billing] WARNING: Using insecure http:// URL in production environment");
262
284
  }
263
285
  window.location.href = result.url;
264
286
  }, []);
@@ -282,7 +304,6 @@ function PolarProvider({ adapter, userId, children }) {
282
304
  return result;
283
305
  },
284
306
  [refresh]
285
- // Only depends on stable refresh
286
307
  );
287
308
  const getPortalUrl = (0, import_react2.useCallback)(async () => {
288
309
  const uid = userIdRef.current;
@@ -301,7 +322,6 @@ function PolarProvider({ adapter, userId, children }) {
301
322
  getPortalUrl
302
323
  }),
303
324
  [status, loading, refresh, syncSubscription]
304
- // startCheckout, getBillingHistory, cancelSubscription, getPortalUrl are stable
305
325
  );
306
326
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PolarContext.Provider, { value, children });
307
327
  }
@@ -311,9 +331,16 @@ function PolarProvider({ adapter, userId, children }) {
311
331
  PolarContext,
312
332
  PolarProvider,
313
333
  SUBSCRIPTION_STATUS,
334
+ asBoolean,
335
+ asString,
314
336
  createFirebaseAdapter,
337
+ isProductionInsecureUrl,
338
+ isTimestamp,
339
+ isValidCheckoutUrl,
340
+ isValidProductId,
315
341
  normalizeBillingCycle,
316
342
  normalizeStatus,
343
+ normalizeUserId,
317
344
  usePolarBilling,
318
345
  useSubscription
319
346
  });
package/dist/index.mjs CHANGED
@@ -1,18 +1,31 @@
1
+ // src/infrastructure/constants/billing.constants.ts
2
+ var SUBSCRIPTION_STATUS = Object.freeze({
3
+ ACTIVE: "active",
4
+ CANCELED: "canceled",
5
+ REVOKED: "revoked",
6
+ TRIALING: "trialing",
7
+ PAST_DUE: "past_due",
8
+ INCOMPLETE: "incomplete",
9
+ INCOMPLETE_EXPIRED: "incomplete_expired",
10
+ UNPAID: "unpaid",
11
+ NONE: "none"
12
+ });
13
+ var FREE_PLAN = "free";
14
+
1
15
  // 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
- };
16
+ var STATUS_MAP = Object.freeze({
17
+ [SUBSCRIPTION_STATUS.ACTIVE]: SUBSCRIPTION_STATUS.ACTIVE,
18
+ [SUBSCRIPTION_STATUS.CANCELED]: SUBSCRIPTION_STATUS.CANCELED,
19
+ [SUBSCRIPTION_STATUS.REVOKED]: SUBSCRIPTION_STATUS.REVOKED,
20
+ [SUBSCRIPTION_STATUS.TRIALING]: SUBSCRIPTION_STATUS.TRIALING,
21
+ [SUBSCRIPTION_STATUS.PAST_DUE]: SUBSCRIPTION_STATUS.PAST_DUE,
22
+ [SUBSCRIPTION_STATUS.INCOMPLETE]: SUBSCRIPTION_STATUS.INCOMPLETE,
23
+ [SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED]: SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED,
24
+ [SUBSCRIPTION_STATUS.UNPAID]: SUBSCRIPTION_STATUS.UNPAID,
25
+ [SUBSCRIPTION_STATUS.NONE]: SUBSCRIPTION_STATUS.NONE
26
+ });
14
27
  function normalizeStatus(raw) {
15
- if (typeof raw !== "string") return "none";
28
+ if (raw == null || typeof raw !== "string") return "none";
16
29
  return STATUS_MAP[raw.toLowerCase()] ?? "none";
17
30
  }
18
31
  function normalizeBillingCycle(interval) {
@@ -23,7 +36,7 @@ function normalizeBillingCycle(interval) {
23
36
  return "monthly";
24
37
  }
25
38
 
26
- // src/infrastructure/services/firebase-billing.service.ts
39
+ // src/infrastructure/utils/firebase-helpers.util.ts
27
40
  function asString(value) {
28
41
  if (typeof value === "string") return value;
29
42
  return void 0;
@@ -35,6 +48,8 @@ function asBoolean(value) {
35
48
  function isTimestamp(value) {
36
49
  return typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate === "function";
37
50
  }
51
+
52
+ // src/infrastructure/services/firebase-billing.service.ts
38
53
  function createFirebaseAdapter(config) {
39
54
  const functions = config.functions;
40
55
  const firestore = config.firestore;
@@ -125,26 +140,26 @@ function createFirebaseAdapter(config) {
125
140
  callables.portal,
126
141
  {}
127
142
  );
128
- const url = result.url ?? result.customerPortalUrl;
129
- if (!url) throw new Error("No portal URL returned from Cloud Function");
130
- return url;
143
+ return result.url;
131
144
  }
132
145
  };
133
146
  }
134
147
 
135
- // src/infrastructure/constants/billing.constants.ts
136
- var SUBSCRIPTION_STATUS = Object.freeze({
137
- ACTIVE: "active",
138
- CANCELED: "canceled",
139
- REVOKED: "revoked",
140
- TRIALING: "trialing",
141
- PAST_DUE: "past_due",
142
- INCOMPLETE: "incomplete",
143
- INCOMPLETE_EXPIRED: "incomplete_expired",
144
- UNPAID: "unpaid",
145
- NONE: "none"
146
- });
147
- var FREE_PLAN = "free";
148
+ // src/infrastructure/utils/validations.util.ts
149
+ function normalizeUserId(userId) {
150
+ if (typeof userId !== "string") return void 0;
151
+ const trimmed = userId.trim();
152
+ return trimmed.length > 0 ? trimmed : void 0;
153
+ }
154
+ function isValidProductId(productId) {
155
+ return typeof productId === "string" && productId.trim().length > 0;
156
+ }
157
+ function isValidCheckoutUrl(url) {
158
+ return url.startsWith("https://") || url.startsWith("http://");
159
+ }
160
+ function isProductionInsecureUrl(url) {
161
+ return url.startsWith("http://") && !url.includes("localhost") && !url.includes("127.0.0.1");
162
+ }
148
163
 
149
164
  // src/presentation/components/PolarProvider.tsx
150
165
  import {
@@ -171,18 +186,13 @@ var FREE_STATUS = {
171
186
  plan: "free",
172
187
  subscriptionStatus: "none"
173
188
  };
174
- function normalizeUserId(userId) {
175
- if (typeof userId !== "string") return void 0;
176
- const trimmed = userId.trim();
177
- return trimmed.length > 0 ? trimmed : void 0;
178
- }
179
189
  function PolarProvider({ adapter, userId, children }) {
180
190
  const [status, setStatus] = useState(FREE_STATUS);
181
191
  const [loading, setLoading] = useState(true);
182
192
  const adapterRef = useRef(adapter);
183
193
  const statusRef = useRef({ setStatus, setLoading });
184
194
  const userIdRef = useRef(normalizeUserId(userId));
185
- const refreshAbortRef = useRef(null);
195
+ const refreshMountedRef = useRef(true);
186
196
  adapterRef.current = adapter;
187
197
  statusRef.current = { setStatus, setLoading };
188
198
  userIdRef.current = normalizeUserId(userId);
@@ -194,33 +204,38 @@ function PolarProvider({ adapter, userId, children }) {
194
204
  setLoading2(false);
195
205
  return;
196
206
  }
197
- refreshAbortRef.current?.abort();
198
- const ctrl = new AbortController();
199
- refreshAbortRef.current = ctrl;
207
+ refreshMountedRef.current = true;
200
208
  try {
201
209
  setLoading2(true);
202
210
  const s = await adapterRef.current.getStatus(uid);
203
- if (!ctrl.signal.aborted) setStatus2(s);
211
+ if (refreshMountedRef.current) setStatus2(s);
204
212
  } catch (err) {
205
- if (!ctrl.signal.aborted) {
213
+ if (refreshMountedRef.current) {
206
214
  console.error("[polar-billing] getStatus failed:", err);
207
215
  setStatus2(FREE_STATUS);
208
216
  }
209
217
  } finally {
210
- if (!ctrl.signal.aborted) setLoading2(false);
218
+ if (refreshMountedRef.current) setLoading2(false);
211
219
  }
212
220
  }, []);
213
221
  useEffect(() => {
222
+ refreshMountedRef.current = true;
214
223
  refresh();
215
224
  return () => {
216
- refreshAbortRef.current?.abort();
225
+ refreshMountedRef.current = false;
217
226
  };
218
- }, [refresh]);
227
+ }, [userId]);
219
228
  const startCheckout = useCallback(async (params) => {
229
+ if (!isValidProductId(params.productId)) {
230
+ throw new Error("[polar-billing] Invalid productId: must be a non-empty string");
231
+ }
220
232
  const uid = userIdRef.current;
221
233
  const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? void 0 });
222
- if (!result.url.startsWith("https://")) {
223
- throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https://");
234
+ if (!isValidCheckoutUrl(result.url)) {
235
+ throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https:// or http://");
236
+ }
237
+ if (isProductionInsecureUrl(result.url)) {
238
+ console.warn("[polar-billing] WARNING: Using insecure http:// URL in production environment");
224
239
  }
225
240
  window.location.href = result.url;
226
241
  }, []);
@@ -244,7 +259,6 @@ function PolarProvider({ adapter, userId, children }) {
244
259
  return result;
245
260
  },
246
261
  [refresh]
247
- // Only depends on stable refresh
248
262
  );
249
263
  const getPortalUrl = useCallback(async () => {
250
264
  const uid = userIdRef.current;
@@ -263,7 +277,6 @@ function PolarProvider({ adapter, userId, children }) {
263
277
  getPortalUrl
264
278
  }),
265
279
  [status, loading, refresh, syncSubscription]
266
- // startCheckout, getBillingHistory, cancelSubscription, getPortalUrl are stable
267
280
  );
268
281
  return /* @__PURE__ */ jsx(PolarContext.Provider, { value, children });
269
282
  }
@@ -272,9 +285,16 @@ export {
272
285
  PolarContext,
273
286
  PolarProvider,
274
287
  SUBSCRIPTION_STATUS,
288
+ asBoolean,
289
+ asString,
275
290
  createFirebaseAdapter,
291
+ isProductionInsecureUrl,
292
+ isTimestamp,
293
+ isValidCheckoutUrl,
294
+ isValidProductId,
276
295
  normalizeBillingCycle,
277
296
  normalizeStatus,
297
+ normalizeUserId,
278
298
  usePolarBilling,
279
299
  useSubscription
280
300
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-polar-payment",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Universal Polar.sh subscription billing — Firebase adapter",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -1,8 +1,3 @@
1
- /**
2
- * Cancellation Entity
3
- * @description Types for subscription cancellation reasons and outcomes
4
- */
5
-
6
1
  export type CancellationReason =
7
2
  | 'too_expensive'
8
3
  | 'missing_features'
@@ -1,14 +1,8 @@
1
- /**
2
- * Checkout Entity
3
- * @description Types for initiating and following process of checkouts
4
- */
5
-
6
1
  export interface CheckoutParams {
7
2
  productId: string;
8
3
  planKey?: string;
9
4
  billingCycle?: 'monthly' | 'yearly';
10
5
  successUrl?: string;
11
- /** Injected automatically by PolarProvider — do not pass manually */
12
6
  userId?: string;
13
7
  }
14
8
 
@@ -1,8 +1,3 @@
1
- /**
2
- * Order Entity
3
- * @description Types for billing history items
4
- */
5
-
6
1
  export interface OrderItem {
7
2
  id: string;
8
3
  createdAt: string;
@@ -1,8 +1,3 @@
1
- /**
2
- * Subscription Entity
3
- * @description Types for subscription status and billing cycles
4
- */
5
-
6
1
  export type SubscriptionStatusValue =
7
2
  | 'active'
8
3
  | 'canceled'
@@ -24,6 +19,5 @@ export interface SubscriptionStatus {
24
19
  currentPeriodEnd?: string;
25
20
  billingCycle?: BillingCycle;
26
21
  polarCustomerId?: string;
27
- /** Token balance (for token-based projects like Aria) */
28
22
  tokens?: number;
29
23
  }
@@ -1,8 +1,3 @@
1
- /**
2
- * Sync Entity
3
- * @description Type for subscription synchronization results
4
- */
5
-
6
1
  export interface SyncResult {
7
2
  synced: boolean;
8
3
  plan?: string;
@@ -1,7 +1,2 @@
1
- /**
2
- * Domain Layer
3
- * Subpath: @umituz/web-polar-payment/domain
4
- */
5
-
6
1
  export * from './entities';
7
2
  export * from './interfaces';
package/src/index.ts CHANGED
@@ -1,11 +1,3 @@
1
- /**
2
- * @umituz/web-polar-payment
3
- * Universal Polar.sh subscription billing — Firebase adapter
4
- *
5
- * IMPORTANT: Apps should NOT use this root barrel export.
6
- * Use subpath imports instead: "@umituz/web-polar-payment/domain"
7
- */
8
-
9
1
  export * from './domain';
10
2
  export * from './infrastructure';
11
3
  export * from './presentation';
@@ -1,9 +1,3 @@
1
- /**
2
- * Billing Constants
3
- * @description Standardized subscription states and plan names
4
- */
5
-
6
- // Object.freeze() prevents accidental mutations and enables V8 optimizations
7
1
  export const SUBSCRIPTION_STATUS = Object.freeze({
8
2
  ACTIVE: 'active' as const,
9
3
  CANCELED: 'canceled' as const,
@@ -1,8 +1,5 @@
1
- /**
2
- * Infrastructure Layer
3
- * Subpath: @umituz/web-polar-payment/infrastructure
4
- */
5
-
6
1
  export * from './services/firebase-billing.service';
7
2
  export * from './constants/billing.constants';
8
3
  export * from './utils/normalization.util';
4
+ export * from './utils/validations.util';
5
+ export * from './utils/firebase-helpers.util';
@@ -9,37 +9,8 @@ import type {
9
9
  SyncResult,
10
10
  } from '../../domain/entities';
11
11
  import { normalizeStatus, normalizeBillingCycle } from '../utils/normalization.util';
12
+ import { asString, asBoolean, isTimestamp } from '../utils/firebase-helpers.util';
12
13
 
13
- /**
14
- * Type guard to safely extract string value from unknown Firestore data
15
- */
16
- function asString(value: unknown): string | undefined {
17
- if (typeof value === 'string') return value;
18
- return undefined;
19
- }
20
-
21
- /**
22
- * Type guard to safely extract boolean value from unknown Firestore data
23
- */
24
- function asBoolean(value: unknown): boolean | undefined {
25
- if (typeof value === 'boolean') return value;
26
- return undefined;
27
- }
28
-
29
- /**
30
- * Type guard to safely check if value is an object with toDate method
31
- */
32
- function isTimestamp(value: unknown): value is { toDate(): Date } {
33
- return (
34
- typeof value === 'object' &&
35
- value !== null &&
36
- 'toDate' in value &&
37
- typeof (value as { toDate: unknown }).toDate === 'function'
38
- );
39
- }
40
-
41
- // Internal type aliases for Firebase SDK compatibility
42
- // Using 'any' internally to avoid DTS build issues with external Firebase packages
43
14
  type FirebaseFunctions = any;
44
15
  type FirebaseFirestore = any;
45
16
 
@@ -67,12 +38,7 @@ export interface FirebaseAdapterConfig {
67
38
  };
68
39
  }
69
40
 
70
- /**
71
- * Firebase Billing Service
72
- * @description Implementation of PolarAdapter for Firebase Functions and Firestore.
73
- */
74
41
  export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapter {
75
- // Cast internally to Firebase types for implementation
76
42
  const functions = config.functions as FirebaseFunctions;
77
43
  const firestore = config.firestore as FirebaseFirestore;
78
44
 
@@ -95,8 +61,6 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
95
61
  currentPeriodEnd: config.db?.currentPeriodEndField ?? 'currentPeriodEnd',
96
62
  };
97
63
 
98
- // Cache imports to avoid repeated dynamic import overhead
99
- // Reduces latency on subsequent calls and GC pressure
100
64
  let httpsCallableCache: typeof import('firebase/functions')['httpsCallable'] | null = null;
101
65
  let firestoreCache: { doc: any; getDoc: any } | null = null;
102
66
 
@@ -176,13 +140,11 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
176
140
  },
177
141
 
178
142
  async getPortalUrl(_userId: string): Promise<string> {
179
- const result = await callable<Record<string, never>, { url?: string; customerPortalUrl?: string }>(
143
+ const result = await callable<Record<string, never>, { url: string }>(
180
144
  callables.portal,
181
145
  {},
182
146
  );
183
- const url = result.url ?? result.customerPortalUrl;
184
- if (!url) throw new Error('No portal URL returned from Cloud Function');
185
- return url;
147
+ return result.url;
186
148
  },
187
149
  };
188
150
  }
@@ -0,0 +1,18 @@
1
+ export function asString(value: unknown): string | undefined {
2
+ if (typeof value === 'string') return value;
3
+ return undefined;
4
+ }
5
+
6
+ export function asBoolean(value: unknown): boolean | undefined {
7
+ if (typeof value === 'boolean') return value;
8
+ return undefined;
9
+ }
10
+
11
+ export function isTimestamp(value: unknown): value is { toDate(): Date } {
12
+ return (
13
+ typeof value === 'object' &&
14
+ value !== null &&
15
+ 'toDate' in value &&
16
+ typeof (value as { toDate: unknown }).toDate === 'function'
17
+ );
18
+ }
@@ -1,33 +1,23 @@
1
1
  import type { SubscriptionStatusValue, BillingCycle } from '../../domain/entities';
2
+ import { SUBSCRIPTION_STATUS } from '../constants/billing.constants';
2
3
 
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
- };
4
+ const STATUS_MAP: Readonly<Record<string, SubscriptionStatusValue>> = Object.freeze({
5
+ [SUBSCRIPTION_STATUS.ACTIVE]: SUBSCRIPTION_STATUS.ACTIVE,
6
+ [SUBSCRIPTION_STATUS.CANCELED]: SUBSCRIPTION_STATUS.CANCELED,
7
+ [SUBSCRIPTION_STATUS.REVOKED]: SUBSCRIPTION_STATUS.REVOKED,
8
+ [SUBSCRIPTION_STATUS.TRIALING]: SUBSCRIPTION_STATUS.TRIALING,
9
+ [SUBSCRIPTION_STATUS.PAST_DUE]: SUBSCRIPTION_STATUS.PAST_DUE,
10
+ [SUBSCRIPTION_STATUS.INCOMPLETE]: SUBSCRIPTION_STATUS.INCOMPLETE,
11
+ [SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED]: SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED,
12
+ [SUBSCRIPTION_STATUS.UNPAID]: SUBSCRIPTION_STATUS.UNPAID,
13
+ [SUBSCRIPTION_STATUS.NONE]: SUBSCRIPTION_STATUS.NONE,
14
+ });
17
15
 
18
- /**
19
- * Normalize a raw Polar status string to a known value.
20
- * @description Defaults to 'none' for unknown statuses or non-string input.
21
- */
22
16
  export function normalizeStatus(raw: string): SubscriptionStatusValue {
23
- if (typeof raw !== 'string') return 'none';
17
+ if (raw == null || typeof raw !== 'string') return 'none';
24
18
  return STATUS_MAP[raw.toLowerCase()] ?? 'none';
25
19
  }
26
20
 
27
- /**
28
- * Normalize billing interval
29
- * @description Maps 'month'/'year' to 'monthly'/'yearly'. Defaults to 'monthly' for unknown values or non-string input.
30
- */
31
21
  export function normalizeBillingCycle(interval: string): BillingCycle {
32
22
  if (typeof interval !== 'string') return 'monthly';
33
23
  const normalized = interval.toLowerCase();
@@ -0,0 +1,17 @@
1
+ export function normalizeUserId(userId: string | undefined): string | undefined {
2
+ if (typeof userId !== 'string') return undefined;
3
+ const trimmed = userId.trim();
4
+ return trimmed.length > 0 ? trimmed : undefined;
5
+ }
6
+
7
+ export function isValidProductId(productId: unknown): productId is string {
8
+ return typeof productId === 'string' && productId.trim().length > 0;
9
+ }
10
+
11
+ export function isValidCheckoutUrl(url: string): boolean {
12
+ return url.startsWith('https://') || url.startsWith('http://');
13
+ }
14
+
15
+ export function isProductionInsecureUrl(url: string): boolean {
16
+ return url.startsWith('http://') && !url.includes('localhost') && !url.includes('127.0.0.1');
17
+ }
@@ -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,28 +29,19 @@ 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);
45
35
 
46
- // Store adapter and setters in refs to create stable callbacks
47
36
  const adapterRef = useRef(adapter);
48
37
  const statusRef = useRef({ setStatus, setLoading });
49
38
  const userIdRef = useRef(normalizeUserId(userId));
50
- const refreshAbortRef = useRef<AbortController | null>(null);
39
+ const refreshMountedRef = useRef(true);
51
40
 
52
- // Keep refs in sync
53
41
  adapterRef.current = adapter;
54
42
  statusRef.current = { setStatus, setLoading };
55
43
  userIdRef.current = normalizeUserId(userId);
56
44
 
57
- // Stable refresh function with no dependencies - prevents cascading re-renders
58
45
  const refresh = useCallback(async () => {
59
46
  const uid = userIdRef.current;
60
47
  const { setStatus, setLoading } = statusRef.current;
@@ -65,37 +52,46 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
65
52
  return;
66
53
  }
67
54
 
68
- refreshAbortRef.current?.abort();
69
- const ctrl = new AbortController();
70
- refreshAbortRef.current = ctrl;
55
+ refreshMountedRef.current = true;
71
56
 
72
57
  try {
73
58
  setLoading(true);
74
59
  const s = await adapterRef.current.getStatus(uid);
75
- if (!ctrl.signal.aborted) setStatus(s);
60
+ if (refreshMountedRef.current) setStatus(s);
76
61
  } catch (err) {
77
- if (!ctrl.signal.aborted) {
62
+ if (refreshMountedRef.current) {
78
63
  console.error('[polar-billing] getStatus failed:', err);
79
64
  setStatus(FREE_STATUS);
80
65
  }
81
66
  } finally {
82
- if (!ctrl.signal.aborted) setLoading(false);
67
+ if (refreshMountedRef.current) setLoading(false);
83
68
  }
84
- }, []); // No dependencies - completely stable
69
+ }, []);
85
70
 
86
71
  useEffect(() => {
72
+ refreshMountedRef.current = true;
87
73
  refresh();
88
- return () => { refreshAbortRef.current?.abort(); };
89
- }, [refresh]); // Only re-run if refresh identity changes (never)
74
+ return () => { refreshMountedRef.current = false; };
75
+ }, [userId]);
90
76
 
91
77
  const startCheckout = useCallback(async (params: CheckoutParams) => {
78
+ if (!isValidProductId(params.productId)) {
79
+ throw new Error('[polar-billing] Invalid productId: must be a non-empty string');
80
+ }
81
+
92
82
  const uid = userIdRef.current;
93
83
  const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? undefined });
94
- if (!result.url.startsWith('https://')) {
95
- throw new Error('[polar-billing] Invalid checkout URL returned: URL must start with https://');
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');
96
91
  }
92
+
97
93
  window.location.href = result.url;
98
- }, []); // No dependencies - stable
94
+ }, []);
99
95
 
100
96
  const syncSubscription = useCallback(async (): Promise<SyncResult> => {
101
97
  const uid = userIdRef.current;
@@ -105,13 +101,13 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
105
101
  const result = await adapterRef.current.syncSubscription(uid, checkoutId);
106
102
  if (result.synced) await refresh();
107
103
  return result;
108
- }, [refresh]); // Only depends on stable refresh
104
+ }, [refresh]);
109
105
 
110
106
  const getBillingHistory = useCallback(async (): Promise<OrderItem[]> => {
111
107
  const uid = userIdRef.current;
112
108
  if (!uid) return [];
113
109
  return adapterRef.current.getBillingHistory(uid);
114
- }, []); // No dependencies - stable
110
+ }, []);
115
111
 
116
112
  const cancelSubscription = useCallback(
117
113
  async (reason?: CancellationReason): Promise<CancelResult> => {
@@ -119,17 +115,15 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
119
115
  if (result.success) await refresh();
120
116
  return result;
121
117
  },
122
- [refresh], // Only depends on stable refresh
118
+ [refresh],
123
119
  );
124
120
 
125
121
  const getPortalUrl = useCallback(async (): Promise<string> => {
126
122
  const uid = userIdRef.current;
127
123
  if (!uid) throw new Error('[polar-billing] Cannot get portal URL: No authenticated user');
128
124
  return adapterRef.current.getPortalUrl(uid);
129
- }, []); // No dependencies - stable
125
+ }, []);
130
126
 
131
- // Memoized context value - only recreates when status/loading changes
132
- // All functions are stable, so they don't trigger re-creation
133
127
  const value = useMemo(
134
128
  () => ({
135
129
  status,
@@ -141,7 +135,7 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
141
135
  cancelSubscription,
142
136
  getPortalUrl,
143
137
  }),
144
- [status, loading, refresh, syncSubscription], // startCheckout, getBillingHistory, cancelSubscription, getPortalUrl are stable
138
+ [status, loading, refresh, syncSubscription],
145
139
  );
146
140
 
147
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';