@umituz/web-polar-payment 1.0.14 → 1.0.16

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.mjs CHANGED
@@ -1,18 +1,164 @@
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"
1
+ // src/presentation/components/PolarProvider.tsx
2
+ import {
3
+ useEffect,
4
+ useState,
5
+ useCallback,
6
+ useRef,
7
+ useMemo
8
+ } from "react";
9
+
10
+ // src/presentation/hooks/usePolarBilling.ts
11
+ import { createContext, useContext } from "react";
12
+ var PolarContext = createContext(void 0);
13
+ function usePolarBilling() {
14
+ const ctx = useContext(PolarContext);
15
+ if (!ctx) throw new Error("usePolarBilling must be used within <PolarProvider>");
16
+ return ctx;
17
+ }
18
+
19
+ // src/infrastructure/utils/validations.util.ts
20
+ function normalizeUserId(userId) {
21
+ if (typeof userId !== "string") return void 0;
22
+ const trimmed = userId.trim();
23
+ return trimmed.length > 0 ? trimmed : void 0;
24
+ }
25
+ function isValidProductId(productId) {
26
+ return typeof productId === "string" && productId.trim().length > 0;
27
+ }
28
+ function isValidCheckoutUrl(url) {
29
+ return url.startsWith("https://") || url.startsWith("http://");
30
+ }
31
+ function isProductionInsecureUrl(url) {
32
+ return url.startsWith("http://") && !url.includes("localhost") && !url.includes("127.0.0.1");
33
+ }
34
+
35
+ // src/presentation/components/PolarProvider.tsx
36
+ import { jsx } from "react/jsx-runtime";
37
+ var FREE_STATUS = {
38
+ plan: "free",
39
+ subscriptionStatus: "none"
13
40
  };
