@umituz/web-polar-payment 1.0.0

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.
@@ -0,0 +1,174 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as react from 'react';
3
+ import { ReactNode } from 'react';
4
+
5
+ /**
6
+ * Subscription Entity
7
+ * @description Types for subscription status and billing cycles
8
+ */
9
+ type SubscriptionStatusValue = 'active' | 'canceled' | 'revoked' | 'trialing' | 'past_due' | 'incomplete' | 'incomplete_expired' | 'unpaid' | 'none';
10
+ type BillingCycle = 'monthly' | 'yearly';
11
+ interface SubscriptionStatus {
12
+ plan: string;
13
+ subscriptionId?: string;
14
+ subscriptionStatus: SubscriptionStatusValue;
15
+ cancelAtPeriodEnd?: boolean;
16
+ currentPeriodEnd?: string;
17
+ billingCycle?: BillingCycle;
18
+ polarCustomerId?: string;
19
+ /** Token balance (for token-based projects like Aria) */
20
+ tokens?: number;
21
+ }
22
+
23
+ /**
24
+ * Order Entity
25
+ * @description Types for billing history items
26
+ */
27
+ interface OrderItem {
28
+ id: string;
29
+ createdAt: string;
30
+ amount: number;
31
+ currency: string;
32
+ status: string;
33
+ paid: boolean;
34
+ productName: string;
35
+ invoiceUrl?: string;
36
+ }
37
+
38
+ /**
39
+ * Checkout Entity
40
+ * @description Types for initiating and following process of checkouts
41
+ */
42
+ interface CheckoutParams {
43
+ productId: string;
44
+ planKey?: string;
45
+ billingCycle?: 'monthly' | 'yearly';
46
+ successUrl?: string;
47
+ /** Injected automatically by PolarProvider — do not pass manually */
48
+ userId?: string;
49
+ }
50
+ interface CheckoutResult {
51
+ url: string;
52
+ id: string;
53
+ }
54
+
55
+ /**
56
+ * Cancellation Entity
57
+ * @description Types for subscription cancellation reasons and outcomes
58
+ */
59
+ type CancellationReason = 'too_expensive' | 'missing_features' | 'switched_service' | 'unused' | 'customer_service' | 'low_quality' | 'too_complex' | 'other';
60
+ interface CancelResult {
61
+ success: boolean;
62
+ endsAt?: string;
63
+ }
64
+
65
+ /**
66
+ * Sync Entity
67
+ * @description Type for subscription synchronization results
68
+ */
69
+ interface SyncResult {
70
+ synced: boolean;
71
+ plan?: string;
72
+ }
73
+
74
+ /**
75
+ * Backend-agnostic interface every adapter must implement.
76
+ * @description Contract for Polar billing adapters (Firebase, Supabase, etc.)
77
+ */
78
+ interface PolarAdapter {
79
+ getStatus(userId: string): Promise<SubscriptionStatus>;
80
+ createCheckout(params: CheckoutParams): Promise<CheckoutResult>;
81
+ /** checkoutId is read from URL by the context and passed explicitly */
82
+ syncSubscription(userId: string, checkoutId?: string): Promise<SyncResult>;
83
+ getBillingHistory(userId: string): Promise<OrderItem[]>;
84
+ cancelSubscription(reason?: CancellationReason): Promise<CancelResult>;
85
+ getPortalUrl(userId: string): Promise<string>;
86
+ }
87
+
88
+ type FirebaseFunctions = any;
89
+ type FirebaseFirestore = any;
90
+ interface FirebaseAdapterConfig {
91
+ functions: FirebaseFunctions;
92
+ firestore: FirebaseFirestore;
93
+ callables?: {
94
+ createCheckout?: string;
95
+ syncSubscription?: string;
96
+ getBillingHistory?: string;
97
+ cancelSubscription?: string;
98
+ getPortalUrl?: string;
99
+ };
100
+ db?: {
101
+ usersCollection?: string;
102
+ planField?: string;
103
+ billingCycleField?: string;
104
+ subscriptionIdField?: string;
105
+ subscriptionStatusField?: string;
106
+ polarCustomerIdField?: string;
107
+ cancelAtPeriodEndField?: string;
108
+ currentPeriodEndField?: string;
109
+ };
110
+ }
111
+ /**
112
+ * Firebase Billing Service
113
+ * @description Implementation of PolarAdapter for Firebase Functions and Firestore.
114
+ */
115
+ declare function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapter;
116
+
117
+ /**
118
+ * Billing Constants
119
+ * @description Standardized subscription states and plan names
120
+ */
121
+ declare const SUBSCRIPTION_STATUS: {
122
+ ACTIVE: "active";
123
+ CANCELED: "canceled";
124
+ REVOKED: "revoked";
125
+ TRIALING: "trialing";
126
+ PAST_DUE: "past_due";
127
+ INCOMPLETE: "incomplete";
128
+ INCOMPLETE_EXPIRED: "incomplete_expired";
129
+ UNPAID: "unpaid";
130
+ NONE: "none";
131
+ };
132
+ declare const FREE_PLAN = "free";
133
+
134
+ /**
135
+ * Normalize a raw Polar status string to a known value.
136
+ * @description Defaults to 'none' for unknown statuses.
137
+ */
138
+ declare function normalizeStatus(raw: string): SubscriptionStatusValue;
139
+ /**
140
+ * Normalize billing interval
141
+ * @description Maps 'month'/'year' to 'monthly'/'yearly'
142
+ */
143
+ declare function normalizeBillingCycle(interval: string): BillingCycle;
144
+
145
+ /**
146
+ * PolarProvider Component
147
+ * @description Context provider for Polar billing management.
148
+ */
149
+ interface PolarProviderProps {
150
+ adapter: PolarAdapter;
151
+ userId?: string;
152
+ children: ReactNode;
153
+ }
154
+ declare function PolarProvider({ adapter, userId, children }: PolarProviderProps): react_jsx_runtime.JSX.Element;
155
+
156
+ interface PolarContextValue {
157
+ status: SubscriptionStatus;
158
+ loading: boolean;
159
+ refresh: () => Promise<void>;
160
+ startCheckout: (params: CheckoutParams) => Promise<void>;
161
+ syncSubscription: () => Promise<SyncResult>;
162
+ getBillingHistory: () => Promise<OrderItem[]>;
163
+ cancelSubscription: (reason?: CancellationReason) => Promise<CancelResult>;
164
+ getPortalUrl: () => Promise<string>;
165
+ }
166
+ declare const PolarContext: react.Context<PolarContextValue | undefined>;
167
+ /**
168
+ * usePolarBilling Hook
169
+ * @description Hook to access Polar billing context
170
+ */
171
+ declare function usePolarBilling(): PolarContextValue;
172
+ declare const useSubscription: typeof usePolarBilling;
173
+
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 };
@@ -0,0 +1,174 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as react from 'react';
3
+ import { ReactNode } from 'react';
4
+
5
+ /**
6
+ * Subscription Entity
7
+ * @description Types for subscription status and billing cycles
8
+ */
9
+ type SubscriptionStatusValue = 'active' | 'canceled' | 'revoked' | 'trialing' | 'past_due' | 'incomplete' | 'incomplete_expired' | 'unpaid' | 'none';
10
+ type BillingCycle = 'monthly' | 'yearly';
11
+ interface SubscriptionStatus {
12
+ plan: string;
13
+ subscriptionId?: string;
14
+ subscriptionStatus: SubscriptionStatusValue;
15
+ cancelAtPeriodEnd?: boolean;
16
+ currentPeriodEnd?: string;
17
+ billingCycle?: BillingCycle;
18
+ polarCustomerId?: string;
19
+ /** Token balance (for token-based projects like Aria) */
20
+ tokens?: number;
21
+ }
22
+
23
+ /**
24
+ * Order Entity
25
+ * @description Types for billing history items
26
+ */
27
+ interface OrderItem {
28
+ id: string;
29
+ createdAt: string;
30
+ amount: number;
31
+ currency: string;
32
+ status: string;
33
+ paid: boolean;
34
+ productName: string;
35
+ invoiceUrl?: string;
36
+ }
37
+
38
+ /**
39
+ * Checkout Entity
40
+ * @description Types for initiating and following process of checkouts
41
+ */
42
+ interface CheckoutParams {
43
+ productId: string;
44
+ planKey?: string;
45
+ billingCycle?: 'monthly' | 'yearly';
46
+ successUrl?: string;
47
+ /** Injected automatically by PolarProvider — do not pass manually */
48
+ userId?: string;
49
+ }
50
+ interface CheckoutResult {
51
+ url: string;
52
+ id: string;
53
+ }
54
+
55
+ /**
56
+ * Cancellation Entity
57
+ * @description Types for subscription cancellation reasons and outcomes
58
+ */
59
+ type CancellationReason = 'too_expensive' | 'missing_features' | 'switched_service' | 'unused' | 'customer_service' | 'low_quality' | 'too_complex' | 'other';
60
+ interface CancelResult {
61
+ success: boolean;
62
+ endsAt?: string;
63
+ }
64
+
65
+ /**
66
+ * Sync Entity
67
+ * @description Type for subscription synchronization results
68
+ */
69
+ interface SyncResult {
70
+ synced: boolean;
71
+ plan?: string;
72
+ }
73
+
74
+ /**
75
+ * Backend-agnostic interface every adapter must implement.
76
+ * @description Contract for Polar billing adapters (Firebase, Supabase, etc.)
77
+ */
78
+ interface PolarAdapter {
79
+ getStatus(userId: string): Promise<SubscriptionStatus>;
80
+ createCheckout(params: CheckoutParams): Promise<CheckoutResult>;
81
+ /** checkoutId is read from URL by the context and passed explicitly */
82
+ syncSubscription(userId: string, checkoutId?: string): Promise<SyncResult>;
83
+ getBillingHistory(userId: string): Promise<OrderItem[]>;
84
+ cancelSubscription(reason?: CancellationReason): Promise<CancelResult>;
85
+ getPortalUrl(userId: string): Promise<string>;
86
+ }
87
+
88
+ type FirebaseFunctions = any;
89
+ type FirebaseFirestore = any;
90
+ interface FirebaseAdapterConfig {
91
+ functions: FirebaseFunctions;
92
+ firestore: FirebaseFirestore;
93
+ callables?: {
94
+ createCheckout?: string;
95
+ syncSubscription?: string;
96
+ getBillingHistory?: string;
97
+ cancelSubscription?: string;
98
+ getPortalUrl?: string;
99
+ };
100
+ db?: {
101
+ usersCollection?: string;
102
+ planField?: string;
103
+ billingCycleField?: string;
104
+ subscriptionIdField?: string;
105
+ subscriptionStatusField?: string;
106
+ polarCustomerIdField?: string;
107
+ cancelAtPeriodEndField?: string;
108
+ currentPeriodEndField?: string;
109
+ };
110
+ }
111
+ /**
112
+ * Firebase Billing Service
113
+ * @description Implementation of PolarAdapter for Firebase Functions and Firestore.
114
+ */
115
+ declare function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapter;
116
+
117
+ /**
118
+ * Billing Constants
119
+ * @description Standardized subscription states and plan names
120
+ */
121
+ declare const SUBSCRIPTION_STATUS: {
122
+ ACTIVE: "active";
123
+ CANCELED: "canceled";
124
+ REVOKED: "revoked";
125
+ TRIALING: "trialing";
126
+ PAST_DUE: "past_due";
127
+ INCOMPLETE: "incomplete";
128
+ INCOMPLETE_EXPIRED: "incomplete_expired";
129
+ UNPAID: "unpaid";
130
+ NONE: "none";
131
+ };
132
+ declare const FREE_PLAN = "free";
133
+
134
+ /**
135
+ * Normalize a raw Polar status string to a known value.
136
+ * @description Defaults to 'none' for unknown statuses.
137
+ */
138
+ declare function normalizeStatus(raw: string): SubscriptionStatusValue;
139
+ /**
140
+ * Normalize billing interval
141
+ * @description Maps 'month'/'year' to 'monthly'/'yearly'
142
+ */
143
+ declare function normalizeBillingCycle(interval: string): BillingCycle;
144
+
145
+ /**
146
+ * PolarProvider Component
147
+ * @description Context provider for Polar billing management.
148
+ */
149
+ interface PolarProviderProps {
150
+ adapter: PolarAdapter;
151
+ userId?: string;
152
+ children: ReactNode;
153
+ }
154
+ declare function PolarProvider({ adapter, userId, children }: PolarProviderProps): react_jsx_runtime.JSX.Element;
155
+
156
+ interface PolarContextValue {
157
+ status: SubscriptionStatus;
158
+ loading: boolean;
159
+ refresh: () => Promise<void>;
160
+ startCheckout: (params: CheckoutParams) => Promise<void>;
161
+ syncSubscription: () => Promise<SyncResult>;
162
+ getBillingHistory: () => Promise<OrderItem[]>;
163
+ cancelSubscription: (reason?: CancellationReason) => Promise<CancelResult>;
164
+ getPortalUrl: () => Promise<string>;
165
+ }
166
+ declare const PolarContext: react.Context<PolarContextValue | undefined>;
167
+ /**
168
+ * usePolarBilling Hook
169
+ * @description Hook to access Polar billing context
170
+ */
171
+ declare function usePolarBilling(): PolarContextValue;
172
+ declare const useSubscription: typeof usePolarBilling;
173
+
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 };
package/dist/index.js ADDED
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ FREE_PLAN: () => FREE_PLAN,
34
+ PolarContext: () => PolarContext,
35
+ PolarProvider: () => PolarProvider,
36
+ SUBSCRIPTION_STATUS: () => SUBSCRIPTION_STATUS,
37
+ createFirebaseAdapter: () => createFirebaseAdapter,
38
+ normalizeBillingCycle: () => normalizeBillingCycle,
39
+ normalizeStatus: () => normalizeStatus,
40
+ usePolarBilling: () => usePolarBilling,
41
+ useSubscription: () => useSubscription
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // src/infrastructure/utils/normalization.util.ts
46
+ 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
+ return map[raw?.toLowerCase()] ?? "none";
60
+ }
61
+ function normalizeBillingCycle(interval) {
62
+ if (interval === "month" || interval === "monthly") return "monthly";
63
+ if (interval === "year" || interval === "yearly") return "yearly";
64
+ return "monthly";
65
+ }
66
+
67
+ // src/infrastructure/services/firebase-billing.service.ts
68
+ function createFirebaseAdapter(config) {
69
+ const callables = {
70
+ createCheckout: config.callables?.createCheckout ?? "createCheckoutSession",
71
+ sync: config.callables?.syncSubscription ?? "syncSubscription",
72
+ billing: config.callables?.getBillingHistory ?? "getBillingHistory",
73
+ cancel: config.callables?.cancelSubscription ?? "cancelSubscription",
74
+ portal: config.callables?.getPortalUrl ?? "getCustomerPortalUrl"
75
+ };
76
+ const db = {
77
+ collection: config.db?.usersCollection ?? "users",
78
+ plan: config.db?.planField ?? "plan",
79
+ billingCycle: config.db?.billingCycleField ?? "billingCycle",
80
+ subscriptionId: config.db?.subscriptionIdField ?? "subscriptionId",
81
+ subscriptionStatus: config.db?.subscriptionStatusField ?? "subscriptionStatus",
82
+ polarCustomerId: config.db?.polarCustomerIdField ?? "polarCustomerId",
83
+ cancelAtPeriodEnd: config.db?.cancelAtPeriodEndField ?? "cancelAtPeriodEnd",
84
+ currentPeriodEnd: config.db?.currentPeriodEndField ?? "currentPeriodEnd"
85
+ };
86
+ async function callable(name, data) {
87
+ const { httpsCallable } = await import("firebase/functions");
88
+ const fn = httpsCallable(config.functions, name);
89
+ const result = await fn(data);
90
+ return result.data;
91
+ }
92
+ return {
93
+ async getStatus(userId) {
94
+ const { doc, getDoc } = await import("firebase/firestore");
95
+ const snap = await getDoc(doc(config.firestore, db.collection, userId));
96
+ if (!snap.exists()) {
97
+ return { plan: "free", subscriptionStatus: "none" };
98
+ }
99
+ const d = snap.data();
100
+ let currentPeriodEnd;
101
+ const rawEnd = d[db.currentPeriodEnd];
102
+ if (rawEnd != null) {
103
+ if (typeof rawEnd === "object" && "toDate" in rawEnd) {
104
+ currentPeriodEnd = rawEnd.toDate().toISOString();
105
+ } else if (typeof rawEnd === "string") {
106
+ currentPeriodEnd = rawEnd;
107
+ }
108
+ }
109
+ return {
110
+ plan: d[db.plan] ?? "free",
111
+ billingCycle: normalizeBillingCycle(d[db.billingCycle] ?? "monthly"),
112
+ subscriptionId: d[db.subscriptionId],
113
+ subscriptionStatus: normalizeStatus(d[db.subscriptionStatus] ?? "none"),
114
+ polarCustomerId: d[db.polarCustomerId],
115
+ cancelAtPeriodEnd: d[db.cancelAtPeriodEnd],
116
+ currentPeriodEnd
117
+ };
118
+ },
119
+ async createCheckout(params) {
120
+ return callable(callables.createCheckout, params);
121
+ },
122
+ async syncSubscription(_userId, _checkoutId) {
123
+ return callable(callables.sync, {});
124
+ },
125
+ async getBillingHistory(_userId) {
126
+ const result = await callable(
127
+ callables.billing,
128
+ {}
129
+ );
130
+ return result.orders ?? [];
131
+ },
132
+ async cancelSubscription(reason) {
133
+ return callable(callables.cancel, { reason });
134
+ },
135
+ async getPortalUrl(_userId) {
136
+ const result = await callable(
137
+ callables.portal,
138
+ {}
139
+ );
140
+ const url = result.url ?? result.customerPortalUrl;
141
+ if (!url) throw new Error("No portal URL returned from Cloud Function");
142
+ return url;
143
+ }
144
+ };
145
+ }
146
+
147
+ // src/infrastructure/constants/billing.constants.ts
148
+ var SUBSCRIPTION_STATUS = {
149
+ ACTIVE: "active",
150
+ CANCELED: "canceled",
151
+ REVOKED: "revoked",
152
+ TRIALING: "trialing",
153
+ PAST_DUE: "past_due",
154
+ INCOMPLETE: "incomplete",
155
+ INCOMPLETE_EXPIRED: "incomplete_expired",
156
+ UNPAID: "unpaid",
157
+ NONE: "none"
158
+ };
159
+ var FREE_PLAN = "free";
160
+
161
+ // src/presentation/components/PolarProvider.tsx
162
+ var import_react2 = require("react");
163
+
164
+ // src/presentation/hooks/usePolarBilling.ts
165
+ var import_react = require("react");
166
+ var PolarContext = (0, import_react.createContext)(void 0);
167
+ function usePolarBilling() {
168
+ const ctx = (0, import_react.useContext)(PolarContext);
169
+ if (!ctx) throw new Error("usePolarBilling must be used within <PolarProvider>");
170
+ return ctx;
171
+ }
172
+ var useSubscription = usePolarBilling;
173
+
174
+ // src/presentation/components/PolarProvider.tsx
175
+ var import_jsx_runtime = require("react/jsx-runtime");
176
+ var FREE_STATUS = {
177
+ plan: "free",
178
+ subscriptionStatus: "none"
179
+ };
180
+ function PolarProvider({ adapter, userId, children }) {
181
+ const [status, setStatus] = (0, import_react2.useState)(FREE_STATUS);
182
+ const [loading, setLoading] = (0, import_react2.useState)(true);
183
+ const adapterRef = (0, import_react2.useRef)(adapter);
184
+ adapterRef.current = adapter;
185
+ const refreshAbortRef = (0, import_react2.useRef)(null);
186
+ const refresh = (0, import_react2.useCallback)(async () => {
187
+ const uid = userId?.trim();
188
+ if (!uid) {
189
+ setStatus(FREE_STATUS);
190
+ setLoading(false);
191
+ return;
192
+ }
193
+ refreshAbortRef.current?.abort();
194
+ const ctrl = new AbortController();
195
+ refreshAbortRef.current = ctrl;
196
+ try {
197
+ setLoading(true);
198
+ const s = await adapterRef.current.getStatus(uid);
199
+ if (!ctrl.signal.aborted) setStatus(s);
200
+ } catch (err) {
201
+ if (!ctrl.signal.aborted) {
202
+ console.error("[polar-billing] getStatus failed:", err);
203
+ setStatus(FREE_STATUS);
204
+ }
205
+ } finally {
206
+ if (!ctrl.signal.aborted) setLoading(false);
207
+ }
208
+ }, [userId]);
209
+ (0, import_react2.useEffect)(() => {
210
+ refresh();
211
+ return () => {
212
+ refreshAbortRef.current?.abort();
213
+ };
214
+ }, [refresh]);
215
+ const startCheckout = (0, import_react2.useCallback)(async (params) => {
216
+ const result = await adapterRef.current.createCheckout({ ...params, userId: userId?.trim() });
217
+ if (!result.url.startsWith("https://")) {
218
+ throw new Error("Invalid checkout URL returned");
219
+ }
220
+ window.location.href = result.url;
221
+ }, [userId]);
222
+ const syncSubscription = (0, import_react2.useCallback)(async () => {
223
+ const uid = userId?.trim();
224
+ if (!uid) return { synced: false };
225
+ const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
226
+ const result = await adapterRef.current.syncSubscription(uid, checkoutId);
227
+ if (result.synced) await refresh();
228
+ return result;
229
+ }, [userId, refresh]);
230
+ const getBillingHistory = (0, import_react2.useCallback)(async () => {
231
+ const uid = userId?.trim();
232
+ if (!uid) return [];
233
+ return adapterRef.current.getBillingHistory(uid);
234
+ }, [userId]);
235
+ const cancelSubscription = (0, import_react2.useCallback)(
236
+ async (reason) => {
237
+ const result = await adapterRef.current.cancelSubscription(reason);
238
+ if (result.success) await refresh();
239
+ return result;
240
+ },
241
+ [refresh]
242
+ );
243
+ const getPortalUrl = (0, import_react2.useCallback)(async () => {
244
+ const uid = userId?.trim();
245
+ if (!uid) throw new Error("No authenticated user");
246
+ return adapterRef.current.getPortalUrl(uid);
247
+ }, [userId]);
248
+ const value = (0, import_react2.useMemo)(
249
+ () => ({
250
+ status,
251
+ loading,
252
+ refresh,
253
+ startCheckout,
254
+ syncSubscription,
255
+ getBillingHistory,
256
+ cancelSubscription,
257
+ getPortalUrl
258
+ }),
259
+ [status, loading, refresh, startCheckout, syncSubscription, getBillingHistory, cancelSubscription, getPortalUrl]
260
+ );
261
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PolarContext.Provider, { value, children });
262
+ }
263
+ // Annotate the CommonJS export names for ESM import in node:
264
+ 0 && (module.exports = {
265
+ FREE_PLAN,
266
+ PolarContext,
267
+ PolarProvider,
268
+ SUBSCRIPTION_STATUS,
269
+ createFirebaseAdapter,
270
+ normalizeBillingCycle,
271
+ normalizeStatus,
272
+ usePolarBilling,
273
+ useSubscription
274
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,235 @@
1
+ // src/infrastructure/utils/normalization.util.ts
2
+ 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
+ return map[raw?.toLowerCase()] ?? "none";
16
+ }
17
+ function normalizeBillingCycle(interval) {
18
+ if (interval === "month" || interval === "monthly") return "monthly";
19
+ if (interval === "year" || interval === "yearly") return "yearly";
20
+ return "monthly";
21
+ }
22
+
23
+ // src/infrastructure/services/firebase-billing.service.ts
24
+ function createFirebaseAdapter(config) {
25
+ const callables = {
26
+ createCheckout: config.callables?.createCheckout ?? "createCheckoutSession",
27
+ sync: config.callables?.syncSubscription ?? "syncSubscription",
28
+ billing: config.callables?.getBillingHistory ?? "getBillingHistory",
29
+ cancel: config.callables?.cancelSubscription ?? "cancelSubscription",
30
+ portal: config.callables?.getPortalUrl ?? "getCustomerPortalUrl"
31
+ };
32
+ const db = {
33
+ collection: config.db?.usersCollection ?? "users",
34
+ plan: config.db?.planField ?? "plan",
35
+ billingCycle: config.db?.billingCycleField ?? "billingCycle",
36
+ subscriptionId: config.db?.subscriptionIdField ?? "subscriptionId",
37
+ subscriptionStatus: config.db?.subscriptionStatusField ?? "subscriptionStatus",
38
+ polarCustomerId: config.db?.polarCustomerIdField ?? "polarCustomerId",
39
+ cancelAtPeriodEnd: config.db?.cancelAtPeriodEndField ?? "cancelAtPeriodEnd",
40
+ currentPeriodEnd: config.db?.currentPeriodEndField ?? "currentPeriodEnd"
41
+ };
42
+ async function callable(name, data) {
43
+ const { httpsCallable } = await import("firebase/functions");
44
+ const fn = httpsCallable(config.functions, name);
45
+ const result = await fn(data);
46
+ return result.data;
47
+ }
48
+ return {
49
+ async getStatus(userId) {
50
+ const { doc, getDoc } = await import("firebase/firestore");
51
+ const snap = await getDoc(doc(config.firestore, db.collection, userId));
52
+ if (!snap.exists()) {
53
+ return { plan: "free", subscriptionStatus: "none" };
54
+ }
55
+ const d = snap.data();
56
+ let currentPeriodEnd;
57
+ const rawEnd = d[db.currentPeriodEnd];
58
+ if (rawEnd != null) {
59
+ if (typeof rawEnd === "object" && "toDate" in rawEnd) {
60
+ currentPeriodEnd = rawEnd.toDate().toISOString();
61
+ } else if (typeof rawEnd === "string") {
62
+ currentPeriodEnd = rawEnd;
63
+ }
64
+ }
65
+ return {
66
+ plan: d[db.plan] ?? "free",
67
+ billingCycle: normalizeBillingCycle(d[db.billingCycle] ?? "monthly"),
68
+ subscriptionId: d[db.subscriptionId],
69
+ subscriptionStatus: normalizeStatus(d[db.subscriptionStatus] ?? "none"),
70
+ polarCustomerId: d[db.polarCustomerId],
71
+ cancelAtPeriodEnd: d[db.cancelAtPeriodEnd],
72
+ currentPeriodEnd
73
+ };
74
+ },
75
+ async createCheckout(params) {
76
+ return callable(callables.createCheckout, params);
77
+ },
78
+ async syncSubscription(_userId, _checkoutId) {
79
+ return callable(callables.sync, {});
80
+ },
81
+ async getBillingHistory(_userId) {
82
+ const result = await callable(
83
+ callables.billing,
84
+ {}
85
+ );
86
+ return result.orders ?? [];
87
+ },
88
+ async cancelSubscription(reason) {
89
+ return callable(callables.cancel, { reason });
90
+ },
91
+ async getPortalUrl(_userId) {
92
+ const result = await callable(
93
+ callables.portal,
94
+ {}
95
+ );
96
+ const url = result.url ?? result.customerPortalUrl;
97
+ if (!url) throw new Error("No portal URL returned from Cloud Function");
98
+ return url;
99
+ }
100
+ };
101
+ }
102
+
103
+ // src/infrastructure/constants/billing.constants.ts
104
+ var SUBSCRIPTION_STATUS = {
105
+ ACTIVE: "active",
106
+ CANCELED: "canceled",
107
+ REVOKED: "revoked",
108
+ TRIALING: "trialing",
109
+ PAST_DUE: "past_due",
110
+ INCOMPLETE: "incomplete",
111
+ INCOMPLETE_EXPIRED: "incomplete_expired",
112
+ UNPAID: "unpaid",
113
+ NONE: "none"
114
+ };
115
+ var FREE_PLAN = "free";
116
+
117
+ // src/presentation/components/PolarProvider.tsx
118
+ import {
119
+ useEffect,
120
+ useState,
121
+ useCallback,
122
+ useRef,
123
+ useMemo
124
+ } from "react";
125
+
126
+ // src/presentation/hooks/usePolarBilling.ts
127
+ import { createContext, useContext } from "react";
128
+ var PolarContext = createContext(void 0);
129
+ function usePolarBilling() {
130
+ const ctx = useContext(PolarContext);
131
+ if (!ctx) throw new Error("usePolarBilling must be used within <PolarProvider>");
132
+ return ctx;
133
+ }
134
+ var useSubscription = usePolarBilling;
135
+
136
+ // src/presentation/components/PolarProvider.tsx
137
+ import { jsx } from "react/jsx-runtime";
138
+ var FREE_STATUS = {
139
+ plan: "free",
140
+ subscriptionStatus: "none"
141
+ };
142
+ function PolarProvider({ adapter, userId, children }) {
143
+ const [status, setStatus] = useState(FREE_STATUS);
144
+ const [loading, setLoading] = useState(true);
145
+ const adapterRef = useRef(adapter);
146
+ adapterRef.current = adapter;
147
+ const refreshAbortRef = useRef(null);
148
+ const refresh = useCallback(async () => {
149
+ const uid = userId?.trim();
150
+ if (!uid) {
151
+ setStatus(FREE_STATUS);
152
+ setLoading(false);
153
+ return;
154
+ }
155
+ refreshAbortRef.current?.abort();
156
+ const ctrl = new AbortController();
157
+ refreshAbortRef.current = ctrl;
158
+ try {
159
+ setLoading(true);
160
+ const s = await adapterRef.current.getStatus(uid);
161
+ if (!ctrl.signal.aborted) setStatus(s);
162
+ } catch (err) {
163
+ if (!ctrl.signal.aborted) {
164
+ console.error("[polar-billing] getStatus failed:", err);
165
+ setStatus(FREE_STATUS);
166
+ }
167
+ } finally {
168
+ if (!ctrl.signal.aborted) setLoading(false);
169
+ }
170
+ }, [userId]);
171
+ useEffect(() => {
172
+ refresh();
173
+ return () => {
174
+ refreshAbortRef.current?.abort();
175
+ };
176
+ }, [refresh]);
177
+ const startCheckout = useCallback(async (params) => {
178
+ const result = await adapterRef.current.createCheckout({ ...params, userId: userId?.trim() });
179
+ if (!result.url.startsWith("https://")) {
180
+ throw new Error("Invalid checkout URL returned");
181
+ }
182
+ window.location.href = result.url;
183
+ }, [userId]);
184
+ const syncSubscription = useCallback(async () => {
185
+ const uid = userId?.trim();
186
+ if (!uid) return { synced: false };
187
+ const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
188
+ const result = await adapterRef.current.syncSubscription(uid, checkoutId);
189
+ if (result.synced) await refresh();
190
+ return result;
191
+ }, [userId, refresh]);
192
+ const getBillingHistory = useCallback(async () => {
193
+ const uid = userId?.trim();
194
+ if (!uid) return [];
195
+ return adapterRef.current.getBillingHistory(uid);
196
+ }, [userId]);
197
+ const cancelSubscription = useCallback(
198
+ async (reason) => {
199
+ const result = await adapterRef.current.cancelSubscription(reason);
200
+ if (result.success) await refresh();
201
+ return result;
202
+ },
203
+ [refresh]
204
+ );
205
+ const getPortalUrl = useCallback(async () => {
206
+ const uid = userId?.trim();
207
+ if (!uid) throw new Error("No authenticated user");
208
+ return adapterRef.current.getPortalUrl(uid);
209
+ }, [userId]);
210
+ const value = useMemo(
211
+ () => ({
212
+ status,
213
+ loading,
214
+ refresh,
215
+ startCheckout,
216
+ syncSubscription,
217
+ getBillingHistory,
218
+ cancelSubscription,
219
+ getPortalUrl
220
+ }),
221
+ [status, loading, refresh, startCheckout, syncSubscription, getBillingHistory, cancelSubscription, getPortalUrl]
222
+ );
223
+ return /* @__PURE__ */ jsx(PolarContext.Provider, { value, children });
224
+ }
225
+ export {
226
+ FREE_PLAN,
227
+ PolarContext,
228
+ PolarProvider,
229
+ SUBSCRIPTION_STATUS,
230
+ createFirebaseAdapter,
231
+ normalizeBillingCycle,
232
+ normalizeStatus,
233
+ usePolarBilling,
234
+ useSubscription
235
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@umituz/web-polar-payment",
3
+ "version": "1.0.0",
4
+ "description": "Universal Polar.sh subscription billing — Supabase & Firebase adapters",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "sideEffects": false,
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.js"
17
+ },
18
+ "./domain": "./src/domain/index.ts",
19
+ "./infrastructure": "./src/infrastructure/index.ts",
20
+ "./presentation": "./src/presentation/index.ts"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean --external firebase --external firebase/functions --external firebase/firestore",
27
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch --external firebase --external firebase/functions --external firebase/firestore",
28
+ "lint": "tsc --noEmit"
29
+ },
30
+ "keywords": [
31
+ "polar",
32
+ "subscription",
33
+ "billing",
34
+ "firebase",
35
+ "react"
36
+ ],
37
+ "author": "Antigravity",
38
+ "license": "MIT",
39
+ "peerDependencies": {
40
+ "react": ">=18",
41
+ "react-dom": ">=18"
42
+ },
43
+ "devDependencies": {
44
+ "@types/react": "^18.2.0",
45
+ "@types/react-dom": "^18.2.0",
46
+ "firebase": "^10.14.1",
47
+ "tsup": "^8.0.0",
48
+ "typescript": "^5.0.0"
49
+ }
50
+ }