@umituz/web-polar-payment 1.0.5 → 1.0.7
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 +4 -3
- package/src/declarations.d.ts +2 -0
- package/src/domain/entities/cancellation.entity.ts +19 -0
- package/src/domain/entities/checkout.entity.ts +18 -0
- package/src/domain/entities/index.ts +5 -0
- package/src/domain/entities/order.entity.ts +15 -0
- package/src/domain/entities/subscription.entity.ts +29 -0
- package/src/domain/entities/sync.entity.ts +9 -0
- package/src/domain/index.ts +7 -0
- package/src/domain/interfaces/index.ts +1 -0
- package/src/domain/interfaces/polar-adapter.interface.ts +23 -0
- package/src/index.ts +11 -0
- package/src/infrastructure/constants/billing.constants.ts +18 -0
- package/src/infrastructure/index.ts +8 -0
- package/src/infrastructure/services/firebase-billing.service.ts +167 -0
- package/src/infrastructure/utils/normalization.util.ts +35 -0
- package/src/presentation/components/PolarProvider.tsx +135 -0
- package/src/presentation/hooks/usePolarBilling.ts +34 -0
- package/src/presentation/index.ts +7 -0
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.7",
|
|
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",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"./presentation": "./src/presentation/index.ts"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
|
-
"dist"
|
|
23
|
+
"dist",
|
|
24
|
+
"src"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean --external firebase --external firebase/functions --external firebase/firestore",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cancellation Entity
|
|
3
|
+
* @description Types for subscription cancellation reasons and outcomes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type CancellationReason =
|
|
7
|
+
| 'too_expensive'
|
|
8
|
+
| 'missing_features'
|
|
9
|
+
| 'switched_service'
|
|
10
|
+
| 'unused'
|
|
11
|
+
| 'customer_service'
|
|
12
|
+
| 'low_quality'
|
|
13
|
+
| 'too_complex'
|
|
14
|
+
| 'other';
|
|
15
|
+
|
|
16
|
+
export interface CancelResult {
|
|
17
|
+
success: boolean;
|
|
18
|
+
endsAt?: string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkout Entity
|
|
3
|
+
* @description Types for initiating and following process of checkouts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CheckoutParams {
|
|
7
|
+
productId: string;
|
|
8
|
+
planKey?: string;
|
|
9
|
+
billingCycle?: 'monthly' | 'yearly';
|
|
10
|
+
successUrl?: string;
|
|
11
|
+
/** Injected automatically by PolarProvider — do not pass manually */
|
|
12
|
+
userId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CheckoutResult {
|
|
16
|
+
url: string;
|
|
17
|
+
id: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Order Entity
|
|
3
|
+
* @description Types for billing history items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface OrderItem {
|
|
7
|
+
id: string;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
amount: number;
|
|
10
|
+
currency: string;
|
|
11
|
+
status: string;
|
|
12
|
+
paid: boolean;
|
|
13
|
+
productName: string;
|
|
14
|
+
invoiceUrl?: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Entity
|
|
3
|
+
* @description Types for subscription status and billing cycles
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type SubscriptionStatusValue =
|
|
7
|
+
| 'active'
|
|
8
|
+
| 'canceled'
|
|
9
|
+
| 'revoked'
|
|
10
|
+
| 'trialing'
|
|
11
|
+
| 'past_due'
|
|
12
|
+
| 'incomplete'
|
|
13
|
+
| 'incomplete_expired'
|
|
14
|
+
| 'unpaid'
|
|
15
|
+
| 'none';
|
|
16
|
+
|
|
17
|
+
export type BillingCycle = 'monthly' | 'yearly';
|
|
18
|
+
|
|
19
|
+
export interface SubscriptionStatus {
|
|
20
|
+
plan: string;
|
|
21
|
+
subscriptionId?: string;
|
|
22
|
+
subscriptionStatus: SubscriptionStatusValue;
|
|
23
|
+
cancelAtPeriodEnd?: boolean;
|
|
24
|
+
currentPeriodEnd?: string;
|
|
25
|
+
billingCycle?: BillingCycle;
|
|
26
|
+
polarCustomerId?: string;
|
|
27
|
+
/** Token balance (for token-based projects like Aria) */
|
|
28
|
+
tokens?: number;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './polar-adapter.interface';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SubscriptionStatus,
|
|
3
|
+
CheckoutParams,
|
|
4
|
+
CheckoutResult,
|
|
5
|
+
OrderItem,
|
|
6
|
+
CancellationReason,
|
|
7
|
+
CancelResult,
|
|
8
|
+
SyncResult,
|
|
9
|
+
} from '../entities';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Backend-agnostic interface every adapter must implement.
|
|
13
|
+
* @description Contract for Polar billing adapters (Firebase, Supabase, etc.)
|
|
14
|
+
*/
|
|
15
|
+
export interface PolarAdapter {
|
|
16
|
+
getStatus(userId: string): Promise<SubscriptionStatus>;
|
|
17
|
+
createCheckout(params: CheckoutParams): Promise<CheckoutResult>;
|
|
18
|
+
/** checkoutId is read from URL by the context and passed explicitly */
|
|
19
|
+
syncSubscription(userId: string, checkoutId?: string): Promise<SyncResult>;
|
|
20
|
+
getBillingHistory(userId: string): Promise<OrderItem[]>;
|
|
21
|
+
cancelSubscription(reason?: CancellationReason): Promise<CancelResult>;
|
|
22
|
+
getPortalUrl(userId: string): Promise<string>;
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/web-polar-payment
|
|
3
|
+
* Universal Polar.sh subscription billing — Firebase adapter
|
|
4
|
+
*
|
|
5
|
+
* ONEMLI: App'ler bu root barrel'i kullanMAMALI.
|
|
6
|
+
* Subpath import kullanin: "@umituz/web-polar-payment/domain"
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from './domain';
|
|
10
|
+
export * from './infrastructure';
|
|
11
|
+
export * from './presentation';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Constants
|
|
3
|
+
* @description Standardized subscription states and plan names
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const SUBSCRIPTION_STATUS = {
|
|
7
|
+
ACTIVE: 'active' as const,
|
|
8
|
+
CANCELED: 'canceled' as const,
|
|
9
|
+
REVOKED: 'revoked' as const,
|
|
10
|
+
TRIALING: 'trialing' as const,
|
|
11
|
+
PAST_DUE: 'past_due' as const,
|
|
12
|
+
INCOMPLETE: 'incomplete' as const,
|
|
13
|
+
INCOMPLETE_EXPIRED: 'incomplete_expired' as const,
|
|
14
|
+
UNPAID: 'unpaid' as const,
|
|
15
|
+
NONE: 'none' as const,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const FREE_PLAN = 'free';
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { PolarAdapter } from '../../domain/interfaces';
|
|
2
|
+
import type {
|
|
3
|
+
CheckoutParams,
|
|
4
|
+
CheckoutResult,
|
|
5
|
+
OrderItem,
|
|
6
|
+
CancellationReason,
|
|
7
|
+
CancelResult,
|
|
8
|
+
SubscriptionStatus,
|
|
9
|
+
SyncResult,
|
|
10
|
+
} from '../../domain/entities';
|
|
11
|
+
import { normalizeStatus, normalizeBillingCycle } from '../utils/normalization.util';
|
|
12
|
+
|
|
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
|
+
type FirebaseFunctions = any;
|
|
44
|
+
type FirebaseFirestore = any;
|
|
45
|
+
|
|
46
|
+
export interface FirebaseAdapterConfig {
|
|
47
|
+
/** Firebase Functions instance from firebase/functions */
|
|
48
|
+
functions: unknown;
|
|
49
|
+
/** Firebase Firestore instance from firebase/firestore */
|
|
50
|
+
firestore: unknown;
|
|
51
|
+
callables?: {
|
|
52
|
+
createCheckout?: string;
|
|
53
|
+
syncSubscription?: string;
|
|
54
|
+
getBillingHistory?: string;
|
|
55
|
+
cancelSubscription?: string;
|
|
56
|
+
getPortalUrl?: string;
|
|
57
|
+
};
|
|
58
|
+
db?: {
|
|
59
|
+
usersCollection?: string;
|
|
60
|
+
planField?: string;
|
|
61
|
+
billingCycleField?: string;
|
|
62
|
+
subscriptionIdField?: string;
|
|
63
|
+
subscriptionStatusField?: string;
|
|
64
|
+
polarCustomerIdField?: string;
|
|
65
|
+
cancelAtPeriodEndField?: string;
|
|
66
|
+
currentPeriodEndField?: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Firebase Billing Service
|
|
72
|
+
* @description Implementation of PolarAdapter for Firebase Functions and Firestore.
|
|
73
|
+
*/
|
|
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
|
+
|
|
79
|
+
const callables = {
|
|
80
|
+
createCheckout: config.callables?.createCheckout ?? 'createCheckoutSession',
|
|
81
|
+
sync: config.callables?.syncSubscription ?? 'syncSubscription',
|
|
82
|
+
billing: config.callables?.getBillingHistory ?? 'getBillingHistory',
|
|
83
|
+
cancel: config.callables?.cancelSubscription ?? 'cancelSubscription',
|
|
84
|
+
portal: config.callables?.getPortalUrl ?? 'getCustomerPortalUrl',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const db = {
|
|
88
|
+
collection: config.db?.usersCollection ?? 'users',
|
|
89
|
+
plan: config.db?.planField ?? 'plan',
|
|
90
|
+
billingCycle: config.db?.billingCycleField ?? 'billingCycle',
|
|
91
|
+
subscriptionId: config.db?.subscriptionIdField ?? 'subscriptionId',
|
|
92
|
+
subscriptionStatus: config.db?.subscriptionStatusField ?? 'subscriptionStatus',
|
|
93
|
+
polarCustomerId: config.db?.polarCustomerIdField ?? 'polarCustomerId',
|
|
94
|
+
cancelAtPeriodEnd: config.db?.cancelAtPeriodEndField ?? 'cancelAtPeriodEnd',
|
|
95
|
+
currentPeriodEnd: config.db?.currentPeriodEndField ?? 'currentPeriodEnd',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
async function callable<T = unknown, R = unknown>(name: string, data?: T): Promise<R> {
|
|
99
|
+
const { httpsCallable } = await import('firebase/functions');
|
|
100
|
+
const fn = (httpsCallable as any)(functions, name) as (data?: T) => Promise<{ data: R }>;
|
|
101
|
+
const result = await fn(data);
|
|
102
|
+
return result.data;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
async getStatus(userId: string): Promise<SubscriptionStatus> {
|
|
107
|
+
const { doc, getDoc } = await import('firebase/firestore');
|
|
108
|
+
const snap = await getDoc((doc as any)(firestore, db.collection, userId));
|
|
109
|
+
|
|
110
|
+
if (!snap.exists()) {
|
|
111
|
+
return { plan: 'free', subscriptionStatus: 'none' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const d = snap.data() as Record<string, unknown>;
|
|
115
|
+
|
|
116
|
+
let currentPeriodEnd: string | undefined;
|
|
117
|
+
const rawEnd = d[db.currentPeriodEnd];
|
|
118
|
+
if (rawEnd != null) {
|
|
119
|
+
if (isTimestamp(rawEnd)) {
|
|
120
|
+
currentPeriodEnd = rawEnd.toDate().toISOString();
|
|
121
|
+
} else if (typeof rawEnd === 'string') {
|
|
122
|
+
currentPeriodEnd = rawEnd;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
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]),
|
|
133
|
+
currentPeriodEnd,
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async createCheckout(params: CheckoutParams): Promise<CheckoutResult> {
|
|
138
|
+
return callable<CheckoutParams, CheckoutResult>(callables.createCheckout, params);
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
async syncSubscription(_userId: string, _checkoutId?: string): Promise<SyncResult> {
|
|
142
|
+
return callable<Record<string, never>, SyncResult>(callables.sync, {});
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
async getBillingHistory(_userId: string): Promise<OrderItem[]> {
|
|
146
|
+
const result = await callable<Record<string, never>, { orders?: OrderItem[] }>(
|
|
147
|
+
callables.billing,
|
|
148
|
+
{},
|
|
149
|
+
);
|
|
150
|
+
return result.orders ?? [];
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
async cancelSubscription(reason?: CancellationReason): Promise<CancelResult> {
|
|
154
|
+
return callable<{ reason?: string }, CancelResult>(callables.cancel, { reason });
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
async getPortalUrl(_userId: string): Promise<string> {
|
|
158
|
+
const result = await callable<Record<string, never>, { url?: string; customerPortalUrl?: string }>(
|
|
159
|
+
callables.portal,
|
|
160
|
+
{},
|
|
161
|
+
);
|
|
162
|
+
const url = result.url ?? result.customerPortalUrl;
|
|
163
|
+
if (!url) throw new Error('No portal URL returned from Cloud Function');
|
|
164
|
+
return url;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SubscriptionStatusValue, BillingCycle } from '../../domain/entities';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a raw Polar status string to a known value.
|
|
5
|
+
* @description Defaults to 'none' for unknown statuses or non-string input.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeStatus(raw: string): SubscriptionStatusValue {
|
|
8
|
+
const map: Record<string, SubscriptionStatusValue> = {
|
|
9
|
+
active: 'active',
|
|
10
|
+
trialing: 'trialing',
|
|
11
|
+
past_due: 'past_due',
|
|
12
|
+
incomplete: 'incomplete',
|
|
13
|
+
incomplete_expired: 'incomplete_expired',
|
|
14
|
+
unpaid: 'unpaid',
|
|
15
|
+
canceled: 'canceled',
|
|
16
|
+
cancelled: 'canceled',
|
|
17
|
+
revoked: 'revoked',
|
|
18
|
+
none: 'none',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (typeof raw !== 'string') return 'none';
|
|
22
|
+
return map[raw.toLowerCase()] ?? 'none';
|
|
23
|
+
}
|
|
24
|
+
|
|
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
|
+
export function normalizeBillingCycle(interval: string): BillingCycle {
|
|
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';
|
|
34
|
+
return 'monthly';
|
|
35
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useState,
|
|
4
|
+
useCallback,
|
|
5
|
+
useRef,
|
|
6
|
+
useMemo,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import type { PolarAdapter } from '../../domain/interfaces';
|
|
10
|
+
import type {
|
|
11
|
+
SubscriptionStatus,
|
|
12
|
+
CheckoutParams,
|
|
13
|
+
CancellationReason,
|
|
14
|
+
CancelResult,
|
|
15
|
+
SyncResult,
|
|
16
|
+
OrderItem,
|
|
17
|
+
} from '../../domain/entities';
|
|
18
|
+
import { PolarContext } from '../hooks/usePolarBilling';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* PolarProvider Component
|
|
22
|
+
* @description Context provider for Polar billing management.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface PolarProviderProps {
|
|
26
|
+
adapter: PolarAdapter;
|
|
27
|
+
userId?: string;
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const FREE_STATUS: SubscriptionStatus = {
|
|
32
|
+
plan: 'free',
|
|
33
|
+
subscriptionStatus: 'none',
|
|
34
|
+
};
|
|
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
|
+
|
|
42
|
+
export function PolarProvider({ adapter, userId, children }: PolarProviderProps) {
|
|
43
|
+
const [status, setStatus] = useState<SubscriptionStatus>(FREE_STATUS);
|
|
44
|
+
const [loading, setLoading] = useState(true);
|
|
45
|
+
const adapterRef = useRef(adapter);
|
|
46
|
+
adapterRef.current = adapter;
|
|
47
|
+
const refreshAbortRef = useRef<AbortController | null>(null);
|
|
48
|
+
|
|
49
|
+
const refresh = useCallback(async () => {
|
|
50
|
+
const uid = normalizeUserId(userId);
|
|
51
|
+
if (!uid) {
|
|
52
|
+
setStatus(FREE_STATUS);
|
|
53
|
+
setLoading(false);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
refreshAbortRef.current?.abort();
|
|
58
|
+
const ctrl = new AbortController();
|
|
59
|
+
refreshAbortRef.current = ctrl;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
setLoading(true);
|
|
63
|
+
const s = await adapterRef.current.getStatus(uid);
|
|
64
|
+
if (!ctrl.signal.aborted) setStatus(s);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (!ctrl.signal.aborted) {
|
|
67
|
+
console.error('[polar-billing] getStatus failed:', err);
|
|
68
|
+
setStatus(FREE_STATUS);
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
if (!ctrl.signal.aborted) setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
}, [userId]);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
refresh();
|
|
77
|
+
return () => { refreshAbortRef.current?.abort(); };
|
|
78
|
+
}, [refresh]);
|
|
79
|
+
|
|
80
|
+
const startCheckout = useCallback(async (params: CheckoutParams) => {
|
|
81
|
+
const uid = normalizeUserId(userId);
|
|
82
|
+
const result = await adapterRef.current.createCheckout({ ...params, userId: uid });
|
|
83
|
+
if (!result.url.startsWith('https://')) {
|
|
84
|
+
throw new Error('[polar-billing] Invalid checkout URL returned: URL must start with https://');
|
|
85
|
+
}
|
|
86
|
+
window.location.href = result.url;
|
|
87
|
+
}, [userId]);
|
|
88
|
+
|
|
89
|
+
const syncSubscription = useCallback(async (): Promise<SyncResult> => {
|
|
90
|
+
const uid = normalizeUserId(userId);
|
|
91
|
+
if (!uid) return { synced: false };
|
|
92
|
+
|
|
93
|
+
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id') ?? undefined;
|
|
94
|
+
const result = await adapterRef.current.syncSubscription(uid, checkoutId);
|
|
95
|
+
if (result.synced) await refresh();
|
|
96
|
+
return result;
|
|
97
|
+
}, [userId, refresh]);
|
|
98
|
+
|
|
99
|
+
const getBillingHistory = useCallback(async (): Promise<OrderItem[]> => {
|
|
100
|
+
const uid = normalizeUserId(userId);
|
|
101
|
+
if (!uid) return [];
|
|
102
|
+
return adapterRef.current.getBillingHistory(uid);
|
|
103
|
+
}, [userId]);
|
|
104
|
+
|
|
105
|
+
const cancelSubscription = useCallback(
|
|
106
|
+
async (reason?: CancellationReason): Promise<CancelResult> => {
|
|
107
|
+
const result = await adapterRef.current.cancelSubscription(reason);
|
|
108
|
+
if (result.success) await refresh();
|
|
109
|
+
return result;
|
|
110
|
+
},
|
|
111
|
+
[refresh],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const getPortalUrl = useCallback(async (): Promise<string> => {
|
|
115
|
+
const uid = normalizeUserId(userId);
|
|
116
|
+
if (!uid) throw new Error('[polar-billing] Cannot get portal URL: No authenticated user');
|
|
117
|
+
return adapterRef.current.getPortalUrl(uid);
|
|
118
|
+
}, [userId]);
|
|
119
|
+
|
|
120
|
+
const value = useMemo(
|
|
121
|
+
() => ({
|
|
122
|
+
status,
|
|
123
|
+
loading,
|
|
124
|
+
refresh,
|
|
125
|
+
startCheckout,
|
|
126
|
+
syncSubscription,
|
|
127
|
+
getBillingHistory,
|
|
128
|
+
cancelSubscription,
|
|
129
|
+
getPortalUrl,
|
|
130
|
+
}),
|
|
131
|
+
[status, loading, refresh, startCheckout, syncSubscription, getBillingHistory, cancelSubscription, getPortalUrl],
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return <PolarContext.Provider value={value}>{children}</PolarContext.Provider>;
|
|
135
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
SubscriptionStatus,
|
|
4
|
+
CheckoutParams,
|
|
5
|
+
CancellationReason,
|
|
6
|
+
CancelResult,
|
|
7
|
+
SyncResult,
|
|
8
|
+
OrderItem,
|
|
9
|
+
} from '../../domain/entities';
|
|
10
|
+
|
|
11
|
+
export interface PolarContextValue {
|
|
12
|
+
status: SubscriptionStatus;
|
|
13
|
+
loading: boolean;
|
|
14
|
+
refresh: () => Promise<void>;
|
|
15
|
+
startCheckout: (params: CheckoutParams) => Promise<void>;
|
|
16
|
+
syncSubscription: () => Promise<SyncResult>;
|
|
17
|
+
getBillingHistory: () => Promise<OrderItem[]>;
|
|
18
|
+
cancelSubscription: (reason?: CancellationReason) => Promise<CancelResult>;
|
|
19
|
+
getPortalUrl: () => Promise<string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const PolarContext = createContext<PolarContextValue | undefined>(undefined);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* usePolarBilling Hook
|
|
26
|
+
* @description Hook to access Polar billing context
|
|
27
|
+
*/
|
|
28
|
+
export function usePolarBilling(): PolarContextValue {
|
|
29
|
+
const ctx = useContext(PolarContext);
|
|
30
|
+
if (!ctx) throw new Error('usePolarBilling must be used within <PolarProvider>');
|
|
31
|
+
return ctx;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const useSubscription = usePolarBilling;
|