41
+ function PolarProvider({ adapter, userId, children }) {
42
+ const [status, setStatus] = useState(FREE_STATUS);
43
+ const [loading, setLoading] = useState(true);
44
+ const adapterRef = useRef(adapter);
45
+ const statusRef = useRef({ setStatus, setLoading });
46
+ const userIdRef = useRef(normalizeUserId(userId));
47
+ const refreshMountedRef = useRef(true);
48
+ adapterRef.current = adapter;
49
+ statusRef.current = { setStatus, setLoading };
50
+ userIdRef.current = normalizeUserId(userId);
51
+ const refresh = useCallback(async () => {
52
+ const uid = userIdRef.current;
53
+ const { setStatus: setStatus2, setLoading: setLoading2 } = statusRef.current;
54
+ if (!uid) {
55
+ setStatus2(FREE_STATUS);
56
+ setLoading2(false);
57
+ return;
58
+ }
59
+ refreshMountedRef.current = true;
60
+ try {
61
+ setLoading2(true);
62
+ const s = await adapterRef.current.getStatus(uid);
63
+ if (refreshMountedRef.current) setStatus2(s);
64
+ } catch (err) {
65
+ if (refreshMountedRef.current) {
66
+ setStatus2(FREE_STATUS);
67
+ }
68
+ } finally {
69
+ if (refreshMountedRef.current) setLoading2(false);
70
+ }
71
+ }, []);
72
+ useEffect(() => {
73
+ refreshMountedRef.current = true;
74
+ refresh();
75
+ return () => {
76
+ refreshMountedRef.current = false;
77
+ };
78
+ }, [userId]);
79
+ const startCheckout = useCallback(async (params) => {
80
+ if (!isValidProductId(params.productId)) {
81
+ throw new Error("[polar-billing] Invalid productId: must be a non-empty string");
82
+ }
83
+ const uid = userIdRef.current;
84
+ const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? void 0 });
85
+ if (!isValidCheckoutUrl(result.url)) {
86
+ throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https:// or http://");
87
+ }
88
+ if (isProductionInsecureUrl(result.url)) {
89
+ throw new Error("[polar-billing] ERROR: Using insecure http:// URL in production environment");
90
+ }
91
+ window.location.href = result.url;
92
+ }, []);
93
+ const syncSubscription = useCallback(async () => {
94
+ const uid = userIdRef.current;
95
+ if (!uid) return { synced: false };
96
+ const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
97
+ const result = await adapterRef.current.syncSubscription(uid, checkoutId);
98
+ if (result.synced) await refresh();
99
+ return result;
100
+ }, [refresh]);
101
+ const getBillingHistory = useCallback(async () => {
102
+ const uid = userIdRef.current;
103
+ if (!uid) return [];
104
+ return adapterRef.current.getBillingHistory(uid);
105
+ }, []);
106
+ const cancelSubscription = useCallback(
107
+ async (reason) => {
108
+ const result = await adapterRef.current.cancelSubscription(reason);
109
+ if (result.success) await refresh();
110
+ return result;
111
+ },
112
+ [refresh]
113
+ );
114
+ const getPortalUrl = useCallback(async () => {
115
+ const uid = userIdRef.current;
116
+ if (!uid) throw new Error("[polar-billing] Cannot get portal URL: No authenticated user");
117
+ return adapterRef.current.getPortalUrl(uid);
118
+ }, []);
119
+ const value = useMemo(
120
+ () => ({
121
+ status,
122
+ loading,
123
+ refresh,
124
+ startCheckout,
125
+ syncSubscription,
126
+ getBillingHistory,
127
+ cancelSubscription,
128
+ getPortalUrl
129
+ }),
130
+ [status, loading, refresh, syncSubscription]
131
+ );
132
+ return /* @__PURE__ */ jsx(PolarContext.Provider, { value, children });
133
+ }
134
+
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
+
148
+ // src/infrastructure/utils/normalization.util.ts
149
+ var STATUS_MAP = Object.freeze({
150
+ [SUBSCRIPTION_STATUS.ACTIVE]: SUBSCRIPTION_STATUS.ACTIVE,
151
+ [SUBSCRIPTION_STATUS.CANCELED]: SUBSCRIPTION_STATUS.CANCELED,
152
+ [SUBSCRIPTION_STATUS.REVOKED]: SUBSCRIPTION_STATUS.REVOKED,
153
+ [SUBSCRIPTION_STATUS.TRIALING]: SUBSCRIPTION_STATUS.TRIALING,
154
+ [SUBSCRIPTION_STATUS.PAST_DUE]: SUBSCRIPTION_STATUS.PAST_DUE,
155
+ [SUBSCRIPTION_STATUS.INCOMPLETE]: SUBSCRIPTION_STATUS.INCOMPLETE,
156
+ [SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED]: SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED,
157
+ [SUBSCRIPTION_STATUS.UNPAID]: SUBSCRIPTION_STATUS.UNPAID,
158
+ [SUBSCRIPTION_STATUS.NONE]: SUBSCRIPTION_STATUS.NONE
159
+ });
14
160
  function normalizeStatus(raw) {
15
- if (typeof raw !== "string") return "none";
161
+ if (raw == null || typeof raw !== "string") return "none";
16
162
  return STATUS_MAP[raw.toLowerCase()] ?? "none";
17
163
  }
