@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
|
@@ -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,74 +29,85 @@ 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);
|
|
35
|
+
|
|
45
36
|
const adapterRef = useRef(adapter);
|
|
37
|
+
const statusRef = useRef({ setStatus, setLoading });
|
|
38
|
+
const userIdRef = useRef(normalizeUserId(userId));
|
|
39
|
+
const refreshMountedRef = useRef(true);
|
|
40
|
+
|
|
46
41
|
adapterRef.current = adapter;
|
|
47
|
-
|
|
42
|
+
statusRef.current = { setStatus, setLoading };
|
|
43
|
+
userIdRef.current = normalizeUserId(userId);
|
|
48
44
|
|
|
49
45
|
const refresh = useCallback(async () => {
|
|
50
|
-
const uid =
|
|
46
|
+
const uid = userIdRef.current;
|
|
47
|
+
const { setStatus, setLoading } = statusRef.current;
|
|
48
|
+
|
|
51
49
|
if (!uid) {
|
|
52
50
|
setStatus(FREE_STATUS);
|
|
53
51
|
setLoading(false);
|
|
54
52
|
return;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
const ctrl = new AbortController();
|
|
59
|
-
refreshAbortRef.current = ctrl;
|
|
55
|
+
refreshMountedRef.current = true;
|
|
60
56
|
|
|
61
57
|
try {
|
|
62
58
|
setLoading(true);
|
|
63
59
|
const s = await adapterRef.current.getStatus(uid);
|
|
64
|
-
if (
|
|
60
|
+
if (refreshMountedRef.current) setStatus(s);
|
|
65
61
|
} catch (err) {
|
|
66
|
-
if (
|
|
62
|
+
if (refreshMountedRef.current) {
|
|
67
63
|
console.error('[polar-billing] getStatus failed:', err);
|
|
68
64
|
setStatus(FREE_STATUS);
|
|
69
65
|
}
|
|
70
66
|
} finally {
|
|
71
|
-
if (
|
|
67
|
+
if (refreshMountedRef.current) setLoading(false);
|
|
72
68
|
}
|
|
73
|
-
}, [
|
|
69
|
+
}, []);
|
|
74
70
|
|
|
75
71
|
useEffect(() => {
|
|
72
|
+
refreshMountedRef.current = true;
|
|
76
73
|
refresh();
|
|
77
|
-
return () => {
|
|
78
|
-
}, [
|
|
74
|
+
return () => { refreshMountedRef.current = false; };
|
|
75
|
+
}, [userId]);
|
|
79
76
|
|
|
80
77
|
const startCheckout = useCallback(async (params: CheckoutParams) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!result.url.startsWith('https://')) {
|
|
84
|
-
throw new Error('[polar-billing] Invalid checkout URL returned: URL must start with https://');
|
|
78
|
+
if (!isValidProductId(params.productId)) {
|
|
79
|
+
throw new Error('[polar-billing] Invalid productId: must be a non-empty string');
|
|
85
80
|
}
|
|
81
|
+
|
|
82
|
+
const uid = userIdRef.current;
|
|
83
|
+
const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? undefined });
|
|
84
|
+
|
|
85
|
+
if (!isValidCheckoutUrl(result.url)) {
|
|
86
|
+
throw new Error('[polar-billing] Invalid checkout URL returned: URL must start with https:// or http://');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (isProductionInsecureUrl(result.url)) {
|
|
90
|
+
console.warn('[polar-billing] WARNING: Using insecure http:// URL in production environment');
|
|
91
|
+
}
|
|
92
|
+
|
|
86
93
|
window.location.href = result.url;
|
|
87
|
-
}, [
|
|
94
|
+
}, []);
|
|
88
95
|
|
|
89
96
|
const syncSubscription = useCallback(async (): Promise<SyncResult> => {
|
|
90
|
-
const uid =
|
|
97
|
+
const uid = userIdRef.current;
|
|
91
98
|
if (!uid) return { synced: false };
|
|
92
99
|
|
|
93
100
|
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id') ?? undefined;
|
|
94
101
|
const result = await adapterRef.current.syncSubscription(uid, checkoutId);
|
|
95
102
|
if (result.synced) await refresh();
|
|
96
103
|
return result;
|
|
97
|
-
}, [
|
|
104
|
+
}, [refresh]);
|
|
98
105
|
|
|
99
106
|
const getBillingHistory = useCallback(async (): Promise<OrderItem[]> => {
|
|
100
|
-
const uid =
|
|
107
|
+
const uid = userIdRef.current;
|
|
101
108
|
if (!uid) return [];
|
|
102
109
|
return adapterRef.current.getBillingHistory(uid);
|
|
103
|
-
}, [
|
|
110
|
+
}, []);
|
|
104
111
|
|
|
105
112
|
const cancelSubscription = useCallback(
|
|
106
113
|
async (reason?: CancellationReason): Promise<CancelResult> => {
|
|
@@ -112,10 +119,10 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
112
119
|
);
|
|
113
120
|
|
|
114
121
|
const getPortalUrl = useCallback(async (): Promise<string> => {
|
|
115
|
-
const uid =
|
|
122
|
+
const uid = userIdRef.current;
|
|
116
123
|
if (!uid) throw new Error('[polar-billing] Cannot get portal URL: No authenticated user');
|
|
117
124
|
return adapterRef.current.getPortalUrl(uid);
|
|
118
|
-
}, [
|
|
125
|
+
}, []);
|
|
119
126
|
|
|
120
127
|
const value = useMemo(
|
|
121
128
|
() => ({
|
|
@@ -128,7 +135,7 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
128
135
|
cancelSubscription,
|
|
129
136
|
getPortalUrl,
|
|
130
137
|
}),
|
|
131
|
-
[status, loading, refresh,
|
|
138
|
+
[status, loading, refresh, syncSubscription],
|
|
132
139
|
);
|
|
133
140
|
|
|
134
141
|
return <PolarContext.Provider value={value}>{children}</PolarContext.Provider>;
|
|
@@ -21,10 +21,6 @@ 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>');
|