@umituz/web-polar-payment 1.0.15 → 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.d.mts +20 -45
- package/dist/index.d.ts +20 -45
- package/dist/index.js +143 -168
- package/dist/index.mjs +148 -161
- package/package.json +1 -1
- package/src/domain/interfaces/polar-adapter.interface.ts +0 -5
- package/src/index.ts +9 -3
- package/src/infrastructure/constants/billing.constants.ts +0 -2
- package/src/infrastructure/services/firebase-billing.service.ts +26 -24
- package/src/infrastructure/utils/firebase-helpers.util.ts +1 -1
- package/src/presentation/components/PolarProvider.tsx +1 -2
- package/src/presentation/hooks/usePolarBilling.ts +0 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import * as react from 'react';
|
|
3
2
|
import { ReactNode } from 'react';
|
|
4
3
|
|
|
5
4
|
type SubscriptionStatusValue = 'active' | 'canceled' | 'revoked' | 'trialing' | 'past_due' | 'incomplete' | 'incomplete_expired' | 'unpaid' | 'none';
|
|
@@ -49,24 +48,36 @@ interface SyncResult {
|
|
|
49
48
|
plan?: string;
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
/**
|
|
53
|
-
* Backend-agnostic interface every adapter must implement.
|
|
54
|
-
* @description Contract for Polar billing adapters (Firebase, Supabase, etc.)
|
|
55
|
-
*/
|
|
56
51
|
interface PolarAdapter {
|
|
57
52
|
getStatus(userId: string): Promise<SubscriptionStatus>;
|
|
58
53
|
createCheckout(params: CheckoutParams): Promise<CheckoutResult>;
|
|
59
|
-
/** checkoutId is read from URL by the context and passed explicitly */
|
|
60
54
|
syncSubscription(userId: string, checkoutId?: string): Promise<SyncResult>;
|
|
61
55
|
getBillingHistory(userId: string): Promise<OrderItem[]>;
|
|
62
56
|
cancelSubscription(reason?: CancellationReason): Promise<CancelResult>;
|
|
63
57
|
getPortalUrl(userId: string): Promise<string>;
|
|
64
58
|
}
|
|
65
59
|
|
|
60
|
+
interface PolarProviderProps {
|
|
61
|
+
adapter: PolarAdapter;
|
|
62
|
+
userId?: string;
|
|
63
|
+
children: ReactNode;
|
|
64
|
+
}
|
|
65
|
+
declare function PolarProvider({ adapter, userId, children }: PolarProviderProps): react_jsx_runtime.JSX.Element;
|
|
66
|
+
|
|
67
|
+
interface PolarContextValue {
|
|
68
|
+
status: SubscriptionStatus;
|
|
69
|
+
loading: boolean;
|
|
70
|
+
refresh: () => Promise<void>;
|
|
71
|
+
startCheckout: (params: CheckoutParams) => Promise<void>;
|
|
72
|
+
syncSubscription: () => Promise<SyncResult>;
|
|
73
|
+
getBillingHistory: () => Promise<OrderItem[]>;
|
|
74
|
+
cancelSubscription: (reason?: CancellationReason) => Promise<CancelResult>;
|
|
75
|
+
getPortalUrl: () => Promise<string>;
|
|
76
|
+
}
|
|
77
|
+
declare function usePolarBilling(): PolarContextValue;
|
|
78
|
+
|
|
66
79
|
interface FirebaseAdapterConfig {
|
|
67
|
-
/** Firebase Functions instance from firebase/functions */
|
|
68
80
|
functions: unknown;
|
|
69
|
-
/** Firebase Firestore instance from firebase/firestore */
|
|
70
81
|
firestore: unknown;
|
|
71
82
|
callables?: {
|
|
72
83
|
createCheckout?: string;
|
|
@@ -99,41 +110,5 @@ declare const SUBSCRIPTION_STATUS: Readonly<{
|
|
|
99
110
|
UNPAID: "unpaid";
|
|
100
111
|
NONE: "none";
|
|
101
112
|
}>;
|
|
102
|
-
declare const FREE_PLAN = "free";
|
|
103
|
-
|
|
104
|
-
declare function normalizeStatus(raw: string): SubscriptionStatusValue;
|
|
105
|
-
declare function normalizeBillingCycle(interval: string): BillingCycle;
|
|
106
|
-
|
|
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
|
-
|
|
118
|
-
interface PolarProviderProps {
|
|
119
|
-
adapter: PolarAdapter;
|
|
120
|
-
userId?: string;
|
|
121
|
-
children: ReactNode;
|
|
122
|
-
}
|
|
123
|
-
declare function PolarProvider({ adapter, userId, children }: PolarProviderProps): react_jsx_runtime.JSX.Element;
|
|
124
|
-
|
|
125
|
-
interface PolarContextValue {
|
|
126
|
-
status: SubscriptionStatus;
|
|
127
|
-
loading: boolean;
|
|
128
|
-
refresh: () => Promise<void>;
|
|
129
|
-
startCheckout: (params: CheckoutParams) => Promise<void>;
|
|
130
|
-
syncSubscription: () => Promise<SyncResult>;
|
|
131
|
-
getBillingHistory: () => Promise<OrderItem[]>;
|
|
132
|
-
cancelSubscription: (reason?: CancellationReason) => Promise<CancelResult>;
|
|
133
|
-
getPortalUrl: () => Promise<string>;
|
|
134
|
-
}
|
|
135
|
-
declare const PolarContext: react.Context<PolarContextValue | undefined>;
|
|
136
|
-
declare function usePolarBilling(): PolarContextValue;
|
|
137
|
-
declare const useSubscription: typeof usePolarBilling;
|
|
138
113
|
|
|
139
|
-
export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult,
|
|
114
|
+
export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult, type FirebaseAdapterConfig, type OrderItem, type PolarAdapter, type PolarContextValue, PolarProvider, SUBSCRIPTION_STATUS, type SubscriptionStatus, type SubscriptionStatusValue, type SyncResult, createFirebaseAdapter, usePolarBilling };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import * as react from 'react';
|
|
3
2
|
import { ReactNode } from 'react';
|
|
4
3
|
|
|
5
4
|
type SubscriptionStatusValue = 'active' | 'canceled' | 'revoked' | 'trialing' | 'past_due' | 'incomplete' | 'incomplete_expired' | 'unpaid' | 'none';
|
|
@@ -49,24 +48,36 @@ interface SyncResult {
|
|
|
49
48
|
plan?: string;
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
/**
|
|
53
|
-
* Backend-agnostic interface every adapter must implement.
|
|
54
|
-
* @description Contract for Polar billing adapters (Firebase, Supabase, etc.)
|
|
55
|
-
*/
|
|
56
51
|
interface PolarAdapter {
|
|
57
52
|
getStatus(userId: string): Promise<SubscriptionStatus>;
|
|
58
53
|
createCheckout(params: CheckoutParams): Promise<CheckoutResult>;
|
|
59
|
-
/** checkoutId is read from URL by the context and passed explicitly */
|
|
60
54
|
syncSubscription(userId: string, checkoutId?: string): Promise<SyncResult>;
|
|
61
55
|
getBillingHistory(userId: string): Promise<OrderItem[]>;
|
|
62
56
|
cancelSubscription(reason?: CancellationReason): Promise<CancelResult>;
|
|
63
57
|
getPortalUrl(userId: string): Promise<string>;
|
|
64
58
|
}
|
|
65
59
|
|
|
60
|
+
interface PolarProviderProps {
|
|
61
|
+
adapter: PolarAdapter;
|
|
62
|
+
userId?: string;
|
|
63
|
+
children: ReactNode;
|
|
64
|
+
}
|
|
65
|
+
declare function PolarProvider({ adapter, userId, children }: PolarProviderProps): react_jsx_runtime.JSX.Element;
|
|
66
|
+
|
|
67
|
+
interface PolarContextValue {
|
|
68
|
+
status: SubscriptionStatus;
|
|
69
|
+
loading: boolean;
|
|
70
|
+
refresh: () => Promise<void>;
|
|
71
|
+
startCheckout: (params: CheckoutParams) => Promise<void>;
|
|
72
|
+
syncSubscription: () => Promise<SyncResult>;
|
|
73
|
+
getBillingHistory: () => Promise<OrderItem[]>;
|
|
74
|
+
cancelSubscription: (reason?: CancellationReason) => Promise<CancelResult>;
|
|
75
|
+
getPortalUrl: () => Promise<string>;
|
|
76
|
+
}
|
|
77
|
+
declare function usePolarBilling(): PolarContextValue;
|
|
78
|
+
|
|
66
79
|
interface FirebaseAdapterConfig {
|
|
67
|
-
/** Firebase Functions instance from firebase/functions */
|
|
68
80
|
functions: unknown;
|
|
69
|
-
/** Firebase Firestore instance from firebase/firestore */
|
|
70
81
|
firestore: unknown;
|
|
71
82
|
callables?: {
|
|
72
83
|
createCheckout?: string;
|
|
@@ -99,41 +110,5 @@ declare const SUBSCRIPTION_STATUS: Readonly<{
|
|
|
99
110
|
UNPAID: "unpaid";
|
|
100
111
|
NONE: "none";
|
|
101
112
|
}>;
|
|
102
|
-
declare const FREE_PLAN = "free";
|
|
103
|
-
|
|
104
|
-
declare function normalizeStatus(raw: string): SubscriptionStatusValue;
|
|
105
|
-
declare function normalizeBillingCycle(interval: string): BillingCycle;
|
|
106
|
-
|
|
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
|
-
|
|
118
|
-
interface PolarProviderProps {
|
|
119
|
-
adapter: PolarAdapter;
|
|
120
|
-
userId?: string;
|
|
121
|
-
children: ReactNode;
|
|
122
|
-
}
|
|
123
|
-
declare function PolarProvider({ adapter, userId, children }: PolarProviderProps): react_jsx_runtime.JSX.Element;
|
|
124
|
-
|
|
125
|
-
interface PolarContextValue {
|
|
126
|
-
status: SubscriptionStatus;
|
|
127
|
-
loading: boolean;
|
|
128
|
-
refresh: () => Promise<void>;
|
|
129
|
-
startCheckout: (params: CheckoutParams) => Promise<void>;
|
|
130
|
-
syncSubscription: () => Promise<SyncResult>;
|
|
131
|
-
getBillingHistory: () => Promise<OrderItem[]>;
|
|
132
|
-
cancelSubscription: (reason?: CancellationReason) => Promise<CancelResult>;
|
|
133
|
-
getPortalUrl: () => Promise<string>;
|
|
134
|
-
}
|
|
135
|
-
declare const PolarContext: react.Context<PolarContextValue | undefined>;
|
|
136
|
-
declare function usePolarBilling(): PolarContextValue;
|
|
137
|
-
declare const useSubscription: typeof usePolarBilling;
|
|
138
113
|
|
|
139
|
-
export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult,
|
|
114
|
+
export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult, type FirebaseAdapterConfig, type OrderItem, type PolarAdapter, type PolarContextValue, PolarProvider, SUBSCRIPTION_STATUS, type SubscriptionStatus, type SubscriptionStatusValue, type SyncResult, createFirebaseAdapter, usePolarBilling };
|
package/dist/index.js
CHANGED
|
@@ -30,25 +30,141 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
FREE_PLAN: () => FREE_PLAN,
|
|
34
|
-
PolarContext: () => PolarContext,
|
|
35
33
|
PolarProvider: () => PolarProvider,
|
|
36
34
|
SUBSCRIPTION_STATUS: () => SUBSCRIPTION_STATUS,
|
|
37
|
-
asBoolean: () => asBoolean,
|
|
38
|
-
asString: () => asString,
|
|
39
35
|
createFirebaseAdapter: () => createFirebaseAdapter,
|
|
40
|
-
|
|
41
|
-
isTimestamp: () => isTimestamp,
|
|
42
|
-
isValidCheckoutUrl: () => isValidCheckoutUrl,
|
|
43
|
-
isValidProductId: () => isValidProductId,
|
|
44
|
-
normalizeBillingCycle: () => normalizeBillingCycle,
|
|
45
|
-
normalizeStatus: () => normalizeStatus,
|
|
46
|
-
normalizeUserId: () => normalizeUserId,
|
|
47
|
-
usePolarBilling: () => usePolarBilling,
|
|
48
|
-
useSubscription: () => useSubscription
|
|
36
|
+
usePolarBilling: () => usePolarBilling
|
|
49
37
|
});
|
|
50
38
|
module.exports = __toCommonJS(index_exports);
|
|
51
39
|
|
|
40
|
+
// src/presentation/components/PolarProvider.tsx
|
|
41
|
+
var import_react2 = require("react");
|
|
42
|
+
|
|
43
|
+
// src/presentation/hooks/usePolarBilling.ts
|
|
44
|
+
var import_react = require("react");
|
|
45
|
+
var PolarContext = (0, import_react.createContext)(void 0);
|
|
46
|
+
function usePolarBilling() {
|
|
47
|
+
const ctx = (0, import_react.useContext)(PolarContext);
|
|
48
|
+
if (!ctx) throw new Error("usePolarBilling must be used within <PolarProvider>");
|
|
49
|
+
return ctx;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/infrastructure/utils/validations.util.ts
|
|
53
|
+
function normalizeUserId(userId) {
|
|
54
|
+
if (typeof userId !== "string") return void 0;
|
|
55
|
+
const trimmed = userId.trim();
|
|
56
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
57
|
+
}
|
|
58
|
+
function isValidProductId(productId) {
|
|
59
|
+
return typeof productId === "string" && productId.trim().length > 0;
|
|
60
|
+
}
|
|
61
|
+
function isValidCheckoutUrl(url) {
|
|
62
|
+
return url.startsWith("https://") || url.startsWith("http://");
|
|
63
|
+
}
|
|
64
|
+
function isProductionInsecureUrl(url) {
|
|
65
|
+
return url.startsWith("http://") && !url.includes("localhost") && !url.includes("127.0.0.1");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/presentation/components/PolarProvider.tsx
|
|
69
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
70
|
+
var FREE_STATUS = {
|
|
71
|
+
plan: "free",
|
|
72
|
+
subscriptionStatus: "none"
|
|
73
|
+
};
|
|
74
|
+
function PolarProvider({ adapter, userId, children }) {
|
|
75
|
+
const [status, setStatus] = (0, import_react2.useState)(FREE_STATUS);
|
|
76
|
+
const [loading, setLoading] = (0, import_react2.useState)(true);
|
|
77
|
+
const adapterRef = (0, import_react2.useRef)(adapter);
|
|
78
|
+
const statusRef = (0, import_react2.useRef)({ setStatus, setLoading });
|
|
79
|
+
const userIdRef = (0, import_react2.useRef)(normalizeUserId(userId));
|
|
80
|
+
const refreshMountedRef = (0, import_react2.useRef)(true);
|
|
81
|
+
adapterRef.current = adapter;
|
|
82
|
+
statusRef.current = { setStatus, setLoading };
|
|
83
|
+
userIdRef.current = normalizeUserId(userId);
|
|
84
|
+
const refresh = (0, import_react2.useCallback)(async () => {
|
|
85
|
+
const uid = userIdRef.current;
|
|
86
|
+
const { setStatus: setStatus2, setLoading: setLoading2 } = statusRef.current;
|
|
87
|
+
if (!uid) {
|
|
88
|
+
setStatus2(FREE_STATUS);
|
|
89
|
+
setLoading2(false);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
refreshMountedRef.current = true;
|
|
93
|
+
try {
|
|
94
|
+
setLoading2(true);
|
|
95
|
+
const s = await adapterRef.current.getStatus(uid);
|
|
96
|
+
if (refreshMountedRef.current) setStatus2(s);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (refreshMountedRef.current) {
|
|
99
|
+
setStatus2(FREE_STATUS);
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
if (refreshMountedRef.current) setLoading2(false);
|
|
103
|
+
}
|
|
104
|
+
}, []);
|
|
105
|
+
(0, import_react2.useEffect)(() => {
|
|
106
|
+
refreshMountedRef.current = true;
|
|
107
|
+
refresh();
|
|
108
|
+
return () => {
|
|
109
|
+
refreshMountedRef.current = false;
|
|
110
|
+
};
|
|
111
|
+
}, [userId]);
|
|
112
|
+
const startCheckout = (0, import_react2.useCallback)(async (params) => {
|
|
113
|
+
if (!isValidProductId(params.productId)) {
|
|
114
|
+
throw new Error("[polar-billing] Invalid productId: must be a non-empty string");
|
|
115
|
+
}
|
|
116
|
+
const uid = userIdRef.current;
|
|
117
|
+
const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? void 0 });
|
|
118
|
+
if (!isValidCheckoutUrl(result.url)) {
|
|
119
|
+
throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https:// or http://");
|
|
120
|
+
}
|
|
121
|
+
if (isProductionInsecureUrl(result.url)) {
|
|
122
|
+
throw new Error("[polar-billing] ERROR: Using insecure http:// URL in production environment");
|
|
123
|
+
}
|
|
124
|
+
window.location.href = result.url;
|
|
125
|
+
}, []);
|
|
126
|
+
const syncSubscription = (0, import_react2.useCallback)(async () => {
|
|
127
|
+
const uid = userIdRef.current;
|
|
128
|
+
if (!uid) return { synced: false };
|
|
129
|
+
const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
|
|
130
|
+
const result = await adapterRef.current.syncSubscription(uid, checkoutId);
|
|
131
|
+
if (result.synced) await refresh();
|
|
132
|
+
return result;
|
|
133
|
+
}, [refresh]);
|
|
134
|
+
const getBillingHistory = (0, import_react2.useCallback)(async () => {
|
|
135
|
+
const uid = userIdRef.current;
|
|
136
|
+
if (!uid) return [];
|
|
137
|
+
return adapterRef.current.getBillingHistory(uid);
|
|
138
|
+
}, []);
|
|
139
|
+
const cancelSubscription = (0, import_react2.useCallback)(
|
|
140
|
+
async (reason) => {
|
|
141
|
+
const result = await adapterRef.current.cancelSubscription(reason);
|
|
142
|
+
if (result.success) await refresh();
|
|
143
|
+
return result;
|
|
144
|
+
},
|
|
145
|
+
[refresh]
|
|
146
|
+
);
|
|
147
|
+
const getPortalUrl = (0, import_react2.useCallback)(async () => {
|
|
148
|
+
const uid = userIdRef.current;
|
|
149
|
+
if (!uid) throw new Error("[polar-billing] Cannot get portal URL: No authenticated user");
|
|
150
|
+
return adapterRef.current.getPortalUrl(uid);
|
|
151
|
+
}, []);
|
|
152
|
+
const value = (0, import_react2.useMemo)(
|
|
153
|
+
() => ({
|
|
154
|
+
status,
|
|
155
|
+
loading,
|
|
156
|
+
refresh,
|
|
157
|
+
startCheckout,
|
|
158
|
+
syncSubscription,
|
|
159
|
+
getBillingHistory,
|
|
160
|
+
cancelSubscription,
|
|
161
|
+
getPortalUrl
|
|
162
|
+
}),
|
|
163
|
+
[status, loading, refresh, syncSubscription]
|
|
164
|
+
);
|
|
165
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PolarContext.Provider, { value, children });
|
|
166
|
+
}
|
|
167
|
+
|
|
52
168
|
// src/infrastructure/constants/billing.constants.ts
|
|
53
169
|
var SUBSCRIPTION_STATUS = Object.freeze({
|
|
54
170
|
ACTIVE: "active",
|
|
@@ -61,7 +177,6 @@ var SUBSCRIPTION_STATUS = Object.freeze({
|
|
|
61
177
|
UNPAID: "unpaid",
|
|
62
178
|
NONE: "none"
|
|
63
179
|
});
|
|
64
|
-
var FREE_PLAN = "free";
|
|
65
180
|
|
|
66
181
|
// src/infrastructure/utils/normalization.util.ts
|
|
67
182
|
var STATUS_MAP = Object.freeze({
|
|
@@ -97,7 +212,7 @@ function asBoolean(value) {
|
|
|
97
212
|
return void 0;
|
|
98
213
|
}
|
|
99
214
|
function isTimestamp(value) {
|
|
100
|
-
return typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate
|
|
215
|
+
return typeof value === "object" && value !== null && "toDate" in value && "function" === typeof value.toDate;
|
|
101
216
|
}
|
|
102
217
|
|
|
103
218
|
// src/infrastructure/services/firebase-billing.service.ts
|
|
@@ -126,7 +241,7 @@ function createFirebaseAdapter(config) {
|
|
|
126
241
|
async function getHttpsCallable() {
|
|
127
242
|
if (!httpsCallableCache) {
|
|
128
243
|
const mod = await import("firebase/functions");
|
|
129
|
-
httpsCallableCache = mod.httpsCallable;
|
|
244
|
+
httpsCallableCache = mod.httpsCallable(functions);
|
|
130
245
|
}
|
|
131
246
|
return httpsCallableCache;
|
|
132
247
|
}
|
|
@@ -139,15 +254,17 @@ function createFirebaseAdapter(config) {
|
|
|
139
254
|
}
|
|
140
255
|
async function callable(name, data) {
|
|
141
256
|
const httpsCallable = await getHttpsCallable();
|
|
142
|
-
const fn = httpsCallable(
|
|
257
|
+
const fn = httpsCallable(name);
|
|
143
258
|
const result = await fn(data);
|
|
144
259
|
return result.data;
|
|
145
260
|
}
|
|
146
261
|
return {
|
|
147
262
|
async getStatus(userId) {
|
|
148
263
|
const { doc, getDoc } = await getFirestore();
|
|
149
|
-
const
|
|
150
|
-
|
|
264
|
+
const docRef = doc(firestore, db.collection, userId);
|
|
265
|
+
const snap = await getDoc(docRef);
|
|
266
|
+
const exists = snap.exists;
|
|
267
|
+
if (!exists) {
|
|
151
268
|
return { plan: "free", subscriptionStatus: "none" };
|
|
152
269
|
}
|
|
153
270
|
const d = snap.data();
|
|
@@ -173,174 +290,32 @@ function createFirebaseAdapter(config) {
|
|
|
173
290
|
async createCheckout(params) {
|
|
174
291
|
return callable(callables.createCheckout, params);
|
|
175
292
|
},
|
|
176
|
-
async syncSubscription(
|
|
177
|
-
return callable(callables.sync, {});
|
|
293
|
+
async syncSubscription(userId, checkoutId) {
|
|
294
|
+
return callable(callables.sync, { userId, checkoutId });
|
|
178
295
|
},
|
|
179
|
-
async getBillingHistory(
|
|
296
|
+
async getBillingHistory(userId) {
|
|
180
297
|
const result = await callable(
|
|
181
298
|
callables.billing,
|
|
182
|
-
{}
|
|
299
|
+
{ userId }
|
|
183
300
|
);
|
|
184
301
|
return result.orders ?? [];
|
|
185
302
|
},
|
|
186
303
|
async cancelSubscription(reason) {
|
|
187
304
|
return callable(callables.cancel, { reason });
|
|
188
305
|
},
|
|
189
|
-
async getPortalUrl(
|
|
306
|
+
async getPortalUrl(userId) {
|
|
190
307
|
const result = await callable(
|
|
191
308
|
callables.portal,
|
|
192
|
-
{}
|
|
309
|
+
{ userId }
|
|
193
310
|
);
|
|
194
311
|
return result.url;
|
|
195
312
|
}
|
|
196
313
|
};
|
|
197
314
|
}
|
|
198
|
-
|
|
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
|
-
}
|
|
214
|
-
|
|
215
|
-
// src/presentation/components/PolarProvider.tsx
|
|
216
|
-
var import_react2 = require("react");
|
|
217
|
-
|
|
218
|
-
// src/presentation/hooks/usePolarBilling.ts
|
|
219
|
-
var import_react = require("react");
|
|
220
|
-
var PolarContext = (0, import_react.createContext)(void 0);
|
|
221
|
-
function usePolarBilling() {
|
|
222
|
-
const ctx = (0, import_react.useContext)(PolarContext);
|
|
223
|
-
if (!ctx) throw new Error("usePolarBilling must be used within <PolarProvider>");
|
|
224
|
-
return ctx;
|
|
225
|
-
}
|
|
226
|
-
var useSubscription = usePolarBilling;
|
|
227
|
-
|
|
228
|
-
// src/presentation/components/PolarProvider.tsx
|
|
229
|
-
var import_jsx_runtime = require("react/jsx-runtime");
|
|
230
|
-
var FREE_STATUS = {
|
|
231
|
-
plan: "free",
|
|
232
|
-
subscriptionStatus: "none"
|
|
233
|
-
};
|
|
234
|
-
function PolarProvider({ adapter, userId, children }) {
|
|
235
|
-
const [status, setStatus] = (0, import_react2.useState)(FREE_STATUS);
|
|
236
|
-
const [loading, setLoading] = (0, import_react2.useState)(true);
|
|
237
|
-
const adapterRef = (0, import_react2.useRef)(adapter);
|
|
238
|
-
const statusRef = (0, import_react2.useRef)({ setStatus, setLoading });
|
|
239
|
-
const userIdRef = (0, import_react2.useRef)(normalizeUserId(userId));
|
|
240
|
-
const refreshMountedRef = (0, import_react2.useRef)(true);
|
|
241
|
-
adapterRef.current = adapter;
|
|
242
|
-
statusRef.current = { setStatus, setLoading };
|
|
243
|
-
userIdRef.current = normalizeUserId(userId);
|
|
244
|
-
const refresh = (0, import_react2.useCallback)(async () => {
|
|
245
|
-
const uid = userIdRef.current;
|
|
246
|
-
const { setStatus: setStatus2, setLoading: setLoading2 } = statusRef.current;
|
|
247
|
-
if (!uid) {
|
|
248
|
-
setStatus2(FREE_STATUS);
|
|
249
|
-
setLoading2(false);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
refreshMountedRef.current = true;
|
|
253
|
-
try {
|
|
254
|
-
setLoading2(true);
|
|
255
|
-
const s = await adapterRef.current.getStatus(uid);
|
|
256
|
-
if (refreshMountedRef.current) setStatus2(s);
|
|
257
|
-
} catch (err) {
|
|
258
|
-
if (refreshMountedRef.current) {
|
|
259
|
-
console.error("[polar-billing] getStatus failed:", err);
|
|
260
|
-
setStatus2(FREE_STATUS);
|
|
261
|
-
}
|
|
262
|
-
} finally {
|
|
263
|
-
if (refreshMountedRef.current) setLoading2(false);
|
|
264
|
-
}
|
|
265
|
-
}, []);
|
|
266
|
-
(0, import_react2.useEffect)(() => {
|
|
267
|
-
refreshMountedRef.current = true;
|
|
268
|
-
refresh();
|
|
269
|
-
return () => {
|
|
270
|
-
refreshMountedRef.current = false;
|
|
271
|
-
};
|
|
272
|
-
}, [userId]);
|
|
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
|
-
}
|
|
277
|
-
const uid = userIdRef.current;
|
|
278
|
-
const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? void 0 });
|
|
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");
|
|
284
|
-
}
|
|
285
|
-
window.location.href = result.url;
|
|
286
|
-
}, []);
|
|
287
|
-
const syncSubscription = (0, import_react2.useCallback)(async () => {
|
|
288
|
-
const uid = userIdRef.current;
|
|
289
|
-
if (!uid) return { synced: false };
|
|
290
|
-
const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
|
|
291
|
-
const result = await adapterRef.current.syncSubscription(uid, checkoutId);
|
|
292
|
-
if (result.synced) await refresh();
|
|
293
|
-
return result;
|
|
294
|
-
}, [refresh]);
|
|
295
|
-
const getBillingHistory = (0, import_react2.useCallback)(async () => {
|
|
296
|
-
const uid = userIdRef.current;
|
|
297
|
-
if (!uid) return [];
|
|
298
|
-
return adapterRef.current.getBillingHistory(uid);
|
|
299
|
-
}, []);
|
|
300
|
-
const cancelSubscription = (0, import_react2.useCallback)(
|
|
301
|
-
async (reason) => {
|
|
302
|
-
const result = await adapterRef.current.cancelSubscription(reason);
|
|
303
|
-
if (result.success) await refresh();
|
|
304
|
-
return result;
|
|
305
|
-
},
|
|
306
|
-
[refresh]
|
|
307
|
-
);
|
|
308
|
-
const getPortalUrl = (0, import_react2.useCallback)(async () => {
|
|
309
|
-
const uid = userIdRef.current;
|
|
310
|
-
if (!uid) throw new Error("[polar-billing] Cannot get portal URL: No authenticated user");
|
|
311
|
-
return adapterRef.current.getPortalUrl(uid);
|
|
312
|
-
}, []);
|
|
313
|
-
const value = (0, import_react2.useMemo)(
|
|
314
|
-
() => ({
|
|
315
|
-
status,
|
|
316
|
-
loading,
|
|
317
|
-
refresh,
|
|
318
|
-
startCheckout,
|
|
319
|
-
syncSubscription,
|
|
320
|
-
getBillingHistory,
|
|
321
|
-
cancelSubscription,
|
|
322
|
-
getPortalUrl
|
|
323
|
-
}),
|
|
324
|
-
[status, loading, refresh, syncSubscription]
|
|
325
|
-
);
|
|
326
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PolarContext.Provider, { value, children });
|
|
327
|
-
}
|
|
328
315
|
// Annotate the CommonJS export names for ESM import in node:
|
|
329
316
|
0 && (module.exports = {
|
|
330
|
-
FREE_PLAN,
|
|
331
|
-
PolarContext,
|
|
332
317
|
PolarProvider,
|
|
333
318
|
SUBSCRIPTION_STATUS,
|
|
334
|
-
asBoolean,
|
|
335
|
-
asString,
|
|
336
319
|
createFirebaseAdapter,
|
|
337
|
-
|
|
338
|
-
isTimestamp,
|
|
339
|
-
isValidCheckoutUrl,
|
|
340
|
-
isValidProductId,
|
|
341
|
-
normalizeBillingCycle,
|
|
342
|
-
normalizeStatus,
|
|
343
|
-
normalizeUserId,
|
|
344
|
-
usePolarBilling,
|
|
345
|
-
useSubscription
|
|
320
|
+
usePolarBilling
|
|
346
321
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,137 @@
|
|
|
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"
|
|
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
|
+
|
|
1
135
|
// src/infrastructure/constants/billing.constants.ts
|
|
2
136
|
var SUBSCRIPTION_STATUS = Object.freeze({
|
|
3
137
|
ACTIVE: "active",
|
|
@@ -10,7 +144,6 @@ var SUBSCRIPTION_STATUS = Object.freeze({
|
|
|
10
144
|
UNPAID: "unpaid",
|
|
11
145
|
NONE: "none"
|
|
12
146
|
});
|
|
13
|
-
var FREE_PLAN = "free";
|
|
14
147
|
|
|
15
148
|
// src/infrastructure/utils/normalization.util.ts
|
|
16
149
|
var STATUS_MAP = Object.freeze({
|
|
@@ -46,7 +179,7 @@ function asBoolean(value) {
|
|
|
46
179
|
return void 0;
|
|
47
180
|
}
|
|
48
181
|
function isTimestamp(value) {
|
|
49
|
-
return typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate
|
|
182
|
+
return typeof value === "object" && value !== null && "toDate" in value && "function" === typeof value.toDate;
|
|
50
183
|
}
|
|
51
184
|
|
|
52
185
|
// src/infrastructure/services/firebase-billing.service.ts
|
|
@@ -75,7 +208,7 @@ function createFirebaseAdapter(config) {
|
|
|
75
208
|
async function getHttpsCallable() {
|
|
76
209
|
if (!httpsCallableCache) {
|
|
77
210
|
const mod = await import("firebase/functions");
|
|
78
|
-
httpsCallableCache = mod.httpsCallable;
|
|
211
|
+
httpsCallableCache = mod.httpsCallable(functions);
|
|
79
212
|
}
|
|
80
213
|
return httpsCallableCache;
|
|
81
214
|
}
|
|
@@ -88,15 +221,17 @@ function createFirebaseAdapter(config) {
|
|
|
88
221
|
}
|
|
89
222
|
async function callable(name, data) {
|
|
90
223
|
const httpsCallable = await getHttpsCallable();
|
|
91
|
-
const fn = httpsCallable(
|
|
224
|
+
const fn = httpsCallable(name);
|
|
92
225
|
const result = await fn(data);
|
|
93
226
|
return result.data;
|
|
94
227
|
}
|
|
95
228
|
return {
|
|
96
229
|
async getStatus(userId) {
|
|
97
230
|
const { doc, getDoc } = await getFirestore();
|
|
98
|
-
const
|
|
99
|
-
|
|
231
|
+
const docRef = doc(firestore, db.collection, userId);
|
|
232
|
+
const snap = await getDoc(docRef);
|
|
233
|
+
const exists = snap.exists;
|
|
234
|
+
if (!exists) {
|
|
100
235
|
return { plan: "free", subscriptionStatus: "none" };
|
|
101
236
|
}
|
|
102
237
|
const d = snap.data();
|
|
@@ -122,179 +257,31 @@ function createFirebaseAdapter(config) {
|
|
|
122
257
|
async createCheckout(params) {
|
|
123
258
|
return callable(callables.createCheckout, params);
|
|
124
259
|
},
|
|
125
|
-
async syncSubscription(
|
|
126
|
-
return callable(callables.sync, {});
|
|
260
|
+
async syncSubscription(userId, checkoutId) {
|
|
261
|
+
return callable(callables.sync, { userId, checkoutId });
|
|
127
262
|
},
|
|
128
|
-
async getBillingHistory(
|
|
263
|
+
async getBillingHistory(userId) {
|
|
129
264
|
const result = await callable(
|
|
130
265
|
callables.billing,
|
|
131
|
-
{}
|
|
266
|
+
{ userId }
|
|
132
267
|
);
|
|
133
268
|
return result.orders ?? [];
|
|
134
269
|
},
|
|
135
270
|
async cancelSubscription(reason) {
|
|
136
271
|
return callable(callables.cancel, { reason });
|
|
137
272
|
},
|
|
138
|
-
async getPortalUrl(
|
|
273
|
+
async getPortalUrl(userId) {
|
|
139
274
|
const result = await callable(
|
|
140
275
|
callables.portal,
|
|
141
|
-
{}
|
|
276
|
+
{ userId }
|
|
142
277
|
);
|
|
143
278
|
return result.url;
|
|
144
279
|
}
|
|
145
280
|
};
|
|
146
281
|
}
|
|
147
|
-
|
|
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
|
-
}
|
|
163
|
-
|
|
164
|
-
// src/presentation/components/PolarProvider.tsx
|
|
165
|
-
import {
|
|
166
|
-
useEffect,
|
|
167
|
-
useState,
|
|
168
|
-
useCallback,
|
|
169
|
-
useRef,
|
|
170
|
-
useMemo
|
|
171
|
-
} from "react";
|
|
172
|
-
|
|
173
|
-
// src/presentation/hooks/usePolarBilling.ts
|
|
174
|
-
import { createContext, useContext } from "react";
|
|
175
|
-
var PolarContext = createContext(void 0);
|
|
176
|
-
function usePolarBilling() {
|
|
177
|
-
const ctx = useContext(PolarContext);
|
|
178
|
-
if (!ctx) throw new Error("usePolarBilling must be used within <PolarProvider>");
|
|
179
|
-
return ctx;
|
|
180
|
-
}
|
|
181
|
-
var useSubscription = usePolarBilling;
|
|
182
|
-
|
|
183
|
-
// src/presentation/components/PolarProvider.tsx
|
|
184
|
-
import { jsx } from "react/jsx-runtime";
|
|
185
|
-
var FREE_STATUS = {
|
|
186
|
-
plan: "free",
|
|
187
|
-
subscriptionStatus: "none"
|
|
188
|
-
};
|
|
189
|
-
function PolarProvider({ adapter, userId, children }) {
|
|
190
|
-
const [status, setStatus] = useState(FREE_STATUS);
|
|
191
|
-
const [loading, setLoading] = useState(true);
|
|
192
|
-
const adapterRef = useRef(adapter);
|
|
193
|
-
const statusRef = useRef({ setStatus, setLoading });
|
|
194
|
-
const userIdRef = useRef(normalizeUserId(userId));
|
|
195
|
-
const refreshMountedRef = useRef(true);
|
|
196
|
-
adapterRef.current = adapter;
|
|
197
|
-
statusRef.current = { setStatus, setLoading };
|
|
198
|
-
userIdRef.current = normalizeUserId(userId);
|
|
199
|
-
const refresh = useCallback(async () => {
|
|
200
|
-
const uid = userIdRef.current;
|
|
201
|
-
const { setStatus: setStatus2, setLoading: setLoading2 } = statusRef.current;
|
|
202
|
-
if (!uid) {
|
|
203
|
-
setStatus2(FREE_STATUS);
|
|
204
|
-
setLoading2(false);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
refreshMountedRef.current = true;
|
|
208
|
-
try {
|
|
209
|
-
setLoading2(true);
|
|
210
|
-
const s = await adapterRef.current.getStatus(uid);
|
|
211
|
-
if (refreshMountedRef.current) setStatus2(s);
|
|
212
|
-
} catch (err) {
|
|
213
|
-
if (refreshMountedRef.current) {
|
|
214
|
-
console.error("[polar-billing] getStatus failed:", err);
|
|
215
|
-
setStatus2(FREE_STATUS);
|
|
216
|
-
}
|
|
217
|
-
} finally {
|
|
218
|
-
if (refreshMountedRef.current) setLoading2(false);
|
|
219
|
-
}
|
|
220
|
-
}, []);
|
|
221
|
-
useEffect(() => {
|
|
222
|
-
refreshMountedRef.current = true;
|
|
223
|
-
refresh();
|
|
224
|
-
return () => {
|
|
225
|
-
refreshMountedRef.current = false;
|
|
226
|
-
};
|
|
227
|
-
}, [userId]);
|
|
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
|
-
}
|
|
232
|
-
const uid = userIdRef.current;
|
|
233
|
-
const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? void 0 });
|
|
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");
|
|
239
|
-
}
|
|
240
|
-
window.location.href = result.url;
|
|
241
|
-
}, []);
|
|
242
|
-
const syncSubscription = useCallback(async () => {
|
|
243
|
-
const uid = userIdRef.current;
|
|
244
|
-
if (!uid) return { synced: false };
|
|
245
|
-
const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
|
|
246
|
-
const result = await adapterRef.current.syncSubscription(uid, checkoutId);
|
|
247
|
-
if (result.synced) await refresh();
|
|
248
|
-
return result;
|
|
249
|
-
}, [refresh]);
|
|
250
|
-
const getBillingHistory = useCallback(async () => {
|
|
251
|
-
const uid = userIdRef.current;
|
|
252
|
-
if (!uid) return [];
|
|
253
|
-
return adapterRef.current.getBillingHistory(uid);
|
|
254
|
-
}, []);
|
|
255
|
-
const cancelSubscription = useCallback(
|
|
256
|
-
async (reason) => {
|
|
257
|
-
const result = await adapterRef.current.cancelSubscription(reason);
|
|
258
|
-
if (result.success) await refresh();
|
|
259
|
-
return result;
|
|
260
|
-
},
|
|
261
|
-
[refresh]
|
|
262
|
-
);
|
|
263
|
-
const getPortalUrl = useCallback(async () => {
|
|
264
|
-
const uid = userIdRef.current;
|
|
265
|
-
if (!uid) throw new Error("[polar-billing] Cannot get portal URL: No authenticated user");
|
|
266
|
-
return adapterRef.current.getPortalUrl(uid);
|
|
267
|
-
}, []);
|
|
268
|
-
const value = useMemo(
|
|
269
|
-
() => ({
|
|
270
|
-
status,
|
|
271
|
-
loading,
|
|
272
|
-
refresh,
|
|
273
|
-
startCheckout,
|
|
274
|
-
syncSubscription,
|
|
275
|
-
getBillingHistory,
|
|
276
|
-
cancelSubscription,
|
|
277
|
-
getPortalUrl
|
|
278
|
-
}),
|
|
279
|
-
[status, loading, refresh, syncSubscription]
|
|
280
|
-
);
|
|
281
|
-
return /* @__PURE__ */ jsx(PolarContext.Provider, { value, children });
|
|
282
|
-
}
|
|
283
282
|
export {
|
|
284
|
-
FREE_PLAN,
|
|
285
|
-
PolarContext,
|
|
286
283
|
PolarProvider,
|
|
287
284
|
SUBSCRIPTION_STATUS,
|
|
288
|
-
asBoolean,
|
|
289
|
-
asString,
|
|
290
285
|
createFirebaseAdapter,
|
|
291
|
-
|
|
292
|
-
isTimestamp,
|
|
293
|
-
isValidCheckoutUrl,
|
|
294
|
-
isValidProductId,
|
|
295
|
-
normalizeBillingCycle,
|
|
296
|
-
normalizeStatus,
|
|
297
|
-
normalizeUserId,
|
|
298
|
-
usePolarBilling,
|
|
299
|
-
useSubscription
|
|
286
|
+
usePolarBilling
|
|
300
287
|
};
|
package/package.json
CHANGED
|
@@ -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,3 +1,9 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
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';
|
|
@@ -11,13 +11,13 @@ import type {
|
|
|
11
11
|
import { normalizeStatus, normalizeBillingCycle } from '../utils/normalization.util';
|
|
12
12
|
import { asString, asBoolean, isTimestamp } from '../utils/firebase-helpers.util';
|
|
13
13
|
|
|
14
|
-
type
|
|
15
|
-
type
|
|
14
|
+
type FirestoreDoc = (firestore: unknown, collectionPath: string, docId: string) => unknown;
|
|
15
|
+
type FirestoreGetDoc = (ref: unknown) => Promise<{ exists: boolean; data(): Record<string, unknown> }>;
|
|
16
|
+
type HttpsCallableFn = <T = unknown, R = unknown>(data?: T) => Promise<{ data: R }>;
|
|
17
|
+
type HttpsCallable = (name: string) => HttpsCallableFn;
|
|
16
18
|
|
|
17
19
|
export interface FirebaseAdapterConfig {
|
|
18
|
-
/** Firebase Functions instance from firebase/functions */
|
|
19
20
|
functions: unknown;
|
|
20
|
-
/** Firebase Firestore instance from firebase/firestore */
|
|
21
21
|
firestore: unknown;
|
|
22
22
|
callables?: {
|
|
23
23
|
createCheckout?: string;
|
|
@@ -39,8 +39,8 @@ export interface FirebaseAdapterConfig {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapter {
|
|
42
|
-
const functions = config.functions
|
|
43
|
-
const firestore = config.firestore
|
|
42
|
+
const functions = config.functions;
|
|
43
|
+
const firestore = config.firestore;
|
|
44
44
|
|
|
45
45
|
const callables = {
|
|
46
46
|
createCheckout: config.callables?.createCheckout ?? 'createCheckoutSession',
|
|
@@ -61,28 +61,28 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
61
61
|
currentPeriodEnd: config.db?.currentPeriodEndField ?? 'currentPeriodEnd',
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
let httpsCallableCache:
|
|
65
|
-
let firestoreCache: { doc:
|
|
64
|
+
let httpsCallableCache: HttpsCallable | null = null;
|
|
65
|
+
let firestoreCache: { doc: FirestoreDoc; getDoc: FirestoreGetDoc } | null = null;
|
|
66
66
|
|
|
67
|
-
async function getHttpsCallable() {
|
|
67
|
+
async function getHttpsCallable(): Promise<HttpsCallable> {
|
|
68
68
|
if (!httpsCallableCache) {
|
|
69
69
|
const mod = await import('firebase/functions');
|
|
70
|
-
httpsCallableCache = mod.httpsCallable;
|
|
70
|
+
httpsCallableCache = mod.httpsCallable(functions) as HttpsCallable;
|
|
71
71
|
}
|
|
72
72
|
return httpsCallableCache;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
async function getFirestore() {
|
|
75
|
+
async function getFirestore(): Promise<{ doc: FirestoreDoc; getDoc: FirestoreGetDoc }> {
|
|
76
76
|
if (!firestoreCache) {
|
|
77
77
|
const mod = await import('firebase/firestore');
|
|
78
|
-
firestoreCache = { doc: mod.doc, getDoc: mod.getDoc };
|
|
78
|
+
firestoreCache = { doc: mod.doc as FirestoreDoc, getDoc: mod.getDoc as FirestoreGetDoc };
|
|
79
79
|
}
|
|
80
80
|
return firestoreCache;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
async function callable<T = unknown, R = unknown>(name: string, data?: T): Promise<R> {
|
|
84
84
|
const httpsCallable = await getHttpsCallable();
|
|
85
|
-
const fn =
|
|
85
|
+
const fn = httpsCallable(name) as (data?: T) => Promise<{ data: R }>;
|
|
86
86
|
const result = await fn(data);
|
|
87
87
|
return result.data;
|
|
88
88
|
}
|
|
@@ -90,13 +90,15 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
90
90
|
return {
|
|
91
91
|
async getStatus(userId: string): Promise<SubscriptionStatus> {
|
|
92
92
|
const { doc, getDoc } = await getFirestore();
|
|
93
|
-
const
|
|
93
|
+
const docRef = doc(firestore, db.collection, userId);
|
|
94
|
+
const snap = await getDoc(docRef);
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
const exists = (snap as { exists: boolean }).exists;
|
|
97
|
+
if (!exists) {
|
|
96
98
|
return { plan: 'free', subscriptionStatus: 'none' };
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
const d = snap.data()
|
|
101
|
+
const d = snap.data();
|
|
100
102
|
|
|
101
103
|
let currentPeriodEnd: string | undefined;
|
|
102
104
|
const rawEnd = d[db.currentPeriodEnd];
|
|
@@ -123,14 +125,14 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
123
125
|
return callable<CheckoutParams, CheckoutResult>(callables.createCheckout, params);
|
|
124
126
|
},
|
|
125
127
|
|
|
126
|
-
async syncSubscription(
|
|
127
|
-
return callable<
|
|
128
|
+
async syncSubscription(userId: string, checkoutId?: string): Promise<SyncResult> {
|
|
129
|
+
return callable<{ userId: string; checkoutId?: string }, SyncResult>(callables.sync, { userId, checkoutId });
|
|
128
130
|
},
|
|
129
131
|
|
|
130
|
-
async getBillingHistory(
|
|
131
|
-
const result = await callable<
|
|
132
|
+
async getBillingHistory(userId: string): Promise<OrderItem[]> {
|
|
133
|
+
const result = await callable<{ userId: string }, { orders?: OrderItem[] }>(
|
|
132
134
|
callables.billing,
|
|
133
|
-
{},
|
|
135
|
+
{ userId },
|
|
134
136
|
);
|
|
135
137
|
return result.orders ?? [];
|
|
136
138
|
},
|
|
@@ -139,10 +141,10 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
139
141
|
return callable<{ reason?: string }, CancelResult>(callables.cancel, { reason });
|
|
140
142
|
},
|
|
141
143
|
|
|
142
|
-
async getPortalUrl(
|
|
143
|
-
const result = await callable<
|
|
144
|
+
async getPortalUrl(userId: string): Promise<string> {
|
|
145
|
+
const result = await callable<{ userId: string }, { url: string }>(
|
|
144
146
|
callables.portal,
|
|
145
|
-
{},
|
|
147
|
+
{ userId },
|
|
146
148
|
);
|
|
147
149
|
return result.url;
|
|
148
150
|
},
|
|
@@ -13,6 +13,6 @@ export function isTimestamp(value: unknown): value is { toDate(): Date } {
|
|
|
13
13
|
typeof value === 'object' &&
|
|
14
14
|
value !== null &&
|
|
15
15
|
'toDate' in value &&
|
|
16
|
-
typeof (value as { toDate
|
|
16
|
+
'function' === typeof (value as { toDate?: unknown }).toDate
|
|
17
17
|
);
|
|
18
18
|
}
|
|
@@ -60,7 +60,6 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
60
60
|
if (refreshMountedRef.current) setStatus(s);
|
|
61
61
|
} catch (err) {
|
|
62
62
|
if (refreshMountedRef.current) {
|
|
63
|
-
console.error('[polar-billing] getStatus failed:', err);
|
|
64
63
|
setStatus(FREE_STATUS);
|
|
65
64
|
}
|
|
66
65
|
} finally {
|
|
@@ -87,7 +86,7 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
if (isProductionInsecureUrl(result.url)) {
|
|
90
|
-
|
|
89
|
+
throw new Error('[polar-billing] ERROR: Using insecure http:// URL in production environment');
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
window.location.href = result.url;
|