@umituz/web-polar-payment 1.0.14 → 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 -80
- package/dist/index.d.ts +20 -80
- package/dist/index.js +172 -170
- package/dist/index.mjs +177 -170
- 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/domain/interfaces/polar-adapter.interface.ts +0 -5
- package/src/index.ts +9 -11
- package/src/infrastructure/constants/billing.constants.ts +0 -8
- package/src/infrastructure/index.ts +2 -5
- package/src/infrastructure/services/firebase-billing.service.ts +28 -64
- package/src/infrastructure/utils/firebase-helpers.util.ts +18 -0
- package/src/infrastructure/utils/normalization.util.ts +13 -23
- package/src/infrastructure/utils/validations.util.ts +17 -0
- package/src/presentation/components/PolarProvider.tsx +28 -35
- package/src/presentation/hooks/usePolarBilling.ts +0 -6
- package/src/presentation/index.ts +0 -5
|
@@ -9,44 +9,15 @@ 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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;
|
|
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;
|
|
45
18
|
|
|
46
19
|
export interface FirebaseAdapterConfig {
|
|
47
|
-
/** Firebase Functions instance from firebase/functions */
|
|
48
20
|
functions: unknown;
|
|
49
|
-
/** Firebase Firestore instance from firebase/firestore */
|
|
50
21
|
firestore: unknown;
|
|
51
22
|
callables?: {
|
|
52
23
|
createCheckout?: string;
|
|
@@ -67,14 +38,9 @@ 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
|
-
|
|
76
|
-
const
|
|
77
|
-
const firestore = config.firestore as FirebaseFirestore;
|
|
42
|
+
const functions = config.functions;
|
|
43
|
+
const firestore = config.firestore;
|
|
78
44
|
|
|
79
45
|
const callables = {
|
|
80
46
|
createCheckout: config.callables?.createCheckout ?? 'createCheckoutSession',
|
|
@@ -95,30 +61,28 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
95
61
|
currentPeriodEnd: config.db?.currentPeriodEndField ?? 'currentPeriodEnd',
|
|
96
62
|
};
|
|
97
63
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
let httpsCallableCache: typeof import('firebase/functions')['httpsCallable'] | null = null;
|
|
101
|
-
let firestoreCache: { doc: any; getDoc: any } | null = null;
|
|
64
|
+
let httpsCallableCache: HttpsCallable | null = null;
|
|
65
|
+
let firestoreCache: { doc: FirestoreDoc; getDoc: FirestoreGetDoc } | null = null;
|
|
102
66
|
|
|
103
|
-
async function getHttpsCallable() {
|
|
67
|
+
async function getHttpsCallable(): Promise<HttpsCallable> {
|
|
104
68
|
if (!httpsCallableCache) {
|
|
105
69
|
const mod = await import('firebase/functions');
|
|
106
|
-
httpsCallableCache = mod.httpsCallable;
|
|
70
|
+
httpsCallableCache = mod.httpsCallable(functions) as HttpsCallable;
|
|
107
71
|
}
|
|
108
72
|
return httpsCallableCache;
|
|
109
73
|
}
|
|
110
74
|
|
|
111
|
-
async function getFirestore() {
|
|
75
|
+
async function getFirestore(): Promise<{ doc: FirestoreDoc; getDoc: FirestoreGetDoc }> {
|
|
112
76
|
if (!firestoreCache) {
|
|
113
77
|
const mod = await import('firebase/firestore');
|
|
114
|
-
firestoreCache = { doc: mod.doc, getDoc: mod.getDoc };
|
|
78
|
+
firestoreCache = { doc: mod.doc as FirestoreDoc, getDoc: mod.getDoc as FirestoreGetDoc };
|
|
115
79
|
}
|
|
116
80
|
return firestoreCache;
|
|
117
81
|
}
|
|
118
82
|
|
|
119
83
|
async function callable<T = unknown, R = unknown>(name: string, data?: T): Promise<R> {
|
|
120
84
|
const httpsCallable = await getHttpsCallable();
|
|
121
|
-
const fn =
|
|
85
|
+
const fn = httpsCallable(name) as (data?: T) => Promise<{ data: R }>;
|
|
122
86
|
const result = await fn(data);
|
|
123
87
|
return result.data;
|
|
124
88
|
}
|
|
@@ -126,13 +90,15 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
126
90
|
return {
|
|
127
91
|
async getStatus(userId: string): Promise<SubscriptionStatus> {
|
|
128
92
|
const { doc, getDoc } = await getFirestore();
|
|
129
|
-
const
|
|
93
|
+
const docRef = doc(firestore, db.collection, userId);
|
|
94
|
+
const snap = await getDoc(docRef);
|
|
130
95
|
|
|
131
|
-
|
|
96
|
+
const exists = (snap as { exists: boolean }).exists;
|
|
97
|
+
if (!exists) {
|
|
132
98
|
return { plan: 'free', subscriptionStatus: 'none' };
|
|
133
99
|
}
|
|
134
100
|
|
|
135
|
-
const d = snap.data()
|
|
101
|
+
const d = snap.data();
|
|
136
102
|
|
|
137
103
|
let currentPeriodEnd: string | undefined;
|
|
138
104
|
const rawEnd = d[db.currentPeriodEnd];
|
|
@@ -159,14 +125,14 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
159
125
|
return callable<CheckoutParams, CheckoutResult>(callables.createCheckout, params);
|
|
160
126
|
},
|
|
161
127
|
|
|
162
|
-
async syncSubscription(
|
|
163
|
-
return callable<
|
|
128
|
+
async syncSubscription(userId: string, checkoutId?: string): Promise<SyncResult> {
|
|
129
|
+
return callable<{ userId: string; checkoutId?: string }, SyncResult>(callables.sync, { userId, checkoutId });
|
|
164
130
|
},
|
|
165
131
|
|
|
166
|
-
async getBillingHistory(
|
|
167
|
-
const result = await callable<
|
|
132
|
+
async getBillingHistory(userId: string): Promise<OrderItem[]> {
|
|
133
|
+
const result = await callable<{ userId: string }, { orders?: OrderItem[] }>(
|
|
168
134
|
callables.billing,
|
|
169
|
-
{},
|
|
135
|
+
{ userId },
|
|
170
136
|
);
|
|
171
137
|
return result.orders ?? [];
|
|
172
138
|
},
|
|
@@ -175,14 +141,12 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
175
141
|
return callable<{ reason?: string }, CancelResult>(callables.cancel, { reason });
|
|
176
142
|
},
|
|
177
143
|
|
|
178
|
-
async getPortalUrl(
|
|
179
|
-
const result = await callable<
|
|
144
|
+
async getPortalUrl(userId: string): Promise<string> {
|
|
145
|
+
const result = await callable<{ userId: string }, { url: string }>(
|
|
180
146
|
callables.portal,
|
|
181
|
-
{},
|
|
147
|
+
{ userId },
|
|
182
148
|
);
|
|
183
|
-
|
|
184
|
-
if (!url) throw new Error('No portal URL returned from Cloud Function');
|
|
185
|
-
return url;
|
|
149
|
+
return result.url;
|
|
186
150
|
},
|
|
187
151
|
};
|
|
188
152
|
}
|
|
@@ -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
|
+
'function' === typeof (value as { toDate?: unknown }).toDate
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -1,33 +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
|
-
revoked: 'revoked',
|
|
15
|
-
none: 'none',
|
|
16
|
-
};
|
|
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
|
+
});
|
|
17
15
|
|
|
18
|
-
/**
|
|
19
|
-
* Normalize a raw Polar status string to a known value.
|
|
20
|
-
* @description Defaults to 'none' for unknown statuses or non-string input.
|
|
21
|
-
*/
|
|
22
16
|
export function normalizeStatus(raw: string): SubscriptionStatusValue {
|
|
23
|
-
if (typeof raw !== 'string') return 'none';
|
|
17
|
+
if (raw == null || typeof raw !== 'string') return 'none';
|
|
24
18
|
return STATUS_MAP[raw.toLowerCase()] ?? 'none';
|
|
25
19
|
}
|
|
26
20
|
|
|
27
|
-
/**
|
|
28
|
-
* Normalize billing interval
|
|
29
|
-
* @description Maps 'month'/'year' to 'monthly'/'yearly'. Defaults to 'monthly' for unknown values or non-string input.
|
|
30
|
-
*/
|
|
31
21
|
export function normalizeBillingCycle(interval: string): BillingCycle {
|
|
32
22
|
if (typeof interval !== 'string') return 'monthly';
|
|
33
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
|
+
}
|
|
@@ -16,11 +16,7 @@ import type {
|
|
|
16
16
|
OrderItem,
|
|
17
17
|
} from '../../domain/entities';
|
|
18
18
|
import { PolarContext } from '../hooks/usePolarBilling';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* PolarProvider Component
|
|
22
|
-
* @description Context provider for Polar billing management.
|
|
23
|
-
*/
|
|
19
|
+
import { normalizeUserId, isValidProductId, isValidCheckoutUrl, isProductionInsecureUrl } from '../../infrastructure/utils/validations.util';
|
|
24
20
|
|
|
25
21
|
interface PolarProviderProps {
|
|
26
22
|
adapter: PolarAdapter;
|
|
@@ -33,28 +29,19 @@ const FREE_STATUS: SubscriptionStatus = {
|
|
|
33
29
|
subscriptionStatus: 'none',
|
|
34
30
|
};
|
|
35
31
|
|
|
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
32
|
export function PolarProvider({ adapter, userId, children }: PolarProviderProps) {
|
|
43
33
|
const [status, setStatus] = useState<SubscriptionStatus>(FREE_STATUS);
|
|
44
34
|
const [loading, setLoading] = useState(true);
|
|
45
35
|
|
|
46
|
-
// Store adapter and setters in refs to create stable callbacks
|
|
47
36
|
const adapterRef = useRef(adapter);
|
|
48
37
|
const statusRef = useRef({ setStatus, setLoading });
|
|
49
38
|
const userIdRef = useRef(normalizeUserId(userId));
|
|
50
|
-
const
|
|
39
|
+
const refreshMountedRef = useRef(true);
|
|
51
40
|
|
|
52
|
-
// Keep refs in sync
|
|
53
41
|
adapterRef.current = adapter;
|
|
54
42
|
statusRef.current = { setStatus, setLoading };
|
|
55
43
|
userIdRef.current = normalizeUserId(userId);
|
|
56
44
|
|
|
57
|
-
// Stable refresh function with no dependencies - prevents cascading re-renders
|
|
58
45
|
const refresh = useCallback(async () => {
|
|
59
46
|
const uid = userIdRef.current;
|
|
60
47
|
const { setStatus, setLoading } = statusRef.current;
|
|
@@ -65,37 +52,45 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
65
52
|
return;
|
|
66
53
|
}
|
|
67
54
|
|
|
68
|
-
|
|
69
|
-
const ctrl = new AbortController();
|
|
70
|
-
refreshAbortRef.current = ctrl;
|
|
55
|
+
refreshMountedRef.current = true;
|
|
71
56
|
|
|
72
57
|
try {
|
|
73
58
|
setLoading(true);
|
|
74
59
|
const s = await adapterRef.current.getStatus(uid);
|
|
75
|
-
if (
|
|
60
|
+
if (refreshMountedRef.current) setStatus(s);
|
|
76
61
|
} catch (err) {
|
|
77
|
-
if (
|
|
78
|
-
console.error('[polar-billing] getStatus failed:', err);
|
|
62
|
+
if (refreshMountedRef.current) {
|
|
79
63
|
setStatus(FREE_STATUS);
|
|
80
64
|
}
|
|
81
65
|
} finally {
|
|
82
|
-
if (
|
|
66
|
+
if (refreshMountedRef.current) setLoading(false);
|
|
83
67
|
}
|
|
84
|
-
}, []);
|
|
68
|
+
}, []);
|
|
85
69
|
|
|
86
70
|
useEffect(() => {
|
|
71
|
+
refreshMountedRef.current = true;
|
|
87
72
|
refresh();
|
|
88
|
-
return () => {
|
|
89
|
-
}, [
|
|
73
|
+
return () => { refreshMountedRef.current = false; };
|
|
74
|
+
}, [userId]);
|
|
90
75
|
|
|
91
76
|
const startCheckout = useCallback(async (params: CheckoutParams) => {
|
|
77
|
+
if (!isValidProductId(params.productId)) {
|
|
78
|
+
throw new Error('[polar-billing] Invalid productId: must be a non-empty string');
|
|
79
|
+
}
|
|
80
|
+
|
|
92
81
|
const uid = userIdRef.current;
|
|
93
82
|
const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? undefined });
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
|
|
84
|
+
if (!isValidCheckoutUrl(result.url)) {
|
|
85
|
+
throw new Error('[polar-billing] Invalid checkout URL returned: URL must start with https:// or http://');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isProductionInsecureUrl(result.url)) {
|
|
89
|
+
throw new Error('[polar-billing] ERROR: Using insecure http:// URL in production environment');
|
|
96
90
|
}
|
|
91
|
+
|
|
97
92
|
window.location.href = result.url;
|
|
98
|
-
}, []);
|
|
93
|
+
}, []);
|
|
99
94
|
|
|
100
95
|
const syncSubscription = useCallback(async (): Promise<SyncResult> => {
|
|
101
96
|
const uid = userIdRef.current;
|
|
@@ -105,13 +100,13 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
105
100
|
const result = await adapterRef.current.syncSubscription(uid, checkoutId);
|
|
106
101
|
if (result.synced) await refresh();
|
|
107
102
|
return result;
|
|
108
|
-
}, [refresh]);
|
|
103
|
+
}, [refresh]);
|
|
109
104
|
|
|
110
105
|
const getBillingHistory = useCallback(async (): Promise<OrderItem[]> => {
|
|
111
106
|
const uid = userIdRef.current;
|
|
112
107
|
if (!uid) return [];
|
|
113
108
|
return adapterRef.current.getBillingHistory(uid);
|
|
114
|
-
}, []);
|
|
109
|
+
}, []);
|
|
115
110
|
|
|
116
111
|
const cancelSubscription = useCallback(
|
|
117
112
|
async (reason?: CancellationReason): Promise<CancelResult> => {
|
|
@@ -119,17 +114,15 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
119
114
|
if (result.success) await refresh();
|
|
120
115
|
return result;
|
|
121
116
|
},
|
|
122
|
-
[refresh],
|
|
117
|
+
[refresh],
|
|
123
118
|
);
|
|
124
119
|
|
|
125
120
|
const getPortalUrl = useCallback(async (): Promise<string> => {
|
|
126
121
|
const uid = userIdRef.current;
|
|
127
122
|
if (!uid) throw new Error('[polar-billing] Cannot get portal URL: No authenticated user');
|
|
128
123
|
return adapterRef.current.getPortalUrl(uid);
|
|
129
|
-
}, []);
|
|
124
|
+
}, []);
|
|
130
125
|
|
|
131
|
-
// Memoized context value - only recreates when status/loading changes
|
|
132
|
-
// All functions are stable, so they don't trigger re-creation
|
|
133
126
|
const value = useMemo(
|
|
134
127
|
() => ({
|
|
135
128
|
status,
|
|
@@ -141,7 +134,7 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
141
134
|
cancelSubscription,
|
|
142
135
|
getPortalUrl,
|
|
143
136
|
}),
|
|
144
|
-
[status, loading, refresh, syncSubscription],
|
|
137
|
+
[status, loading, refresh, syncSubscription],
|
|
145
138
|
);
|
|
146
139
|
|
|
147
140
|
return <PolarContext.Provider value={value}>{children}</PolarContext.Provider>;
|
|
@@ -21,14 +21,8 @@ export interface PolarContextValue {
|
|
|
21
21
|
|
|
22
22
|
export const PolarContext = createContext<PolarContextValue | undefined>(undefined);
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
* usePolarBilling Hook
|
|
26
|
-
* @description Hook to access Polar billing context
|
|
27
|
-
*/
|
|
28
24
|
export function usePolarBilling(): PolarContextValue {
|
|
29
25
|
const ctx = useContext(PolarContext);
|
|
30
26
|
if (!ctx) throw new Error('usePolarBilling must be used within <PolarProvider>');
|
|
31
27
|
return ctx;
|
|
32
28
|
}
|
|
33
|
-
|
|
34
|
-
export const useSubscription = usePolarBilling;
|