@umituz/web-polar-payment 1.0.6 → 1.0.8
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 +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +41 -19
- package/dist/index.mjs +41 -19
- package/package.json +2 -2
- package/src/infrastructure/services/firebase-billing.service.ts +48 -13
- package/src/infrastructure/utils/normalization.util.ts +10 -6
- package/src/presentation/components/PolarProvider.tsx +14 -7
package/dist/index.d.mts
CHANGED
|
@@ -85,11 +85,11 @@ interface PolarAdapter {
|
|
|
85
85
|
getPortalUrl(userId: string): Promise<string>;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
type FirebaseFunctions = any;
|
|
89
|
-
type FirebaseFirestore = any;
|
|
90
88
|
interface FirebaseAdapterConfig {
|
|
91
|
-
functions
|
|
92
|
-
|
|
89
|
+
/** Firebase Functions instance from firebase/functions */
|
|
90
|
+
functions: unknown;
|
|
91
|
+
/** Firebase Firestore instance from firebase/firestore */
|
|
92
|
+
firestore: unknown;
|
|
93
93
|
callables?: {
|
|
94
94
|
createCheckout?: string;
|
|
95
95
|
syncSubscription?: string;
|
|
@@ -133,12 +133,12 @@ declare const FREE_PLAN = "free";
|
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* Normalize a raw Polar status string to a known value.
|
|
136
|
-
* @description Defaults to 'none' for unknown statuses.
|
|
136
|
+
* @description Defaults to 'none' for unknown statuses or non-string input.
|
|
137
137
|
*/
|
|
138
138
|
declare function normalizeStatus(raw: string): SubscriptionStatusValue;
|
|
139
139
|
/**
|
|
140
140
|
* Normalize billing interval
|
|
141
|
-
* @description Maps 'month'/'year' to 'monthly'/'yearly'
|
|
141
|
+
* @description Maps 'month'/'year' to 'monthly'/'yearly'. Defaults to 'monthly' for unknown values or non-string input.
|
|
142
142
|
*/
|
|
143
143
|
declare function normalizeBillingCycle(interval: string): BillingCycle;
|
|
144
144
|
|
package/dist/index.d.ts
CHANGED
|
@@ -85,11 +85,11 @@ interface PolarAdapter {
|
|
|
85
85
|
getPortalUrl(userId: string): Promise<string>;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
type FirebaseFunctions = any;
|
|
89
|
-
type FirebaseFirestore = any;
|
|
90
88
|
interface FirebaseAdapterConfig {
|
|
91
|
-
functions
|
|
92
|
-
|
|
89
|
+
/** Firebase Functions instance from firebase/functions */
|
|
90
|
+
functions: unknown;
|
|
91
|
+
/** Firebase Firestore instance from firebase/firestore */
|
|
92
|
+
firestore: unknown;
|
|
93
93
|
callables?: {
|
|
94
94
|
createCheckout?: string;
|
|
95
95
|
syncSubscription?: string;
|
|
@@ -133,12 +133,12 @@ declare const FREE_PLAN = "free";
|
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* Normalize a raw Polar status string to a known value.
|
|
136
|
-
* @description Defaults to 'none' for unknown statuses.
|
|
136
|
+
* @description Defaults to 'none' for unknown statuses or non-string input.
|
|
137
137
|
*/
|
|
138
138
|
declare function normalizeStatus(raw: string): SubscriptionStatusValue;
|
|
139
139
|
/**
|
|
140
140
|
* Normalize billing interval
|
|
141
|
-
* @description Maps 'month'/'year' to 'monthly'/'yearly'
|
|
141
|
+
* @description Maps 'month'/'year' to 'monthly'/'yearly'. Defaults to 'monthly' for unknown values or non-string input.
|
|
142
142
|
*/
|
|
143
143
|
declare function normalizeBillingCycle(interval: string): BillingCycle;
|
|
144
144
|
|
package/dist/index.js
CHANGED
|
@@ -56,16 +56,32 @@ function normalizeStatus(raw) {
|
|
|
56
56
|
revoked: "revoked",
|
|
57
57
|
none: "none"
|
|
58
58
|
};
|
|
59
|
-
|
|
59
|
+
if (typeof raw !== "string") return "none";
|
|
60
|
+
return map[raw.toLowerCase()] ?? "none";
|
|
60
61
|
}
|
|
61
62
|
function normalizeBillingCycle(interval) {
|
|
62
|
-
if (
|
|
63
|
-
|
|
63
|
+
if (typeof interval !== "string") return "monthly";
|
|
64
|
+
const normalized = interval.toLowerCase();
|
|
65
|
+
if (normalized === "month" || normalized === "monthly") return "monthly";
|
|
66
|
+
if (normalized === "year" || normalized === "yearly") return "yearly";
|
|
64
67
|
return "monthly";
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
// src/infrastructure/services/firebase-billing.service.ts
|
|
71
|
+
function asString(value) {
|
|
72
|
+
if (typeof value === "string") return value;
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
function asBoolean(value) {
|
|
76
|
+
if (typeof value === "boolean") return value;
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
function isTimestamp(value) {
|
|
80
|
+
return typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate === "function";
|
|
81
|
+
}
|
|
68
82
|
function createFirebaseAdapter(config) {
|
|
83
|
+
const functions = config.functions;
|
|
84
|
+
const firestore = config.firestore;
|
|
69
85
|
const callables = {
|
|
70
86
|
createCheckout: config.callables?.createCheckout ?? "createCheckoutSession",
|
|
71
87
|
sync: config.callables?.syncSubscription ?? "syncSubscription",
|
|
@@ -85,14 +101,14 @@ function createFirebaseAdapter(config) {
|
|
|
85
101
|
};
|
|
86
102
|
async function callable(name, data) {
|
|
87
103
|
const { httpsCallable } = await import("firebase/functions");
|
|
88
|
-
const fn = httpsCallable(
|
|
104
|
+
const fn = httpsCallable(functions, name);
|
|
89
105
|
const result = await fn(data);
|
|
90
106
|
return result.data;
|
|
91
107
|
}
|
|
92
108
|
return {
|
|
93
109
|
async getStatus(userId) {
|
|
94
110
|
const { doc, getDoc } = await import("firebase/firestore");
|
|
95
|
-
const snap = await getDoc(doc(
|
|
111
|
+
const snap = await getDoc(doc(firestore, db.collection, userId));
|
|
96
112
|
if (!snap.exists()) {
|
|
97
113
|
return { plan: "free", subscriptionStatus: "none" };
|
|
98
114
|
}
|
|
@@ -100,19 +116,19 @@ function createFirebaseAdapter(config) {
|
|
|
100
116
|
let currentPeriodEnd;
|
|
101
117
|
const rawEnd = d[db.currentPeriodEnd];
|
|
102
118
|
if (rawEnd != null) {
|
|
103
|
-
if (
|
|
119
|
+
if (isTimestamp(rawEnd)) {
|
|
104
120
|
currentPeriodEnd = rawEnd.toDate().toISOString();
|
|
105
121
|
} else if (typeof rawEnd === "string") {
|
|
106
122
|
currentPeriodEnd = rawEnd;
|
|
107
123
|
}
|
|
108
124
|
}
|
|
109
125
|
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],
|
|
126
|
+
plan: asString(d[db.plan]) ?? "free",
|
|
127
|
+
billingCycle: normalizeBillingCycle(asString(d[db.billingCycle]) ?? "monthly"),
|
|
128
|
+
subscriptionId: asString(d[db.subscriptionId]),
|
|
129
|
+
subscriptionStatus: normalizeStatus(asString(d[db.subscriptionStatus]) ?? "none"),
|
|
130
|
+
polarCustomerId: asString(d[db.polarCustomerId]),
|
|
131
|
+
cancelAtPeriodEnd: asBoolean(d[db.cancelAtPeriodEnd]),
|
|
116
132
|
currentPeriodEnd
|
|
117
133
|
};
|
|
118
134
|
},
|
|
@@ -177,6 +193,11 @@ var FREE_STATUS = {
|
|
|
177
193
|
plan: "free",
|
|
178
194
|
subscriptionStatus: "none"
|
|
179
195
|
};
|
|
196
|
+
function normalizeUserId(userId) {
|
|
197
|
+
if (typeof userId !== "string") return void 0;
|
|
198
|
+
const trimmed = userId.trim();
|
|
199
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
200
|
+
}
|
|
180
201
|
function PolarProvider({ adapter, userId, children }) {
|
|
181
202
|
const [status, setStatus] = (0, import_react2.useState)(FREE_STATUS);
|
|
182
203
|
const [loading, setLoading] = (0, import_react2.useState)(true);
|
|
@@ -184,7 +205,7 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
184
205
|
adapterRef.current = adapter;
|
|
185
206
|
const refreshAbortRef = (0, import_react2.useRef)(null);
|
|
186
207
|
const refresh = (0, import_react2.useCallback)(async () => {
|
|
187
|
-
const uid = userId
|
|
208
|
+
const uid = normalizeUserId(userId);
|
|
188
209
|
if (!uid) {
|
|
189
210
|
setStatus(FREE_STATUS);
|
|
190
211
|
setLoading(false);
|
|
@@ -213,14 +234,15 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
213
234
|
};
|
|
214
235
|
}, [refresh]);
|
|
215
236
|
const startCheckout = (0, import_react2.useCallback)(async (params) => {
|
|
216
|
-
const
|
|
237
|
+
const uid = normalizeUserId(userId);
|
|
238
|
+
const result = await adapterRef.current.createCheckout({ ...params, userId: uid });
|
|
217
239
|
if (!result.url.startsWith("https://")) {
|
|
218
|
-
throw new Error("Invalid checkout URL returned");
|
|
240
|
+
throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https://");
|
|
219
241
|
}
|
|
220
242
|
window.location.href = result.url;
|
|
221
243
|
}, [userId]);
|
|
222
244
|
const syncSubscription = (0, import_react2.useCallback)(async () => {
|
|
223
|
-
const uid = userId
|
|
245
|
+
const uid = normalizeUserId(userId);
|
|
224
246
|
if (!uid) return { synced: false };
|
|
225
247
|
const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
|
|
226
248
|
const result = await adapterRef.current.syncSubscription(uid, checkoutId);
|
|
@@ -228,7 +250,7 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
228
250
|
return result;
|
|
229
251
|
}, [userId, refresh]);
|
|
230
252
|
const getBillingHistory = (0, import_react2.useCallback)(async () => {
|
|
231
|
-
const uid = userId
|
|
253
|
+
const uid = normalizeUserId(userId);
|
|
232
254
|
if (!uid) return [];
|
|
233
255
|
return adapterRef.current.getBillingHistory(uid);
|
|
234
256
|
}, [userId]);
|
|
@@ -241,8 +263,8 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
241
263
|
[refresh]
|
|
242
264
|
);
|
|
243
265
|
const getPortalUrl = (0, import_react2.useCallback)(async () => {
|
|
244
|
-
const uid = userId
|
|
245
|
-
if (!uid) throw new Error("No authenticated user");
|
|
266
|
+
const uid = normalizeUserId(userId);
|
|
267
|
+
if (!uid) throw new Error("[polar-billing] Cannot get portal URL: No authenticated user");
|
|
246
268
|
return adapterRef.current.getPortalUrl(uid);
|
|
247
269
|
}, [userId]);
|
|
248
270
|
const value = (0, import_react2.useMemo)(
|
package/dist/index.mjs
CHANGED
|
@@ -12,16 +12,32 @@ function normalizeStatus(raw) {
|
|
|
12
12
|
revoked: "revoked",
|
|
13
13
|
none: "none"
|
|
14
14
|
};
|
|
15
|
-
|
|
15
|
+
if (typeof raw !== "string") return "none";
|
|
16
|
+
return map[raw.toLowerCase()] ?? "none";
|
|
16
17
|
}
|
|
17
18
|
function normalizeBillingCycle(interval) {
|
|
18
|
-
if (
|
|
19
|
-
|
|
19
|
+
if (typeof interval !== "string") return "monthly";
|
|
20
|
+
const normalized = interval.toLowerCase();
|
|
21
|
+
if (normalized === "month" || normalized === "monthly") return "monthly";
|
|
22
|
+
if (normalized === "year" || normalized === "yearly") return "yearly";
|
|
20
23
|
return "monthly";
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
// src/infrastructure/services/firebase-billing.service.ts
|
|
27
|
+
function asString(value) {
|
|
28
|
+
if (typeof value === "string") return value;
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
function asBoolean(value) {
|
|
32
|
+
if (typeof value === "boolean") return value;
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
function isTimestamp(value) {
|
|
36
|
+
return typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate === "function";
|
|
37
|
+
}
|
|
24
38
|
function createFirebaseAdapter(config) {
|
|
39
|
+
const functions = config.functions;
|
|
40
|
+
const firestore = config.firestore;
|
|
25
41
|
const callables = {
|
|
26
42
|
createCheckout: config.callables?.createCheckout ?? "createCheckoutSession",
|
|
27
43
|
sync: config.callables?.syncSubscription ?? "syncSubscription",
|
|
@@ -41,14 +57,14 @@ function createFirebaseAdapter(config) {
|
|
|
41
57
|
};
|
|
42
58
|
async function callable(name, data) {
|
|
43
59
|
const { httpsCallable } = await import("firebase/functions");
|
|
44
|
-
const fn = httpsCallable(
|
|
60
|
+
const fn = httpsCallable(functions, name);
|
|
45
61
|
const result = await fn(data);
|
|
46
62
|
return result.data;
|
|
47
63
|
}
|
|
48
64
|
return {
|
|
49
65
|
async getStatus(userId) {
|
|
50
66
|
const { doc, getDoc } = await import("firebase/firestore");
|
|
51
|
-
const snap = await getDoc(doc(
|
|
67
|
+
const snap = await getDoc(doc(firestore, db.collection, userId));
|
|
52
68
|
if (!snap.exists()) {
|
|
53
69
|
return { plan: "free", subscriptionStatus: "none" };
|
|
54
70
|
}
|
|
@@ -56,19 +72,19 @@ function createFirebaseAdapter(config) {
|
|
|
56
72
|
let currentPeriodEnd;
|
|
57
73
|
const rawEnd = d[db.currentPeriodEnd];
|
|
58
74
|
if (rawEnd != null) {
|
|
59
|
-
if (
|
|
75
|
+
if (isTimestamp(rawEnd)) {
|
|
60
76
|
currentPeriodEnd = rawEnd.toDate().toISOString();
|
|
61
77
|
} else if (typeof rawEnd === "string") {
|
|
62
78
|
currentPeriodEnd = rawEnd;
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
81
|
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],
|
|
82
|
+
plan: asString(d[db.plan]) ?? "free",
|
|
83
|
+
billingCycle: normalizeBillingCycle(asString(d[db.billingCycle]) ?? "monthly"),
|
|
84
|
+
subscriptionId: asString(d[db.subscriptionId]),
|
|
85
|
+
subscriptionStatus: normalizeStatus(asString(d[db.subscriptionStatus]) ?? "none"),
|
|
86
|
+
polarCustomerId: asString(d[db.polarCustomerId]),
|
|
87
|
+
cancelAtPeriodEnd: asBoolean(d[db.cancelAtPeriodEnd]),
|
|
72
88
|
currentPeriodEnd
|
|
73
89
|
};
|
|
74
90
|
},
|
|
@@ -139,6 +155,11 @@ var FREE_STATUS = {
|
|
|
139
155
|
plan: "free",
|
|
140
156
|
subscriptionStatus: "none"
|
|
141
157
|
};
|
|
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
|
+
}
|
|
142
163
|
function PolarProvider({ adapter, userId, children }) {
|
|
143
164
|
const [status, setStatus] = useState(FREE_STATUS);
|
|
144
165
|
const [loading, setLoading] = useState(true);
|
|
@@ -146,7 +167,7 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
146
167
|
adapterRef.current = adapter;
|
|
147
168
|
const refreshAbortRef = useRef(null);
|
|
148
169
|
const refresh = useCallback(async () => {
|
|
149
|
-
const uid = userId
|
|
170
|
+
const uid = normalizeUserId(userId);
|
|
150
171
|
if (!uid) {
|
|
151
172
|
setStatus(FREE_STATUS);
|
|
152
173
|
setLoading(false);
|
|
@@ -175,14 +196,15 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
175
196
|
};
|
|
176
197
|
}, [refresh]);
|
|
177
198
|
const startCheckout = useCallback(async (params) => {
|
|
178
|
-
const
|
|
199
|
+
const uid = normalizeUserId(userId);
|
|
200
|
+
const result = await adapterRef.current.createCheckout({ ...params, userId: uid });
|
|
179
201
|
if (!result.url.startsWith("https://")) {
|
|
180
|
-
throw new Error("Invalid checkout URL returned");
|
|
202
|
+
throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https://");
|
|
181
203
|
}
|
|
182
204
|
window.location.href = result.url;
|
|
183
205
|
}, [userId]);
|
|
184
206
|
const syncSubscription = useCallback(async () => {
|
|
185
|
-
const uid = userId
|
|
207
|
+
const uid = normalizeUserId(userId);
|
|
186
208
|
if (!uid) return { synced: false };
|
|
187
209
|
const checkoutId = new URLSearchParams(window.location.search).get("checkout_id") ?? void 0;
|
|
188
210
|
const result = await adapterRef.current.syncSubscription(uid, checkoutId);
|
|
@@ -190,7 +212,7 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
190
212
|
return result;
|
|
191
213
|
}, [userId, refresh]);
|
|
192
214
|
const getBillingHistory = useCallback(async () => {
|
|
193
|
-
const uid = userId
|
|
215
|
+
const uid = normalizeUserId(userId);
|
|
194
216
|
if (!uid) return [];
|
|
195
217
|
return adapterRef.current.getBillingHistory(uid);
|
|
196
218
|
}, [userId]);
|
|
@@ -203,8 +225,8 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
203
225
|
[refresh]
|
|
204
226
|
);
|
|
205
227
|
const getPortalUrl = useCallback(async () => {
|
|
206
|
-
const uid = userId
|
|
207
|
-
if (!uid) throw new Error("No authenticated user");
|
|
228
|
+
const uid = normalizeUserId(userId);
|
|
229
|
+
if (!uid) throw new Error("[polar-billing] Cannot get portal URL: No authenticated user");
|
|
208
230
|
return adapterRef.current.getPortalUrl(uid);
|
|
209
231
|
}, [userId]);
|
|
210
232
|
const value = useMemo(
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-polar-payment",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Universal Polar.sh subscription billing —
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "Universal Polar.sh subscription billing — Firebase adapter",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -10,13 +10,44 @@ import type {
|
|
|
10
10
|
} from '../../domain/entities';
|
|
11
11
|
import { normalizeStatus, normalizeBillingCycle } from '../utils/normalization.util';
|
|
12
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
|
|
14
43
|
type FirebaseFunctions = any;
|
|
15
44
|
type FirebaseFirestore = any;
|
|
16
45
|
|
|
17
46
|
export interface FirebaseAdapterConfig {
|
|
18
|
-
functions
|
|
19
|
-
|
|
47
|
+
/** Firebase Functions instance from firebase/functions */
|
|
48
|
+
functions: unknown;
|
|
49
|
+
/** Firebase Firestore instance from firebase/firestore */
|
|
50
|
+
firestore: unknown;
|
|
20
51
|
callables?: {
|
|
21
52
|
createCheckout?: string;
|
|
22
53
|
syncSubscription?: string;
|
|
@@ -41,6 +72,10 @@ export interface FirebaseAdapterConfig {
|
|
|
41
72
|
* @description Implementation of PolarAdapter for Firebase Functions and Firestore.
|
|
42
73
|
*/
|
|
43
74
|
export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapter {
|
|
75
|
+
// Cast internally to Firebase types for implementation
|
|
76
|
+
const functions = config.functions as FirebaseFunctions;
|
|
77
|
+
const firestore = config.firestore as FirebaseFirestore;
|
|
78
|
+
|
|
44
79
|
const callables = {
|
|
45
80
|
createCheckout: config.callables?.createCheckout ?? 'createCheckoutSession',
|
|
46
81
|
sync: config.callables?.syncSubscription ?? 'syncSubscription',
|
|
@@ -62,7 +97,7 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
62
97
|
|
|
63
98
|
async function callable<T = unknown, R = unknown>(name: string, data?: T): Promise<R> {
|
|
64
99
|
const { httpsCallable } = await import('firebase/functions');
|
|
65
|
-
const fn = (httpsCallable as any)(
|
|
100
|
+
const fn = (httpsCallable as any)(functions, name) as (data?: T) => Promise<{ data: R }>;
|
|
66
101
|
const result = await fn(data);
|
|
67
102
|
return result.data;
|
|
68
103
|
}
|
|
@@ -70,7 +105,7 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
70
105
|
return {
|
|
71
106
|
async getStatus(userId: string): Promise<SubscriptionStatus> {
|
|
72
107
|
const { doc, getDoc } = await import('firebase/firestore');
|
|
73
|
-
const snap = await getDoc((doc as any)(
|
|
108
|
+
const snap = await getDoc((doc as any)(firestore, db.collection, userId));
|
|
74
109
|
|
|
75
110
|
if (!snap.exists()) {
|
|
76
111
|
return { plan: 'free', subscriptionStatus: 'none' };
|
|
@@ -81,20 +116,20 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
81
116
|
let currentPeriodEnd: string | undefined;
|
|
82
117
|
const rawEnd = d[db.currentPeriodEnd];
|
|
83
118
|
if (rawEnd != null) {
|
|
84
|
-
if (
|
|
85
|
-
currentPeriodEnd =
|
|
119
|
+
if (isTimestamp(rawEnd)) {
|
|
120
|
+
currentPeriodEnd = rawEnd.toDate().toISOString();
|
|
86
121
|
} else if (typeof rawEnd === 'string') {
|
|
87
122
|
currentPeriodEnd = rawEnd;
|
|
88
123
|
}
|
|
89
124
|
}
|
|
90
125
|
|
|
91
126
|
return {
|
|
92
|
-
plan: (d[db.plan]
|
|
93
|
-
billingCycle: normalizeBillingCycle((d[db.billingCycle]
|
|
94
|
-
subscriptionId: d[db.subscriptionId]
|
|
95
|
-
subscriptionStatus: normalizeStatus((d[db.subscriptionStatus]
|
|
96
|
-
polarCustomerId: d[db.polarCustomerId]
|
|
97
|
-
cancelAtPeriodEnd: d[db.cancelAtPeriodEnd]
|
|
127
|
+
plan: asString(d[db.plan]) ?? 'free',
|
|
128
|
+
billingCycle: normalizeBillingCycle(asString(d[db.billingCycle]) ?? 'monthly'),
|
|
129
|
+
subscriptionId: asString(d[db.subscriptionId]),
|
|
130
|
+
subscriptionStatus: normalizeStatus(asString(d[db.subscriptionStatus]) ?? 'none'),
|
|
131
|
+
polarCustomerId: asString(d[db.polarCustomerId]),
|
|
132
|
+
cancelAtPeriodEnd: asBoolean(d[db.cancelAtPeriodEnd]),
|
|
98
133
|
currentPeriodEnd,
|
|
99
134
|
};
|
|
100
135
|
},
|
|
@@ -2,7 +2,7 @@ import type { SubscriptionStatusValue, BillingCycle } from '../../domain/entitie
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Normalize a raw Polar status string to a known value.
|
|
5
|
-
* @description Defaults to 'none' for unknown statuses.
|
|
5
|
+
* @description Defaults to 'none' for unknown statuses or non-string input.
|
|
6
6
|
*/
|
|
7
7
|
export function normalizeStatus(raw: string): SubscriptionStatusValue {
|
|
8
8
|
const map: Record<string, SubscriptionStatusValue> = {
|
|
@@ -17,15 +17,19 @@ export function normalizeStatus(raw: string): SubscriptionStatusValue {
|
|
|
17
17
|
revoked: 'revoked',
|
|
18
18
|
none: 'none',
|
|
19
19
|
};
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
if (typeof raw !== 'string') return 'none';
|
|
22
|
+
return map[raw.toLowerCase()] ?? 'none';
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
/**
|
|
25
|
+
/**
|
|
24
26
|
* Normalize billing interval
|
|
25
|
-
* @description Maps 'month'/'year' to 'monthly'/'yearly'
|
|
27
|
+
* @description Maps 'month'/'year' to 'monthly'/'yearly'. Defaults to 'monthly' for unknown values or non-string input.
|
|
26
28
|
*/
|
|
27
29
|
export function normalizeBillingCycle(interval: string): BillingCycle {
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
+
if (typeof interval !== 'string') return 'monthly';
|
|
31
|
+
const normalized = interval.toLowerCase();
|
|
32
|
+
if (normalized === 'month' || normalized === 'monthly') return 'monthly';
|
|
33
|
+
if (normalized === 'year' || normalized === 'yearly') return 'yearly';
|
|
30
34
|
return 'monthly';
|
|
31
35
|
}
|
|
@@ -33,6 +33,12 @@ const FREE_STATUS: SubscriptionStatus = {
|
|
|
33
33
|
subscriptionStatus: 'none',
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
function normalizeUserId(userId: string | undefined): string | undefined {
|
|
37
|
+
if (typeof userId !== 'string') return undefined;
|
|
38
|
+
const trimmed = userId.trim();
|
|
39
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
export function PolarProvider({ adapter, userId, children }: PolarProviderProps) {
|
|
37
43
|
const [status, setStatus] = useState<SubscriptionStatus>(FREE_STATUS);
|
|
38
44
|
const [loading, setLoading] = useState(true);
|
|
@@ -41,7 +47,7 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
41
47
|
const refreshAbortRef = useRef<AbortController | null>(null);
|
|
42
48
|
|
|
43
49
|
const refresh = useCallback(async () => {
|
|
44
|
-
const uid = userId
|
|
50
|
+
const uid = normalizeUserId(userId);
|
|
45
51
|
if (!uid) {
|
|
46
52
|
setStatus(FREE_STATUS);
|
|
47
53
|
setLoading(false);
|
|
@@ -72,15 +78,16 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
72
78
|
}, [refresh]);
|
|
73
79
|
|
|
74
80
|
const startCheckout = useCallback(async (params: CheckoutParams) => {
|
|
75
|
-
const
|
|
81
|
+
const uid = normalizeUserId(userId);
|
|
82
|
+
const result = await adapterRef.current.createCheckout({ ...params, userId: uid });
|
|
76
83
|
if (!result.url.startsWith('https://')) {
|
|
77
|
-
throw new Error('Invalid checkout URL returned');
|
|
84
|
+
throw new Error('[polar-billing] Invalid checkout URL returned: URL must start with https://');
|
|
78
85
|
}
|
|
79
86
|
window.location.href = result.url;
|
|
80
87
|
}, [userId]);
|
|
81
88
|
|
|
82
89
|
const syncSubscription = useCallback(async (): Promise<SyncResult> => {
|
|
83
|
-
const uid = userId
|
|
90
|
+
const uid = normalizeUserId(userId);
|
|
84
91
|
if (!uid) return { synced: false };
|
|
85
92
|
|
|
86
93
|
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id') ?? undefined;
|
|
@@ -90,7 +97,7 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
90
97
|
}, [userId, refresh]);
|
|
91
98
|
|
|
92
99
|
const getBillingHistory = useCallback(async (): Promise<OrderItem[]> => {
|
|
93
|
-
const uid = userId
|
|
100
|
+
const uid = normalizeUserId(userId);
|
|
94
101
|
if (!uid) return [];
|
|
95
102
|
return adapterRef.current.getBillingHistory(uid);
|
|
96
103
|
}, [userId]);
|
|
@@ -105,8 +112,8 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
105
112
|
);
|
|
106
113
|
|
|
107
114
|
const getPortalUrl = useCallback(async (): Promise<string> => {
|
|
108
|
-
const uid = userId
|
|
109
|
-
if (!uid) throw new Error('No authenticated user');
|
|
115
|
+
const uid = normalizeUserId(userId);
|
|
116
|
+
if (!uid) throw new Error('[polar-billing] Cannot get portal URL: No authenticated user');
|
|
110
117
|
return adapterRef.current.getPortalUrl(uid);
|
|
111
118
|
}, [userId]);
|
|
112
119
|
|