@umituz/web-polar-payment 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +14 -49
- package/dist/index.d.ts +14 -49
- package/dist/index.js +115 -65
- package/dist/index.mjs +108 -65
- package/package.json +1 -1
- package/src/domain/entities/cancellation.entity.ts +0 -5
- package/src/domain/entities/checkout.entity.ts +0 -6
- package/src/domain/entities/order.entity.ts +0 -5
- package/src/domain/entities/subscription.entity.ts +0 -6
- package/src/domain/entities/sync.entity.ts +0 -5
- package/src/domain/index.ts +0 -5
- package/src/index.ts +0 -8
- package/src/infrastructure/constants/billing.constants.ts +12 -7
- package/src/infrastructure/index.ts +2 -5
- package/src/infrastructure/services/firebase-billing.service.ts +24 -41
- package/src/infrastructure/utils/firebase-helpers.util.ts +18 -0
- package/src/infrastructure/utils/normalization.util.ts +15 -23
- package/src/infrastructure/utils/validations.util.ts +17 -0
- package/src/presentation/components/PolarProvider.tsx +41 -34
- package/src/presentation/hooks/usePolarBilling.ts +0 -4
- package/src/presentation/index.ts +0 -5
package/dist/index.mjs
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
|
+
// src/infrastructure/constants/billing.constants.ts
|
|
2
|
+
var SUBSCRIPTION_STATUS = Object.freeze({
|
|
3
|
+
ACTIVE: "active",
|
|
4
|
+
CANCELED: "canceled",
|
|
5
|
+
REVOKED: "revoked",
|
|
6
|
+
TRIALING: "trialing",
|
|
7
|
+
PAST_DUE: "past_due",
|
|
8
|
+
INCOMPLETE: "incomplete",
|
|
9
|
+
INCOMPLETE_EXPIRED: "incomplete_expired",
|
|
10
|
+
UNPAID: "unpaid",
|
|
11
|
+
NONE: "none"
|
|
12
|
+
});
|
|
13
|
+
var FREE_PLAN = "free";
|
|
14
|
+
|
|
1
15
|
// src/infrastructure/utils/normalization.util.ts
|
|
16
|
+
var STATUS_MAP = Object.freeze({
|
|
17
|
+
[SUBSCRIPTION_STATUS.ACTIVE]: SUBSCRIPTION_STATUS.ACTIVE,
|
|
18
|
+
[SUBSCRIPTION_STATUS.CANCELED]: SUBSCRIPTION_STATUS.CANCELED,
|
|
19
|
+
[SUBSCRIPTION_STATUS.REVOKED]: SUBSCRIPTION_STATUS.REVOKED,
|
|
20
|
+
[SUBSCRIPTION_STATUS.TRIALING]: SUBSCRIPTION_STATUS.TRIALING,
|
|
21
|
+
[SUBSCRIPTION_STATUS.PAST_DUE]: SUBSCRIPTION_STATUS.PAST_DUE,
|
|
22
|
+
[SUBSCRIPTION_STATUS.INCOMPLETE]: SUBSCRIPTION_STATUS.INCOMPLETE,
|
|
23
|
+
[SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED]: SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED,
|
|
24
|
+
[SUBSCRIPTION_STATUS.UNPAID]: SUBSCRIPTION_STATUS.UNPAID,
|
|
25
|
+
[SUBSCRIPTION_STATUS.NONE]: SUBSCRIPTION_STATUS.NONE
|
|
26
|
+
});
|
|
2
27
|
function normalizeStatus(raw) {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
if (typeof raw !== "string") return "none";
|
|
16
|
-
return map[raw.toLowerCase()] ?? "none";
|
|
28
|
+
if (raw == null || typeof raw !== "string") return "none";
|
|
29
|
+
return STATUS_MAP[raw.toLowerCase()] ?? "none";
|
|
17
30
|
}
|
|
18
31
|
function normalizeBillingCycle(interval) {
|
|
19
32
|
if (typeof interval !== "string") return "monthly";
|
|
@@ -23,7 +36,7 @@ function normalizeBillingCycle(interval) {
|
|
|
23
36
|
return "monthly";
|
|
24
37
|
}
|
|
25
38
|
|
|
26
|
-
// src/infrastructure/
|
|
39
|
+
// src/infrastructure/utils/firebase-helpers.util.ts
|
|
27
40
|
function asString(value) {
|
|
28
41
|
if (typeof value === "string") return value;
|
|
29
42
|
return void 0;
|
|
@@ -35,6 +48,8 @@ function asBoolean(value) {
|
|
|
35
48
|
function isTimestamp(value) {
|
|
36
49
|
return typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate === "function";
|
|
37
50
|
}
|
|
51
|
+
|
|
52
|
+
// src/infrastructure/services/firebase-billing.service.ts
|
|
38
53
|
function createFirebaseAdapter(config) {
|
|
39
54
|
const functions = config.functions;
|
|
40
55
|
const firestore = config.firestore;
|
|
@@ -55,15 +70,31 @@ function createFirebaseAdapter(config) {
|
|
|
55
70
|
cancelAtPeriodEnd: config.db?.cancelAtPeriodEndField ?? "cancelAtPeriodEnd",
|
|
56
71
|
currentPeriodEnd: config.db?.currentPeriodEndField ?? "currentPeriodEnd"
|
|
57
72
|
};
|
|
73
|
+
let httpsCallableCache = null;
|
|
74
|
+
let firestoreCache = null;
|
|
75
|
+
async function getHttpsCallable() {
|
|
76
|
+
if (!httpsCallableCache) {
|
|
77
|
+
const mod = await import("firebase/functions");
|
|
78
|
+
httpsCallableCache = mod.httpsCallable;
|
|
79
|
+
}
|
|
80
|
+
return httpsCallableCache;
|
|
81
|
+
}
|
|
82
|
+
async function getFirestore() {
|
|
83
|
+
if (!firestoreCache) {
|
|
84
|
+
const mod = await import("firebase/firestore");
|
|
85
|
+
firestoreCache = { doc: mod.doc, getDoc: mod.getDoc };
|
|
86
|
+
}
|
|
87
|
+
return firestoreCache;
|
|
88
|
+
}
|
|
58
89
|
async function callable(name, data) {
|
|
59
|
-
const
|
|
90
|
+
const httpsCallable = await getHttpsCallable();
|
|
60
91
|
const fn = httpsCallable(functions, name);
|
|
61
92
|
const result = await fn(data);
|
|
62
93
|
return result.data;
|
|
63
94
|
}
|
|
64
95
|
return {
|
|
65
96
|
async getStatus(userId) {
|
|
66
|
-
const { doc, getDoc } = await
|
|
97
|
+
const { doc, getDoc } = await getFirestore();
|
|
67
98
|
const snap = await getDoc(doc(firestore, db.collection, userId));
|
|
68
99
|
if (!snap.exists()) {
|
|
69
100
|
return { plan: "free", subscriptionStatus: "none" };
|
|
@@ -109,26 +140,26 @@ function createFirebaseAdapter(config) {
|
|
|
109
140
|
callables.portal,
|
|
110
141
|
{}
|
|
111
142
|
);
|
|
112
|
-
|
|
113
|
-
if (!url) throw new Error("No portal URL returned from Cloud Function");
|
|
114
|
-
return url;
|
|
143
|
+
return result.url;
|
|
115
144
|
}
|
|
116
145
|
};
|
|
117
146
|
}
|
|
118
147
|
|
|
119
|
-
// src/infrastructure/
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
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
|
+
}
|
|
132
163
|
|
|
133
164
|
// src/presentation/components/PolarProvider.tsx
|
|
134
165
|
import {
|
|
@@ -155,67 +186,72 @@ var FREE_STATUS = {
|
|
|
155
186
|
plan: "free",
|
|
156
187
|
subscriptionStatus: "none"
|
|
157
188
|
};
|
|
158
|
-
function normalizeUserId(userId) {
|
|
159
|
-
if (typeof userId !== "string") return void 0;
|
|
160
|
-
const trimmed = userId.trim();
|
|
161
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
162
|
-
}
|
|
163
189
|
function PolarProvider({ adapter, userId, children }) {
|
|
164
190
|
const [status, setStatus] = useState(FREE_STATUS);
|
|
165
191
|
const [loading, setLoading] = useState(true);
|
|
166
192
|
const adapterRef = useRef(adapter);
|
|
193
|
+
const statusRef = useRef({ setStatus, setLoading });
|
|
194
|
+
const userIdRef = useRef(normalizeUserId(userId));
|
|
195
|
+
const refreshMountedRef = useRef(true);
|
|
167
196
|
adapterRef.current = adapter;
|
|
168
|
-
|
|
197
|
+
statusRef.current = { setStatus, setLoading };
|
|
198
|
+
userIdRef.current = normalizeUserId(userId);
|
|
169
199
|
const refresh = useCallback(async () => {
|
|
170
|
-
const uid =
|
|
200
|
+
const uid = userIdRef.current;
|
|
201
|
+
const { setStatus: setStatus2, setLoading: setLoading2 } = statusRef.current;
|
|
171
202
|
if (!uid) {
|
|
172
|
-
|
|
173
|
-
|
|
203
|
+
setStatus2(FREE_STATUS);
|
|
204
|
+
setLoading2(false);
|
|
174
205
|
return;
|
|
175
206
|
}
|
|
176
|
-
|
|
177
|
-
const ctrl = new AbortController();
|
|
178
|
-
refreshAbortRef.current = ctrl;
|
|
207
|
+
refreshMountedRef.current = true;
|
|
179
208
|
try {
|
|
180
|
-
|
|
209
|
+
setLoading2(true);
|
|
181
210
|
const s = await adapterRef.current.getStatus(uid);
|
|
182
|
-
if (
|
|
211
|
+
if (refreshMountedRef.current) setStatus2(s);
|
|
183
212
|
} catch (err) {
|
|
184
|
-
if (
|
|
213
|
+
if (refreshMountedRef.current) {
|
|
185
214
|
console.error("[polar-billing] getStatus failed:", err);
|
|
186
|
-
|
|
215
|
+
setStatus2(FREE_STATUS);
|
|
187
216
|
}
|
|
188
217
|
} finally {
|
|
189
|
-
if (
|
|
218
|
+
if (refreshMountedRef.current) setLoading2(false);
|
|
190
219
|
}
|
|
191
|
-
}, [
|
|
220
|
+
}, []);
|
|
192
221
|
useEffect(() => {
|
|
222
|
+
refreshMountedRef.current = true;
|
|
193
223
|
refresh();
|
|
194
224
|
return () => {
|
|
195
|
-
|
|
225
|
+
refreshMountedRef.current = false;
|
|
196
226
|
};
|
|
197
|
-
}, [
|
|
227
|
+
}, [userId]);
|
|
198
228
|
const startCheckout = useCallback(async (params) => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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");
|
|
203
239
|
}
|
|
204
240
|
window.location.href = result.url;
|
|
205
|
-
}, [
|
|
241
|
+
}, []);
|
|
206
242
|
const syncSubscription = useCallback(async () => {
|
|
207
|
-
const uid =
|
|
243
|
+
const uid = userIdRef.current;
|
|
208
244
|
if (!uid) return { synced: false };
|
|
209
245
|
const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
|
|
210
246
|
const result = await adapterRef.current.syncSubscription(uid, checkoutId);
|
|
211
247
|
if (result.synced) await refresh();
|
|
212
248
|
return result;
|
|
213
|
-
}, [
|
|
249
|
+
}, [refresh]);
|
|
214
250
|
const getBillingHistory = useCallback(async () => {
|
|
215
|
-
const uid =
|
|
251
|
+
const uid = userIdRef.current;
|
|
216
252
|
if (!uid) return [];
|
|
217
253
|
return adapterRef.current.getBillingHistory(uid);
|
|
218
|
-
}, [
|
|
254
|
+
}, []);
|
|
219
255
|
const cancelSubscription = useCallback(
|
|
220
256
|
async (reason) => {
|
|
221
257
|
const result = await adapterRef.current.cancelSubscription(reason);
|
|
@@ -225,10 +261,10 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
225
261
|
[refresh]
|
|
226
262
|
);
|
|
227
263
|
const getPortalUrl = useCallback(async () => {
|
|
228
|
-
const uid =
|
|
264
|
+
const uid = userIdRef.current;
|
|
229
265
|
if (!uid) throw new Error("[polar-billing] Cannot get portal URL: No authenticated user");
|
|
230
266
|
return adapterRef.current.getPortalUrl(uid);
|
|
231
|
-
}, [
|
|
267
|
+
}, []);
|
|
232
268
|
const value = useMemo(
|
|
233
269
|
() => ({
|
|
234
270
|
status,
|
|
@@ -240,7 +276,7 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
240
276
|
cancelSubscription,
|
|
241
277
|
getPortalUrl
|
|
242
278
|
}),
|
|
243
|
-
[status, loading, refresh,
|
|
279
|
+
[status, loading, refresh, syncSubscription]
|
|
244
280
|
);
|
|
245
281
|
return /* @__PURE__ */ jsx(PolarContext.Provider, { value, children });
|
|
246
282
|
}
|
|
@@ -249,9 +285,16 @@ export {
|
|
|
249
285
|
PolarContext,
|
|
250
286
|
PolarProvider,
|
|
251
287
|
SUBSCRIPTION_STATUS,
|
|
288
|
+
asBoolean,
|
|
289
|
+
asString,
|
|
252
290
|
createFirebaseAdapter,
|
|
291
|
+
isProductionInsecureUrl,
|
|
292
|
+
isTimestamp,
|
|
293
|
+
isValidCheckoutUrl,
|
|
294
|
+
isValidProductId,
|
|
253
295
|
normalizeBillingCycle,
|
|
254
296
|
normalizeStatus,
|
|
297
|
+
normalizeUserId,
|
|
255
298
|
usePolarBilling,
|
|
256
299
|
useSubscription
|
|
257
300
|
};
|
package/package.json
CHANGED
|
@@ -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
|
-
* 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
|
}
|
package/src/domain/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @umituz/web-polar-payment
|
|
3
|
-
* Universal Polar.sh subscription billing — Firebase adapter
|
|
4
|
-
*
|
|
5
|
-
* IMPORTANT: Apps should NOT use this root barrel export.
|
|
6
|
-
* Use subpath imports instead: "@umituz/web-polar-payment/domain"
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
export * from './domain';
|
|
10
2
|
export * from './infrastructure';
|
|
11
3
|
export * from './presentation';
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Billing Constants
|
|
3
|
-
* @description Standardized subscription states and plan names
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const SUBSCRIPTION_STATUS = {
|
|
1
|
+
export const SUBSCRIPTION_STATUS = Object.freeze({
|
|
7
2
|
ACTIVE: 'active' as const,
|
|
8
3
|
CANCELED: 'canceled' as const,
|
|
9
4
|
REVOKED: 'revoked' as const,
|
|
@@ -13,6 +8,16 @@ export const SUBSCRIPTION_STATUS = {
|
|
|
13
8
|
INCOMPLETE_EXPIRED: 'incomplete_expired' as const,
|
|
14
9
|
UNPAID: 'unpaid' as const,
|
|
15
10
|
NONE: 'none' as const,
|
|
16
|
-
}
|
|
11
|
+
}) as Readonly<{
|
|
12
|
+
ACTIVE: 'active';
|
|
13
|
+
CANCELED: 'canceled';
|
|
14
|
+
REVOKED: 'revoked';
|
|
15
|
+
TRIALING: 'trialing';
|
|
16
|
+
PAST_DUE: 'past_due';
|
|
17
|
+
INCOMPLETE: 'incomplete';
|
|
18
|
+
INCOMPLETE_EXPIRED: 'incomplete_expired';
|
|
19
|
+
UNPAID: 'unpaid';
|
|
20
|
+
NONE: 'none';
|
|
21
|
+
}>;
|
|
17
22
|
|
|
18
23
|
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';
|
|
@@ -9,37 +9,8 @@ import type {
|
|
|
9
9
|
SyncResult,
|
|
10
10
|
} from '../../domain/entities';
|
|
11
11
|
import { normalizeStatus, normalizeBillingCycle } from '../utils/normalization.util';
|
|
12
|
+
import { asString, asBoolean, isTimestamp } from '../utils/firebase-helpers.util';
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
* Type guard to safely extract string value from unknown Firestore data
|
|
15
|
-
*/
|
|
16
|
-
function asString(value: unknown): string | undefined {
|
|
17
|
-
if (typeof value === 'string') return value;
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Type guard to safely extract boolean value from unknown Firestore data
|
|
23
|
-
*/
|
|
24
|
-
function asBoolean(value: unknown): boolean | undefined {
|
|
25
|
-
if (typeof value === 'boolean') return value;
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Type guard to safely check if value is an object with toDate method
|
|
31
|
-
*/
|
|
32
|
-
function isTimestamp(value: unknown): value is { toDate(): Date } {
|
|
33
|
-
return (
|
|
34
|
-
typeof value === 'object' &&
|
|
35
|
-
value !== null &&
|
|
36
|
-
'toDate' in value &&
|
|
37
|
-
typeof (value as { toDate: unknown }).toDate === 'function'
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Internal type aliases for Firebase SDK compatibility
|
|
42
|
-
// Using 'any' internally to avoid DTS build issues with external Firebase packages
|
|
43
14
|
type FirebaseFunctions = any;
|
|
44
15
|
type FirebaseFirestore = any;
|
|
45
16
|
|
|
@@ -67,12 +38,7 @@ export interface FirebaseAdapterConfig {
|
|
|
67
38
|
};
|
|
68
39
|
}
|
|
69
40
|
|
|
70
|
-
/**
|
|
71
|
-
* Firebase Billing Service
|
|
72
|
-
* @description Implementation of PolarAdapter for Firebase Functions and Firestore.
|
|
73
|
-
*/
|
|
74
41
|
export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapter {
|
|
75
|
-
// Cast internally to Firebase types for implementation
|
|
76
42
|
const functions = config.functions as FirebaseFunctions;
|
|
77
43
|
const firestore = config.firestore as FirebaseFirestore;
|
|
78
44
|
|
|
@@ -95,8 +61,27 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
95
61
|
currentPeriodEnd: config.db?.currentPeriodEndField ?? 'currentPeriodEnd',
|
|
96
62
|
};
|
|
97
63
|
|
|
64
|
+
let httpsCallableCache: typeof import('firebase/functions')['httpsCallable'] | null = null;
|
|
65
|
+
let firestoreCache: { doc: any; getDoc: any } | null = null;
|
|
66
|
+
|
|
67
|
+
async function getHttpsCallable() {
|
|
68
|
+
if (!httpsCallableCache) {
|
|
69
|
+
const mod = await import('firebase/functions');
|
|
70
|
+
httpsCallableCache = mod.httpsCallable;
|
|
71
|
+
}
|
|
72
|
+
return httpsCallableCache;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function getFirestore() {
|
|
76
|
+
if (!firestoreCache) {
|
|
77
|
+
const mod = await import('firebase/firestore');
|
|
78
|
+
firestoreCache = { doc: mod.doc, getDoc: mod.getDoc };
|
|
79
|
+
}
|
|
80
|
+
return firestoreCache;
|
|
81
|
+
}
|
|
82
|
+
|
|
98
83
|
async function callable<T = unknown, R = unknown>(name: string, data?: T): Promise<R> {
|
|
99
|
-
const
|
|
84
|
+
const httpsCallable = await getHttpsCallable();
|
|
100
85
|
const fn = (httpsCallable as any)(functions, name) as (data?: T) => Promise<{ data: R }>;
|
|
101
86
|
const result = await fn(data);
|
|
102
87
|
return result.data;
|
|
@@ -104,7 +89,7 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
104
89
|
|
|
105
90
|
return {
|
|
106
91
|
async getStatus(userId: string): Promise<SubscriptionStatus> {
|
|
107
|
-
const { doc, getDoc } = await
|
|
92
|
+
const { doc, getDoc } = await getFirestore();
|
|
108
93
|
const snap = await getDoc((doc as any)(firestore, db.collection, userId));
|
|
109
94
|
|
|
110
95
|
if (!snap.exists()) {
|
|
@@ -155,13 +140,11 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
155
140
|
},
|
|
156
141
|
|
|
157
142
|
async getPortalUrl(_userId: string): Promise<string> {
|
|
158
|
-
const result = await callable<Record<string, never>, { url
|
|
143
|
+
const result = await callable<Record<string, never>, { url: string }>(
|
|
159
144
|
callables.portal,
|
|
160
145
|
{},
|
|
161
146
|
);
|
|
162
|
-
|
|
163
|
-
if (!url) throw new Error('No portal URL returned from Cloud Function');
|
|
164
|
-
return url;
|
|
147
|
+
return result.url;
|
|
165
148
|
},
|
|
166
149
|
};
|
|
167
150
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function asString(value: unknown): string | undefined {
|
|
2
|
+
if (typeof value === 'string') return value;
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function asBoolean(value: unknown): boolean | undefined {
|
|
7
|
+
if (typeof value === 'boolean') return value;
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isTimestamp(value: unknown): value is { toDate(): Date } {
|
|
12
|
+
return (
|
|
13
|
+
typeof value === 'object' &&
|
|
14
|
+
value !== null &&
|
|
15
|
+
'toDate' in value &&
|
|
16
|
+
typeof (value as { toDate: unknown }).toDate === 'function'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
import type { SubscriptionStatusValue, BillingCycle } from '../../domain/entities';
|
|
2
|
+
import { SUBSCRIPTION_STATUS } from '../constants/billing.constants';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
unpaid: 'unpaid',
|
|
15
|
-
canceled: 'canceled',
|
|
16
|
-
cancelled: 'canceled',
|
|
17
|
-
revoked: 'revoked',
|
|
18
|
-
none: 'none',
|
|
19
|
-
};
|
|
4
|
+
const STATUS_MAP: Readonly<Record<string, SubscriptionStatusValue>> = Object.freeze({
|
|
5
|
+
[SUBSCRIPTION_STATUS.ACTIVE]: SUBSCRIPTION_STATUS.ACTIVE,
|
|
6
|
+
[SUBSCRIPTION_STATUS.CANCELED]: SUBSCRIPTION_STATUS.CANCELED,
|
|
7
|
+
[SUBSCRIPTION_STATUS.REVOKED]: SUBSCRIPTION_STATUS.REVOKED,
|
|
8
|
+
[SUBSCRIPTION_STATUS.TRIALING]: SUBSCRIPTION_STATUS.TRIALING,
|
|
9
|
+
[SUBSCRIPTION_STATUS.PAST_DUE]: SUBSCRIPTION_STATUS.PAST_DUE,
|
|
10
|
+
[SUBSCRIPTION_STATUS.INCOMPLETE]: SUBSCRIPTION_STATUS.INCOMPLETE,
|
|
11
|
+
[SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED]: SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED,
|
|
12
|
+
[SUBSCRIPTION_STATUS.UNPAID]: SUBSCRIPTION_STATUS.UNPAID,
|
|
13
|
+
[SUBSCRIPTION_STATUS.NONE]: SUBSCRIPTION_STATUS.NONE,
|
|
14
|
+
});
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
export function normalizeStatus(raw: string): SubscriptionStatusValue {
|
|
17
|
+
if (raw == null || typeof raw !== 'string') return 'none';
|
|
18
|
+
return STATUS_MAP[raw.toLowerCase()] ?? 'none';
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
/**
|
|
26
|
-
* Normalize billing interval
|
|
27
|
-
* @description Maps 'month'/'year' to 'monthly'/'yearly'. Defaults to 'monthly' for unknown values or non-string input.
|
|
28
|
-
*/
|
|
29
21
|
export function normalizeBillingCycle(interval: string): BillingCycle {
|
|
30
22
|
if (typeof interval !== 'string') return 'monthly';
|
|
31
23
|
const normalized = interval.toLowerCase();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function normalizeUserId(userId: string | undefined): string | undefined {
|
|
2
|
+
if (typeof userId !== 'string') return undefined;
|
|
3
|
+
const trimmed = userId.trim();
|
|
4
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isValidProductId(productId: unknown): productId is string {
|
|
8
|
+
return typeof productId === 'string' && productId.trim().length > 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isValidCheckoutUrl(url: string): boolean {
|
|
12
|
+
return url.startsWith('https://') || url.startsWith('http://');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isProductionInsecureUrl(url: string): boolean {
|
|
16
|
+
return url.startsWith('http://') && !url.includes('localhost') && !url.includes('127.0.0.1');
|
|
17
|
+
}
|