18
164
  function normalizeBillingCycle(interval) {
@@ -23,7 +169,7 @@ function normalizeBillingCycle(interval) {
23
169
  return "monthly";
24
170
  }
25
171
 
26
- // src/infrastructure/services/firebase-billing.service.ts
172
+ // src/infrastructure/utils/firebase-helpers.util.ts
27
173
  function asString(value) {
28
174
  if (typeof value === "string") return value;
29
175
  return void 0;
@@ -33,8 +179,10 @@ function asBoolean(value) {
33
179
  return void 0;
34
180
  }
35
181
  function isTimestamp(value) {
36
- return typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate === "function";
182
+ return typeof value === "object" && value !== null && "toDate" in value && "function" === typeof value.toDate;
37
183
  }
184
+
185
+ // src/infrastructure/services/firebase-billing.service.ts
38
186
  function createFirebaseAdapter(config) {
39
187
  const functions = config.functions;
40
188
  const firestore = config.firestore;
@@ -60,7 +208,7 @@ function createFirebaseAdapter(config) {
60
208
  async function getHttpsCallable() {
61
209
  if (!httpsCallableCache) {
62
210
  const mod = await import("firebase/functions");
63
- httpsCallableCache = mod.httpsCallable;
211
+ httpsCallableCache = mod.httpsCallable(functions);
64
212
  }
65
213
  return httpsCallableCache;
66
214
  }
@@ -73,15 +221,17 @@ function createFirebaseAdapter(config) {
73
221
  }
74
222
  async function callable(name, data) {
75
223
  const httpsCallable = await getHttpsCallable();
76
- const fn = httpsCallable(functions, name);
224
+ const fn = httpsCallable(name);
77
225
  const result = await fn(data);
78
226
  return result.data;
79
227
  }
80
228
  return {
81
229
  async getStatus(userId) {
82
230
  const { doc, getDoc } = await getFirestore();
83
- const snap = await getDoc(doc(firestore, db.collection, userId));
84
- if (!snap.exists()) {
231
+ const docRef = doc(firestore, db.collection, userId);
232
+ const snap = await getDoc(docRef);
233
+ const exists = snap.exists;
234
+ if (!exists) {
85
235
  return { plan: "free", subscriptionStatus: "none" };
86
236
  }
87
237
  const d = snap.data();
@@ -107,174 +257,31 @@ function createFirebaseAdapter(config) {
107
257
  async createCheckout(params) {
108
258
  return callable(callables.createCheckout, params);
109
259
  },
110
- async syncSubscription(_userId, _checkoutId) {
111
- return callable(callables.sync, {});
260
+ async syncSubscription(userId, checkoutId) {
261
+ return callable(callables.sync, { userId, checkoutId });
112
262
  },
113
- async getBillingHistory(_userId) {
263
+ async getBillingHistory(userId) {
114
264
  const result = await callable(
115
265
  callables.billing,
116
- {}
266
+ { userId }
117
267
  );
118
268
  return result.orders ?? [];
119
269
  },
120
270
  async cancelSubscription(reason) {
121
271
  return callable(callables.cancel, { reason });
122
272
  },
123
- async getPortalUrl(_userId) {
273
+ async getPortalUrl(userId) {
124
274
  const result = await callable(
125
275
  callables.portal,
126
- {}
276
+ { userId }
127
277
  );
128
- const url = result.url ?? result.customerPortalUrl;
129
- if (!url) throw new Error("No portal URL returned from Cloud Function");
130
- return url;
278
+ return result.url;
131
279
  }
132
280
  };
133
281
  }
134
-
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
-
149
- // src/presentation/components/PolarProvider.tsx
150
- import {
151
- useEffect,
152
- useState,
153
- useCallback,
154
- useRef,
155
- useMemo
156
- } from "react";
157
-
158
- // src/presentation/hooks/usePolarBilling.ts
159
- import { createContext, useContext } from "react";
160
- var PolarContext = createContext(void 0);
161
- function usePolarBilling() {
162
- const ctx = useContext(PolarContext);
163
- if (!ctx) throw new Error("usePolarBilling must be used within <PolarProvider>");
164
- return ctx;
165
- }
166
- var useSubscription = usePolarBilling;
167
-
168
- // src/presentation/components/PolarProvider.tsx
169
- import { jsx } from "react/jsx-runtime";
170
- var FREE_STATUS = {
171
- plan: "free",
172
- subscriptionStatus: "none"
173
- };
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
- function PolarProvider({ adapter, userId, children }) {
180
- const [status, setStatus] = useState(FREE_STATUS);
181
- const [loading, setLoading] = useState(true);
182
- const adapterRef = useRef(adapter);
183
- const statusRef = useRef({ setStatus, setLoading });
184
- const userIdRef = useRef(normalizeUserId(userId));
185
- const refreshAbortRef = useRef(null);
186
- adapterRef.current = adapter;
187
- statusRef.current = { setStatus, setLoading };
188
- userIdRef.current = normalizeUserId(userId);
189
- const refresh = useCallback(async () => {
190
- const uid = userIdRef.current;
191
- const { setStatus: setStatus2, setLoading: setLoading2 } = statusRef.current;
192
- if (!uid) {
193
- setStatus2(FREE_STATUS);
194
- setLoading2(false);
195
- return;
196
- }
197
- refreshAbortRef.current?.abort();
198
- const ctrl = new AbortController();
199
- refreshAbortRef.current = ctrl;
200
- try {
201
- setLoading2(true);
202
- const s = await adapterRef.current.getStatus(uid);
203
- if (!ctrl.signal.aborted) setStatus2(s);
204
- } catch (err) {
205
- if (!ctrl.signal.aborted) {
206
- console.error("[polar-billing] getStatus failed:", err);
207
- setStatus2(FREE_STATUS);
208
- }
209
- } finally {
210
- if (!ctrl.signal.aborted) setLoading2(false);
211
- }
212
- }, []);
213
- useEffect(() => {
214
- refresh();
215
- return () => {
216
- refreshAbortRef.current?.abort();
217
- };
218
- }, [refresh]);
219
- const startCheckout = useCallback(async (params) => {
220
- const uid = userIdRef.current;
221
- 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://");
224
- }
225
- window.location.href = result.url;
226
- }, []);
227
- const syncSubscription = useCallback(async () => {
228
- const uid = userIdRef.current;
229
- if (!uid) return { synced: false };
230
- const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
231
- const result = await adapterRef.current.syncSubscription(uid, checkoutId);
232
- if (result.synced) await refresh();
233
- return result;
234
- }, [refresh]);
235
- const getBillingHistory = useCallback(async () => {
236
- const uid = userIdRef.current;
237
- if (!uid) return [];
238
- return adapterRef.current.getBillingHistory(uid);
239
- }, []);
240
- const cancelSubscription = useCallback(
241
- async (reason) => {
242
- const result = await adapterRef.current.cancelSubscription(reason);
243
- if (result.success) await refresh();
244
- return result;
245
- },
246
- [refresh]
247
- // Only depends on stable refresh
248
- );
249
- const getPortalUrl = useCallback(async () => {
250
- const uid = userIdRef.current;
251
- if (!uid) throw new Error("[polar-billing] Cannot get portal URL: No authenticated user");
252
- return adapterRef.current.getPortalUrl(uid);
253
- }, []);
254
- const value = useMemo(
255
- () => ({
256
- status,
257
- loading,
258
- refresh,
259
- startCheckout,
260
- syncSubscription,
261
- getBillingHistory,
262
- cancelSubscription,
263
- getPortalUrl
264
- }),
265
- [status, loading, refresh, syncSubscription]
266
- // startCheckout, getBillingHistory, cancelSubscription, getPortalUrl are stable
267
- );
268
- return /* @__PURE__ */ jsx(PolarContext.Provider, { value, children });
269
- }
270
282
  export {
271
- FREE_PLAN,
272
- PolarContext,
273
283
  PolarProvider,
274
284
  SUBSCRIPTION_STATUS,
275
285
  createFirebaseAdapter,
276
- normalizeBillingCycle,
277
- normalizeStatus,
278
- usePolarBilling,
279
- useSubscription
286
+ usePolarBilling
280
287
  };
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.16",
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';
@@ -8,14 +8,9 @@ import type {
8
8
  SyncResult,
9
9
  } from '../entities';
10
10
 
11
- /**
12
- * Backend-agnostic interface every adapter must implement.
13
- * @description Contract for Polar billing adapters (Firebase, Supabase, etc.)
14
- */
15
11
  export interface PolarAdapter {
16
12
  getStatus(userId: string): Promise<SubscriptionStatus>;
17
13
  createCheckout(params: CheckoutParams): Promise<CheckoutResult>;
18
- /** checkoutId is read from URL by the context and passed explicitly */
19
14
  syncSubscription(userId: string, checkoutId?: string): Promise<SyncResult>;
20
15
  getBillingHistory(userId: string): Promise<OrderItem[]>;
21
16
  cancelSubscription(reason?: CancellationReason): Promise<CancelResult>;
package/src/index.ts CHANGED
@@ -1,11 +1,9 @@
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
- export * from './domain';
10
- export * from './infrastructure';
11
- export * from './presentation';
1
+ export { PolarProvider } from './presentation/components/PolarProvider';
2
+ export { usePolarBilling } from './presentation/hooks/usePolarBilling';
3
+ export type { PolarContextValue } from './presentation/hooks/usePolarBilling';
4
+ export { createFirebaseAdapter } from './infrastructure/services/firebase-billing.service';
5
+ export type { FirebaseAdapterConfig } from './infrastructure/services/firebase-billing.service';
6
+ export { SUBSCRIPTION_STATUS } from './infrastructure/constants/billing.constants';
7
+ export type { SubscriptionStatusValue, BillingCycle } from './domain/entities/subscription.entity';
8
+ export type { SubscriptionStatus, CheckoutParams, CheckoutResult, OrderItem, CancellationReason, CancelResult, SyncResult } from './domain/entities';
9
+ export type { PolarAdapter } from './domain/interfaces';
@@ -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,
@@ -25,5 +19,3 @@ export const SUBSCRIPTION_STATUS = Object.freeze({
25
19
  UNPAID: 'unpaid';
26
20
  NONE: 'none';
27
21
  }>;
28
-
29
- export const FREE_PLAN = 'free';
@@ -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';