@umituz/web-polar-payment 1.0.14 → 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 +12 -47
- package/dist/index.d.ts +12 -47
- package/dist/index.js +75 -48
- package/dist/index.mjs +68 -48
- 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 +0 -6
- package/src/infrastructure/index.ts +2 -5
- package/src/infrastructure/services/firebase-billing.service.ts +3 -41
- 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 -34
- package/src/presentation/hooks/usePolarBilling.ts +0 -4
- package/src/presentation/index.ts +0 -5
package/dist/index.d.mts
CHANGED
|
@@ -2,10 +2,6 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
import { ReactNode } from 'react';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Subscription Entity
|
|
7
|
-
* @description Types for subscription status and billing cycles
|
|
8
|
-
*/
|
|
9
5
|
type SubscriptionStatusValue = 'active' | 'canceled' | 'revoked' | 'trialing' | 'past_due' | 'incomplete' | 'incomplete_expired' | 'unpaid' | 'none';
|
|
10
6
|
type BillingCycle = 'monthly' | 'yearly';
|
|
11
7
|
interface SubscriptionStatus {
|
|
@@ -16,14 +12,9 @@ interface SubscriptionStatus {
|
|
|
16
12
|
currentPeriodEnd?: string;
|
|
17
13
|
billingCycle?: BillingCycle;
|
|
18
14
|
polarCustomerId?: string;
|
|
19
|
-
/** Token balance (for token-based projects like Aria) */
|
|
20
15
|
tokens?: number;
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
/**
|
|
24
|
-
* Order Entity
|
|
25
|
-
* @description Types for billing history items
|
|
26
|
-
*/
|
|
27
18
|
interface OrderItem {
|
|
28
19
|
id: string;
|
|
29
20
|
createdAt: string;
|
|
@@ -35,16 +26,11 @@ interface OrderItem {
|
|
|
35
26
|
invoiceUrl?: string;
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
/**
|
|
39
|
-
* Checkout Entity
|
|
40
|
-
* @description Types for initiating and following process of checkouts
|
|
41
|
-
*/
|
|
42
29
|
interface CheckoutParams {
|
|
43
30
|
productId: string;
|
|
44
31
|
planKey?: string;
|
|
45
32
|
billingCycle?: 'monthly' | 'yearly';
|
|
46
33
|
successUrl?: string;
|
|
47
|
-
/** Injected automatically by PolarProvider — do not pass manually */
|
|
48
34
|
userId?: string;
|
|
49
35
|
}
|
|
50
36
|
interface CheckoutResult {
|
|
@@ -52,20 +38,12 @@ interface CheckoutResult {
|
|
|
52
38
|
id: string;
|
|
53
39
|
}
|
|
54
40
|
|
|
55
|
-
/**
|
|
56
|
-
* Cancellation Entity
|
|
57
|
-
* @description Types for subscription cancellation reasons and outcomes
|
|
58
|
-
*/
|
|
59
41
|
type CancellationReason = 'too_expensive' | 'missing_features' | 'switched_service' | 'unused' | 'customer_service' | 'low_quality' | 'too_complex' | 'other';
|
|
60
42
|
interface CancelResult {
|
|
61
43
|
success: boolean;
|
|
62
44
|
endsAt?: string;
|
|
63
45
|
}
|
|
64
46
|
|
|
65
|
-
/**
|
|
66
|
-
* Sync Entity
|
|
67
|
-
* @description Type for subscription synchronization results
|
|
68
|
-
*/
|
|
69
47
|
interface SyncResult {
|
|
70
48
|
synced: boolean;
|
|
71
49
|
plan?: string;
|
|
@@ -108,16 +86,8 @@ interface FirebaseAdapterConfig {
|
|
|
108
86
|
currentPeriodEndField?: string;
|
|
109
87
|
};
|
|
110
88
|
}
|
|
111
|
-
/**
|
|
112
|
-
* Firebase Billing Service
|
|
113
|
-
* @description Implementation of PolarAdapter for Firebase Functions and Firestore.
|
|
114
|
-
*/
|
|
115
89
|
declare function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapter;
|
|
116
90
|
|
|
117
|
-
/**
|
|
118
|
-
* Billing Constants
|
|
119
|
-
* @description Standardized subscription states and plan names
|
|
120
|
-
*/
|
|
121
91
|
declare const SUBSCRIPTION_STATUS: Readonly<{
|
|
122
92
|
ACTIVE: "active";
|
|
123
93
|
CANCELED: "canceled";
|
|
@@ -131,21 +101,20 @@ declare const SUBSCRIPTION_STATUS: Readonly<{
|
|
|
131
101
|
}>;
|
|
132
102
|
declare const FREE_PLAN = "free";
|
|
133
103
|
|
|
134
|
-
/**
|
|
135
|
-
* Normalize a raw Polar status string to a known value.
|
|
136
|
-
* @description Defaults to 'none' for unknown statuses or non-string input.
|
|
137
|
-
*/
|
|
138
104
|
declare function normalizeStatus(raw: string): SubscriptionStatusValue;
|
|
139
|
-
/**
|
|
140
|
-
* Normalize billing interval
|
|
141
|
-
* @description Maps 'month'/'year' to 'monthly'/'yearly'. Defaults to 'monthly' for unknown values or non-string input.
|
|
142
|
-
*/
|
|
143
105
|
declare function normalizeBillingCycle(interval: string): BillingCycle;
|
|
144
106
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
107
|
+
declare function normalizeUserId(userId: string | undefined): string | undefined;
|
|
108
|
+
declare function isValidProductId(productId: unknown): productId is string;
|
|
109
|
+
declare function isValidCheckoutUrl(url: string): boolean;
|
|
110
|
+
declare function isProductionInsecureUrl(url: string): boolean;
|
|
111
|
+
|
|
112
|
+
declare function asString(value: unknown): string | undefined;
|
|
113
|
+
declare function asBoolean(value: unknown): boolean | undefined;
|
|
114
|
+
declare function isTimestamp(value: unknown): value is {
|
|
115
|
+
toDate(): Date;
|
|
116
|
+
};
|
|
117
|
+
|
|
149
118
|
interface PolarProviderProps {
|
|
150
119
|
adapter: PolarAdapter;
|
|
151
120
|
userId?: string;
|
|
@@ -164,11 +133,7 @@ interface PolarContextValue {
|
|
|
164
133
|
getPortalUrl: () => Promise<string>;
|
|
165
134
|
}
|
|
166
135
|
declare const PolarContext: react.Context<PolarContextValue | undefined>;
|
|
167
|
-
/**
|
|
168
|
-
* usePolarBilling Hook
|
|
169
|
-
* @description Hook to access Polar billing context
|
|
170
|
-
*/
|
|
171
136
|
declare function usePolarBilling(): PolarContextValue;
|
|
172
137
|
declare const useSubscription: typeof usePolarBilling;
|
|
173
138
|
|
|
174
|
-
export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult, FREE_PLAN, type FirebaseAdapterConfig, type OrderItem, type PolarAdapter, PolarContext, type PolarContextValue, PolarProvider, SUBSCRIPTION_STATUS, type SubscriptionStatus, type SubscriptionStatusValue, type SyncResult, createFirebaseAdapter, normalizeBillingCycle, normalizeStatus, usePolarBilling, useSubscription };
|
|
139
|
+
export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult, FREE_PLAN, type FirebaseAdapterConfig, type OrderItem, type PolarAdapter, PolarContext, type PolarContextValue, PolarProvider, SUBSCRIPTION_STATUS, type SubscriptionStatus, type SubscriptionStatusValue, type SyncResult, asBoolean, asString, createFirebaseAdapter, isProductionInsecureUrl, isTimestamp, isValidCheckoutUrl, isValidProductId, normalizeBillingCycle, normalizeStatus, normalizeUserId, usePolarBilling, useSubscription };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,6 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
import { ReactNode } from 'react';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Subscription Entity
|
|
7
|
-
* @description Types for subscription status and billing cycles
|
|
8
|
-
*/
|
|
9
5
|
type SubscriptionStatusValue = 'active' | 'canceled' | 'revoked' | 'trialing' | 'past_due' | 'incomplete' | 'incomplete_expired' | 'unpaid' | 'none';
|
|
10
6
|
type BillingCycle = 'monthly' | 'yearly';
|
|
11
7
|
interface SubscriptionStatus {
|
|
@@ -16,14 +12,9 @@ interface SubscriptionStatus {
|
|
|
16
12
|
currentPeriodEnd?: string;
|
|
17
13
|
billingCycle?: BillingCycle;
|
|
18
14
|
polarCustomerId?: string;
|
|
19
|
-
/** Token balance (for token-based projects like Aria) */
|
|
20
15
|
tokens?: number;
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
/**
|
|
24
|
-
* Order Entity
|
|
25
|
-
* @description Types for billing history items
|
|
26
|
-
*/
|
|
27
18
|
interface OrderItem {
|
|
28
19
|
id: string;
|
|
29
20
|
createdAt: string;
|
|
@@ -35,16 +26,11 @@ interface OrderItem {
|
|
|
35
26
|
invoiceUrl?: string;
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
/**
|
|
39
|
-
* Checkout Entity
|
|
40
|
-
* @description Types for initiating and following process of checkouts
|
|
41
|
-
*/
|
|
42
29
|
interface CheckoutParams {
|
|
43
30
|
productId: string;
|
|
44
31
|
planKey?: string;
|
|
45
32
|
billingCycle?: 'monthly' | 'yearly';
|
|
46
33
|
successUrl?: string;
|
|
47
|
-
/** Injected automatically by PolarProvider — do not pass manually */
|
|
48
34
|
userId?: string;
|
|
49
35
|
}
|
|
50
36
|
interface CheckoutResult {
|
|
@@ -52,20 +38,12 @@ interface CheckoutResult {
|
|
|
52
38
|
id: string;
|
|
53
39
|
}
|
|
54
40
|
|
|
55
|
-
/**
|
|
56
|
-
* Cancellation Entity
|
|
57
|
-
* @description Types for subscription cancellation reasons and outcomes
|
|
58
|
-
*/
|
|
59
41
|
type CancellationReason = 'too_expensive' | 'missing_features' | 'switched_service' | 'unused' | 'customer_service' | 'low_quality' | 'too_complex' | 'other';
|
|
60
42
|
interface CancelResult {
|
|
61
43
|
success: boolean;
|
|
62
44
|
endsAt?: string;
|
|
63
45
|
}
|
|
64
46
|
|
|
65
|
-
/**
|
|
66
|
-
* Sync Entity
|
|
67
|
-
* @description Type for subscription synchronization results
|
|
68
|
-
*/
|
|
69
47
|
interface SyncResult {
|
|
70
48
|
synced: boolean;
|
|
71
49
|
plan?: string;
|
|
@@ -108,16 +86,8 @@ interface FirebaseAdapterConfig {
|
|
|
108
86
|
currentPeriodEndField?: string;
|
|
109
87
|
};
|
|
110
88
|
}
|
|
111
|
-
/**
|
|
112
|
-
* Firebase Billing Service
|
|
113
|
-
* @description Implementation of PolarAdapter for Firebase Functions and Firestore.
|
|
114
|
-
*/
|
|
115
89
|
declare function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapter;
|
|
116
90
|
|
|
117
|
-
/**
|
|
118
|
-
* Billing Constants
|
|
119
|
-
* @description Standardized subscription states and plan names
|
|
120
|
-
*/
|
|
121
91
|
declare const SUBSCRIPTION_STATUS: Readonly<{
|
|
122
92
|
ACTIVE: "active";
|
|
123
93
|
CANCELED: "canceled";
|
|
@@ -131,21 +101,20 @@ declare const SUBSCRIPTION_STATUS: Readonly<{
|
|
|
131
101
|
}>;
|
|
132
102
|
declare const FREE_PLAN = "free";
|
|
133
103
|
|
|
134
|
-
/**
|
|
135
|
-
* Normalize a raw Polar status string to a known value.
|
|
136
|
-
* @description Defaults to 'none' for unknown statuses or non-string input.
|
|
137
|
-
*/
|
|
138
104
|
declare function normalizeStatus(raw: string): SubscriptionStatusValue;
|
|
139
|
-
/**
|
|
140
|
-
* Normalize billing interval
|
|
141
|
-
* @description Maps 'month'/'year' to 'monthly'/'yearly'. Defaults to 'monthly' for unknown values or non-string input.
|
|
142
|
-
*/
|
|
143
105
|
declare function normalizeBillingCycle(interval: string): BillingCycle;
|
|
144
106
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
107
|
+
declare function normalizeUserId(userId: string | undefined): string | undefined;
|
|
108
|
+
declare function isValidProductId(productId: unknown): productId is string;
|
|
109
|
+
declare function isValidCheckoutUrl(url: string): boolean;
|
|
110
|
+
declare function isProductionInsecureUrl(url: string): boolean;
|
|
111
|
+
|
|
112
|
+
declare function asString(value: unknown): string | undefined;
|
|
113
|
+
declare function asBoolean(value: unknown): boolean | undefined;
|
|
114
|
+
declare function isTimestamp(value: unknown): value is {
|
|
115
|
+
toDate(): Date;
|
|
116
|
+
};
|
|
117
|
+
|
|
149
118
|
interface PolarProviderProps {
|
|
150
119
|
adapter: PolarAdapter;
|
|
151
120
|
userId?: string;
|
|
@@ -164,11 +133,7 @@ interface PolarContextValue {
|
|
|
164
133
|
getPortalUrl: () => Promise<string>;
|
|
165
134
|
}
|
|
166
135
|
declare const PolarContext: react.Context<PolarContextValue | undefined>;
|
|
167
|
-
/**
|
|
168
|
-
* usePolarBilling Hook
|
|
169
|
-
* @description Hook to access Polar billing context
|
|
170
|
-
*/
|
|
171
136
|
declare function usePolarBilling(): PolarContextValue;
|
|
172
137
|
declare const useSubscription: typeof usePolarBilling;
|
|
173
138
|
|
|
174
|
-
export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult, FREE_PLAN, type FirebaseAdapterConfig, type OrderItem, type PolarAdapter, PolarContext, type PolarContextValue, PolarProvider, SUBSCRIPTION_STATUS, type SubscriptionStatus, type SubscriptionStatusValue, type SyncResult, createFirebaseAdapter, normalizeBillingCycle, normalizeStatus, usePolarBilling, useSubscription };
|
|
139
|
+
export { type BillingCycle, type CancelResult, type CancellationReason, type CheckoutParams, type CheckoutResult, FREE_PLAN, type FirebaseAdapterConfig, type OrderItem, type PolarAdapter, PolarContext, type PolarContextValue, PolarProvider, SUBSCRIPTION_STATUS, type SubscriptionStatus, type SubscriptionStatusValue, type SyncResult, asBoolean, asString, createFirebaseAdapter, isProductionInsecureUrl, isTimestamp, isValidCheckoutUrl, isValidProductId, normalizeBillingCycle, normalizeStatus, normalizeUserId, usePolarBilling, useSubscription };
|
package/dist/index.js
CHANGED
|
@@ -34,29 +34,49 @@ __export(index_exports, {
|
|
|
34
34
|
PolarContext: () => PolarContext,
|
|
35
35
|
PolarProvider: () => PolarProvider,
|
|
36
36
|
SUBSCRIPTION_STATUS: () => SUBSCRIPTION_STATUS,
|
|
37
|
+
asBoolean: () => asBoolean,
|
|
38
|
+
asString: () => asString,
|
|
37
39
|
createFirebaseAdapter: () => createFirebaseAdapter,
|
|
40
|
+
isProductionInsecureUrl: () => isProductionInsecureUrl,
|
|
41
|
+
isTimestamp: () => isTimestamp,
|
|
42
|
+
isValidCheckoutUrl: () => isValidCheckoutUrl,
|
|
43
|
+
isValidProductId: () => isValidProductId,
|
|
38
44
|
normalizeBillingCycle: () => normalizeBillingCycle,
|
|
39
45
|
normalizeStatus: () => normalizeStatus,
|
|
46
|
+
normalizeUserId: () => normalizeUserId,
|
|
40
47
|
usePolarBilling: () => usePolarBilling,
|
|
41
48
|
useSubscription: () => useSubscription
|
|
42
49
|
});
|
|
43
50
|
module.exports = __toCommonJS(index_exports);
|
|
44
51
|
|
|
52
|
+
// src/infrastructure/constants/billing.constants.ts
|
|
53
|
+
var SUBSCRIPTION_STATUS = Object.freeze({
|
|
54
|
+
ACTIVE: "active",
|
|
55
|
+
CANCELED: "canceled",
|
|
56
|
+
REVOKED: "revoked",
|
|
57
|
+
TRIALING: "trialing",
|
|
58
|
+
PAST_DUE: "past_due",
|
|
59
|
+
INCOMPLETE: "incomplete",
|
|
60
|
+
INCOMPLETE_EXPIRED: "incomplete_expired",
|
|
61
|
+
UNPAID: "unpaid",
|
|
62
|
+
NONE: "none"
|
|
63
|
+
});
|
|
64
|
+
var FREE_PLAN = "free";
|
|
65
|
+
|
|
45
66
|
// src/infrastructure/utils/normalization.util.ts
|
|
46
|
-
var STATUS_MAP = {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
67
|
+
var STATUS_MAP = Object.freeze({
|
|
68
|
+
[SUBSCRIPTION_STATUS.ACTIVE]: SUBSCRIPTION_STATUS.ACTIVE,
|
|
69
|
+
[SUBSCRIPTION_STATUS.CANCELED]: SUBSCRIPTION_STATUS.CANCELED,
|
|
70
|
+
[SUBSCRIPTION_STATUS.REVOKED]: SUBSCRIPTION_STATUS.REVOKED,
|
|
71
|
+
[SUBSCRIPTION_STATUS.TRIALING]: SUBSCRIPTION_STATUS.TRIALING,
|
|
72
|
+
[SUBSCRIPTION_STATUS.PAST_DUE]: SUBSCRIPTION_STATUS.PAST_DUE,
|
|
73
|
+
[SUBSCRIPTION_STATUS.INCOMPLETE]: SUBSCRIPTION_STATUS.INCOMPLETE,
|
|
74
|
+
[SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED]: SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED,
|
|
75
|
+
[SUBSCRIPTION_STATUS.UNPAID]: SUBSCRIPTION_STATUS.UNPAID,
|
|
76
|
+
[SUBSCRIPTION_STATUS.NONE]: SUBSCRIPTION_STATUS.NONE
|
|
77
|
+
});
|
|
58
78
|
function normalizeStatus(raw) {
|
|
59
|
-
if (typeof raw !== "string") return "none";
|
|
79
|
+
if (raw == null || typeof raw !== "string") return "none";
|
|
60
80
|
return STATUS_MAP[raw.toLowerCase()] ?? "none";
|
|
61
81
|
}
|
|
62
82
|
function normalizeBillingCycle(interval) {
|
|
@@ -67,7 +87,7 @@ function normalizeBillingCycle(interval) {
|
|
|
67
87
|
return "monthly";
|
|
68
88
|
}
|
|
69
89
|
|
|
70
|
-
// src/infrastructure/
|
|
90
|
+
// src/infrastructure/utils/firebase-helpers.util.ts
|
|
71
91
|
function asString(value) {
|
|
72
92
|
if (typeof value === "string") return value;
|
|
73
93
|
return void 0;
|
|
@@ -79,6 +99,8 @@ function asBoolean(value) {
|
|
|
79
99
|
function isTimestamp(value) {
|
|
80
100
|
return typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate === "function";
|
|
81
101
|
}
|
|
102
|
+
|
|
103
|
+
// src/infrastructure/services/firebase-billing.service.ts
|
|
82
104
|
function createFirebaseAdapter(config) {
|
|
83
105
|
const functions = config.functions;
|
|
84
106
|
const firestore = config.firestore;
|
|
@@ -169,26 +191,26 @@ function createFirebaseAdapter(config) {
|
|
|
169
191
|
callables.portal,
|
|
170
192
|
{}
|
|
171
193
|
);
|
|
172
|
-
|
|
173
|
-
if (!url) throw new Error("No portal URL returned from Cloud Function");
|
|
174
|
-
return url;
|
|
194
|
+
return result.url;
|
|
175
195
|
}
|
|
176
196
|
};
|
|
177
197
|
}
|
|
178
198
|
|
|
179
|
-
// src/infrastructure/
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
199
|
+
// src/infrastructure/utils/validations.util.ts
|
|
200
|
+
function normalizeUserId(userId) {
|
|
201
|
+
if (typeof userId !== "string") return void 0;
|
|
202
|
+
const trimmed = userId.trim();
|
|
203
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
204
|
+
}
|
|
205
|
+
function isValidProductId(productId) {
|
|
206
|
+
return typeof productId === "string" && productId.trim().length > 0;
|
|
207
|
+
}
|
|
208
|
+
function isValidCheckoutUrl(url) {
|
|
209
|
+
return url.startsWith("https://") || url.startsWith("http://");
|
|
210
|
+
}
|
|
211
|
+
function isProductionInsecureUrl(url) {
|
|
212
|
+
return url.startsWith("http://") && !url.includes("localhost") && !url.includes("127.0.0.1");
|
|
213
|
+
}
|
|
192
214
|
|
|
193
215
|
// src/presentation/components/PolarProvider.tsx
|
|
194
216
|
var import_react2 = require("react");
|
|
@@ -209,18 +231,13 @@ var FREE_STATUS = {
|
|
|
209
231
|
plan: "free",
|
|
210
232
|
subscriptionStatus: "none"
|
|
211
233
|
};
|
|
212
|
-
function normalizeUserId(userId) {
|
|
213
|
-
if (typeof userId !== "string") return void 0;
|
|
214
|
-
const trimmed = userId.trim();
|
|
215
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
216
|
-
}
|
|
217
234
|
function PolarProvider({ adapter, userId, children }) {
|
|
218
235
|
const [status, setStatus] = (0, import_react2.useState)(FREE_STATUS);
|
|
219
236
|
const [loading, setLoading] = (0, import_react2.useState)(true);
|
|
220
237
|
const adapterRef = (0, import_react2.useRef)(adapter);
|
|
221
238
|
const statusRef = (0, import_react2.useRef)({ setStatus, setLoading });
|
|
222
239
|
const userIdRef = (0, import_react2.useRef)(normalizeUserId(userId));
|
|
223
|
-
const
|
|
240
|
+
const refreshMountedRef = (0, import_react2.useRef)(true);
|
|
224
241
|
adapterRef.current = adapter;
|
|
225
242
|
statusRef.current = { setStatus, setLoading };
|
|
226
243
|
userIdRef.current = normalizeUserId(userId);
|
|
@@ -232,33 +249,38 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
232
249
|
setLoading2(false);
|
|
233
250
|
return;
|
|
234
251
|
}
|
|
235
|
-
|
|
236
|
-
const ctrl = new AbortController();
|
|
237
|
-
refreshAbortRef.current = ctrl;
|
|
252
|
+
refreshMountedRef.current = true;
|
|
238
253
|
try {
|
|
239
254
|
setLoading2(true);
|
|
240
255
|
const s = await adapterRef.current.getStatus(uid);
|
|
241
|
-
if (
|
|
256
|
+
if (refreshMountedRef.current) setStatus2(s);
|
|
242
257
|
} catch (err) {
|
|
243
|
-
if (
|
|
258
|
+
if (refreshMountedRef.current) {
|
|
244
259
|
console.error("[polar-billing] getStatus failed:", err);
|
|
245
260
|
setStatus2(FREE_STATUS);
|
|
246
261
|
}
|
|
247
262
|
} finally {
|
|
248
|
-
if (
|
|
263
|
+
if (refreshMountedRef.current) setLoading2(false);
|
|
249
264
|
}
|
|
250
265
|
}, []);
|
|
251
266
|
(0, import_react2.useEffect)(() => {
|
|
267
|
+
refreshMountedRef.current = true;
|
|
252
268
|
refresh();
|
|
253
269
|
return () => {
|
|
254
|
-
|
|
270
|
+
refreshMountedRef.current = false;
|
|
255
271
|
};
|
|
256
|
-
}, [
|
|
272
|
+
}, [userId]);
|
|
257
273
|
const startCheckout = (0, import_react2.useCallback)(async (params) => {
|
|
274
|
+
if (!isValidProductId(params.productId)) {
|
|
275
|
+
throw new Error("[polar-billing] Invalid productId: must be a non-empty string");
|
|
276
|
+
}
|
|
258
277
|
const uid = userIdRef.current;
|
|
259
278
|
const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? void 0 });
|
|
260
|
-
if (!result.url
|
|
261
|
-
throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https://");
|
|
279
|
+
if (!isValidCheckoutUrl(result.url)) {
|
|
280
|
+
throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https:// or http://");
|
|
281
|
+
}
|
|
282
|
+
if (isProductionInsecureUrl(result.url)) {
|
|
283
|
+
console.warn("[polar-billing] WARNING: Using insecure http:// URL in production environment");
|
|
262
284
|
}
|
|
263
285
|
window.location.href = result.url;
|
|
264
286
|
}, []);
|
|
@@ -282,7 +304,6 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
282
304
|
return result;
|
|
283
305
|
},
|
|
284
306
|
[refresh]
|
|
285
|
-
// Only depends on stable refresh
|
|
286
307
|
);
|
|
287
308
|
const getPortalUrl = (0, import_react2.useCallback)(async () => {
|
|
288
309
|
const uid = userIdRef.current;
|
|
@@ -301,7 +322,6 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
301
322
|
getPortalUrl
|
|
302
323
|
}),
|
|
303
324
|
[status, loading, refresh, syncSubscription]
|
|
304
|
-
// startCheckout, getBillingHistory, cancelSubscription, getPortalUrl are stable
|
|
305
325
|
);
|
|
306
326
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PolarContext.Provider, { value, children });
|
|
307
327
|
}
|
|
@@ -311,9 +331,16 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
311
331
|
PolarContext,
|
|
312
332
|
PolarProvider,
|
|
313
333
|
SUBSCRIPTION_STATUS,
|
|
334
|
+
asBoolean,
|
|
335
|
+
asString,
|
|
314
336
|
createFirebaseAdapter,
|
|
337
|
+
isProductionInsecureUrl,
|
|
338
|
+
isTimestamp,
|
|
339
|
+
isValidCheckoutUrl,
|
|
340
|
+
isValidProductId,
|
|
315
341
|
normalizeBillingCycle,
|
|
316
342
|
normalizeStatus,
|
|
343
|
+
normalizeUserId,
|
|
317
344
|
usePolarBilling,
|
|
318
345
|
useSubscription
|
|
319
346
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
|
+
// src/infrastructure/constants/billing.constants.ts
|
|
2
|
+
var SUBSCRIPTION_STATUS = Object.freeze({
|
|
3
|
+
ACTIVE: "active",
|
|
4
|
+
CANCELED: "canceled",
|
|
5
|
+
REVOKED: "revoked",
|
|
6
|
+
TRIALING: "trialing",
|
|
7
|
+
PAST_DUE: "past_due",
|
|
8
|
+
INCOMPLETE: "incomplete",
|
|
9
|
+
INCOMPLETE_EXPIRED: "incomplete_expired",
|
|
10
|
+
UNPAID: "unpaid",
|
|
11
|
+
NONE: "none"
|
|
12
|
+
});
|
|
13
|
+
var FREE_PLAN = "free";
|
|
14
|
+
|
|
1
15
|
// src/infrastructure/utils/normalization.util.ts
|
|
2
|
-
var STATUS_MAP = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
16
|
+
var STATUS_MAP = Object.freeze({
|
|
17
|
+
[SUBSCRIPTION_STATUS.ACTIVE]: SUBSCRIPTION_STATUS.ACTIVE,
|
|
18
|
+
[SUBSCRIPTION_STATUS.CANCELED]: SUBSCRIPTION_STATUS.CANCELED,
|
|
19
|
+
[SUBSCRIPTION_STATUS.REVOKED]: SUBSCRIPTION_STATUS.REVOKED,
|
|
20
|
+
[SUBSCRIPTION_STATUS.TRIALING]: SUBSCRIPTION_STATUS.TRIALING,
|
|
21
|
+
[SUBSCRIPTION_STATUS.PAST_DUE]: SUBSCRIPTION_STATUS.PAST_DUE,
|
|
22
|
+
[SUBSCRIPTION_STATUS.INCOMPLETE]: SUBSCRIPTION_STATUS.INCOMPLETE,
|
|
23
|
+
[SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED]: SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED,
|
|
24
|
+
[SUBSCRIPTION_STATUS.UNPAID]: SUBSCRIPTION_STATUS.UNPAID,
|
|
25
|
+
[SUBSCRIPTION_STATUS.NONE]: SUBSCRIPTION_STATUS.NONE
|
|
26
|
+
});
|
|
14
27
|
function normalizeStatus(raw) {
|
|
15
|
-
if (typeof raw !== "string") return "none";
|
|
28
|
+
if (raw == null || typeof raw !== "string") return "none";
|
|
16
29
|
return STATUS_MAP[raw.toLowerCase()] ?? "none";
|
|
17
30
|
}
|
|
18
31
|
function normalizeBillingCycle(interval) {
|
|
@@ -23,7 +36,7 @@ function normalizeBillingCycle(interval) {
|
|
|
23
36
|
return "monthly";
|
|
24
37
|
}
|
|
25
38
|
|
|
26
|
-
// src/infrastructure/
|
|
39
|
+
// src/infrastructure/utils/firebase-helpers.util.ts
|
|
27
40
|
function asString(value) {
|
|
28
41
|
if (typeof value === "string") return value;
|
|
29
42
|
return void 0;
|
|
@@ -35,6 +48,8 @@ function asBoolean(value) {
|
|
|
35
48
|
function isTimestamp(value) {
|
|
36
49
|
return typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate === "function";
|
|
37
50
|
}
|
|
51
|
+
|
|
52
|
+
// src/infrastructure/services/firebase-billing.service.ts
|
|
38
53
|
function createFirebaseAdapter(config) {
|
|
39
54
|
const functions = config.functions;
|
|
40
55
|
const firestore = config.firestore;
|
|
@@ -125,26 +140,26 @@ function createFirebaseAdapter(config) {
|
|
|
125
140
|
callables.portal,
|
|
126
141
|
{}
|
|
127
142
|
);
|
|
128
|
-
|
|
129
|
-
if (!url) throw new Error("No portal URL returned from Cloud Function");
|
|
130
|
-
return url;
|
|
143
|
+
return result.url;
|
|
131
144
|
}
|
|
132
145
|
};
|
|
133
146
|
}
|
|
134
147
|
|
|
135
|
-
// src/infrastructure/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
+
// src/infrastructure/utils/validations.util.ts
|
|
149
|
+
function normalizeUserId(userId) {
|
|
150
|
+
if (typeof userId !== "string") return void 0;
|
|
151
|
+
const trimmed = userId.trim();
|
|
152
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
153
|
+
}
|
|
154
|
+
function isValidProductId(productId) {
|
|
155
|
+
return typeof productId === "string" && productId.trim().length > 0;
|
|
156
|
+
}
|
|
157
|
+
function isValidCheckoutUrl(url) {
|
|
158
|
+
return url.startsWith("https://") || url.startsWith("http://");
|
|
159
|
+
}
|
|
160
|
+
function isProductionInsecureUrl(url) {
|
|
161
|
+
return url.startsWith("http://") && !url.includes("localhost") && !url.includes("127.0.0.1");
|
|
162
|
+
}
|
|
148
163
|
|
|
149
164
|
// src/presentation/components/PolarProvider.tsx
|
|
150
165
|
import {
|
|
@@ -171,18 +186,13 @@ var FREE_STATUS = {
|
|
|
171
186
|
plan: "free",
|
|
172
187
|
subscriptionStatus: "none"
|
|
173
188
|
};
|
|
174
|
-
function normalizeUserId(userId) {
|
|
175
|
-
if (typeof userId !== "string") return void 0;
|
|
176
|
-
const trimmed = userId.trim();
|
|
177
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
178
|
-
}
|
|
179
189
|
function PolarProvider({ adapter, userId, children }) {
|
|
180
190
|
const [status, setStatus] = useState(FREE_STATUS);
|
|
181
191
|
const [loading, setLoading] = useState(true);
|
|
182
192
|
const adapterRef = useRef(adapter);
|
|
183
193
|
const statusRef = useRef({ setStatus, setLoading });
|
|
184
194
|
const userIdRef = useRef(normalizeUserId(userId));
|
|
185
|
-
const
|
|
195
|
+
const refreshMountedRef = useRef(true);
|
|
186
196
|
adapterRef.current = adapter;
|
|
187
197
|
statusRef.current = { setStatus, setLoading };
|
|
188
198
|
userIdRef.current = normalizeUserId(userId);
|
|
@@ -194,33 +204,38 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
194
204
|
setLoading2(false);
|
|
195
205
|
return;
|
|
196
206
|
}
|
|
197
|
-
|
|
198
|
-
const ctrl = new AbortController();
|
|
199
|
-
refreshAbortRef.current = ctrl;
|
|
207
|
+
refreshMountedRef.current = true;
|
|
200
208
|
try {
|
|
201
209
|
setLoading2(true);
|
|
202
210
|
const s = await adapterRef.current.getStatus(uid);
|
|
203
|
-
if (
|
|
211
|
+
if (refreshMountedRef.current) setStatus2(s);
|
|
204
212
|
} catch (err) {
|
|
205
|
-
if (
|
|
213
|
+
if (refreshMountedRef.current) {
|
|
206
214
|
console.error("[polar-billing] getStatus failed:", err);
|
|
207
215
|
setStatus2(FREE_STATUS);
|
|
208
216
|
}
|
|
209
217
|
} finally {
|
|
210
|
-
if (
|
|
218
|
+
if (refreshMountedRef.current) setLoading2(false);
|
|
211
219
|
}
|
|
212
220
|
}, []);
|
|
213
221
|
useEffect(() => {
|
|
222
|
+
refreshMountedRef.current = true;
|
|
214
223
|
refresh();
|
|
215
224
|
return () => {
|
|
216
|
-
|
|
225
|
+
refreshMountedRef.current = false;
|
|
217
226
|
};
|
|
218
|
-
}, [
|
|
227
|
+
}, [userId]);
|
|
219
228
|
const startCheckout = useCallback(async (params) => {
|
|
229
|
+
if (!isValidProductId(params.productId)) {
|
|
230
|
+
throw new Error("[polar-billing] Invalid productId: must be a non-empty string");
|
|
231
|
+
}
|
|
220
232
|
const uid = userIdRef.current;
|
|
221
233
|
const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? void 0 });
|
|
222
|
-
if (!result.url
|
|
223
|
-
throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https://");
|
|
234
|
+
if (!isValidCheckoutUrl(result.url)) {
|
|
235
|
+
throw new Error("[polar-billing] Invalid checkout URL returned: URL must start with https:// or http://");
|
|
236
|
+
}
|
|
237
|
+
if (isProductionInsecureUrl(result.url)) {
|
|
238
|
+
console.warn("[polar-billing] WARNING: Using insecure http:// URL in production environment");
|
|
224
239
|
}
|
|
225
240
|
window.location.href = result.url;
|
|
226
241
|
}, []);
|
|
@@ -244,7 +259,6 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
244
259
|
return result;
|
|
245
260
|
},
|
|
246
261
|
[refresh]
|
|
247
|
-
// Only depends on stable refresh
|
|
248
262
|
);
|
|
249
263
|
const getPortalUrl = useCallback(async () => {
|
|
250
264
|
const uid = userIdRef.current;
|
|
@@ -263,7 +277,6 @@ function PolarProvider({ adapter, userId, children }) {
|
|
|
263
277
|
getPortalUrl
|
|
264
278
|
}),
|
|
265
279
|
[status, loading, refresh, syncSubscription]
|
|
266
|
-
// startCheckout, getBillingHistory, cancelSubscription, getPortalUrl are stable
|
|
267
280
|
);
|
|
268
281
|
return /* @__PURE__ */ jsx(PolarContext.Provider, { value, children });
|
|
269
282
|
}
|
|
@@ -272,9 +285,16 @@ export {
|
|
|
272
285
|
PolarContext,
|
|
273
286
|
PolarProvider,
|
|
274
287
|
SUBSCRIPTION_STATUS,
|
|
288
|
+
asBoolean,
|
|
289
|
+
asString,
|
|
275
290
|
createFirebaseAdapter,
|
|
291
|
+
isProductionInsecureUrl,
|
|
292
|
+
isTimestamp,
|
|
293
|
+
isValidCheckoutUrl,
|
|
294
|
+
isValidProductId,
|
|
276
295
|
normalizeBillingCycle,
|
|
277
296
|
normalizeStatus,
|
|
297
|
+
normalizeUserId,
|
|
278
298
|
usePolarBilling,
|
|
279
299
|
useSubscription
|
|
280
300
|
};
|
package/package.json
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checkout Entity
|
|
3
|
-
* @description Types for initiating and following process of checkouts
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
export interface CheckoutParams {
|
|
7
2
|
productId: string;
|
|
8
3
|
planKey?: string;
|
|
9
4
|
billingCycle?: 'monthly' | 'yearly';
|
|
10
5
|
successUrl?: string;
|
|
11
|
-
/** Injected automatically by PolarProvider — do not pass manually */
|
|
12
6
|
userId?: string;
|
|
13
7
|
}
|
|
14
8
|
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Entity
|
|
3
|
-
* @description Types for subscription status and billing cycles
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
export type SubscriptionStatusValue =
|
|
7
2
|
| 'active'
|
|
8
3
|
| 'canceled'
|
|
@@ -24,6 +19,5 @@ export interface SubscriptionStatus {
|
|
|
24
19
|
currentPeriodEnd?: string;
|
|
25
20
|
billingCycle?: BillingCycle;
|
|
26
21
|
polarCustomerId?: string;
|
|
27
|
-
/** Token balance (for token-based projects like Aria) */
|
|
28
22
|
tokens?: number;
|
|
29
23
|
}
|
package/src/domain/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @umituz/web-polar-payment
|
|
3
|
-
* Universal Polar.sh subscription billing — Firebase adapter
|
|
4
|
-
*
|
|
5
|
-
* IMPORTANT: Apps should NOT use this root barrel export.
|
|
6
|
-
* Use subpath imports instead: "@umituz/web-polar-payment/domain"
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
export * from './domain';
|
|
10
2
|
export * from './infrastructure';
|
|
11
3
|
export * from './presentation';
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Billing Constants
|
|
3
|
-
* @description Standardized subscription states and plan names
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Object.freeze() prevents accidental mutations and enables V8 optimizations
|
|
7
1
|
export const SUBSCRIPTION_STATUS = Object.freeze({
|
|
8
2
|
ACTIVE: 'active' as const,
|
|
9
3
|
CANCELED: 'canceled' as const,
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Infrastructure Layer
|
|
3
|
-
* Subpath: @umituz/web-polar-payment/infrastructure
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
export * from './services/firebase-billing.service';
|
|
7
2
|
export * from './constants/billing.constants';
|
|
8
3
|
export * from './utils/normalization.util';
|
|
4
|
+
export * from './utils/validations.util';
|
|
5
|
+
export * from './utils/firebase-helpers.util';
|
|
@@ -9,37 +9,8 @@ import type {
|
|
|
9
9
|
SyncResult,
|
|
10
10
|
} from '../../domain/entities';
|
|
11
11
|
import { normalizeStatus, normalizeBillingCycle } from '../utils/normalization.util';
|
|
12
|
+
import { asString, asBoolean, isTimestamp } from '../utils/firebase-helpers.util';
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
* Type guard to safely extract string value from unknown Firestore data
|
|
15
|
-
*/
|
|
16
|
-
function asString(value: unknown): string | undefined {
|
|
17
|
-
if (typeof value === 'string') return value;
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Type guard to safely extract boolean value from unknown Firestore data
|
|
23
|
-
*/
|
|
24
|
-
function asBoolean(value: unknown): boolean | undefined {
|
|
25
|
-
if (typeof value === 'boolean') return value;
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Type guard to safely check if value is an object with toDate method
|
|
31
|
-
*/
|
|
32
|
-
function isTimestamp(value: unknown): value is { toDate(): Date } {
|
|
33
|
-
return (
|
|
34
|
-
typeof value === 'object' &&
|
|
35
|
-
value !== null &&
|
|
36
|
-
'toDate' in value &&
|
|
37
|
-
typeof (value as { toDate: unknown }).toDate === 'function'
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Internal type aliases for Firebase SDK compatibility
|
|
42
|
-
// Using 'any' internally to avoid DTS build issues with external Firebase packages
|
|
43
14
|
type FirebaseFunctions = any;
|
|
44
15
|
type FirebaseFirestore = any;
|
|
45
16
|
|
|
@@ -67,12 +38,7 @@ export interface FirebaseAdapterConfig {
|
|
|
67
38
|
};
|
|
68
39
|
}
|
|
69
40
|
|
|
70
|
-
/**
|
|
71
|
-
* Firebase Billing Service
|
|
72
|
-
* @description Implementation of PolarAdapter for Firebase Functions and Firestore.
|
|
73
|
-
*/
|
|
74
41
|
export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapter {
|
|
75
|
-
// Cast internally to Firebase types for implementation
|
|
76
42
|
const functions = config.functions as FirebaseFunctions;
|
|
77
43
|
const firestore = config.firestore as FirebaseFirestore;
|
|
78
44
|
|
|
@@ -95,8 +61,6 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
95
61
|
currentPeriodEnd: config.db?.currentPeriodEndField ?? 'currentPeriodEnd',
|
|
96
62
|
};
|
|
97
63
|
|
|
98
|
-
// Cache imports to avoid repeated dynamic import overhead
|
|
99
|
-
// Reduces latency on subsequent calls and GC pressure
|
|
100
64
|
let httpsCallableCache: typeof import('firebase/functions')['httpsCallable'] | null = null;
|
|
101
65
|
let firestoreCache: { doc: any; getDoc: any } | null = null;
|
|
102
66
|
|
|
@@ -176,13 +140,11 @@ export function createFirebaseAdapter(config: FirebaseAdapterConfig): PolarAdapt
|
|
|
176
140
|
},
|
|
177
141
|
|
|
178
142
|
async getPortalUrl(_userId: string): Promise<string> {
|
|
179
|
-
const result = await callable<Record<string, never>, { url
|
|
143
|
+
const result = await callable<Record<string, never>, { url: string }>(
|
|
180
144
|
callables.portal,
|
|
181
145
|
{},
|
|
182
146
|
);
|
|
183
|
-
|
|
184
|
-
if (!url) throw new Error('No portal URL returned from Cloud Function');
|
|
185
|
-
return url;
|
|
147
|
+
return result.url;
|
|
186
148
|
},
|
|
187
149
|
};
|
|
188
150
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function asString(value: unknown): string | undefined {
|
|
2
|
+
if (typeof value === 'string') return value;
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function asBoolean(value: unknown): boolean | undefined {
|
|
7
|
+
if (typeof value === 'boolean') return value;
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isTimestamp(value: unknown): value is { toDate(): Date } {
|
|
12
|
+
return (
|
|
13
|
+
typeof value === 'object' &&
|
|
14
|
+
value !== null &&
|
|
15
|
+
'toDate' in value &&
|
|
16
|
+
typeof (value as { toDate: unknown }).toDate === 'function'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -1,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,46 @@ 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 (
|
|
62
|
+
if (refreshMountedRef.current) {
|
|
78
63
|
console.error('[polar-billing] getStatus failed:', err);
|
|
79
64
|
setStatus(FREE_STATUS);
|
|
80
65
|
}
|
|
81
66
|
} finally {
|
|
82
|
-
if (
|
|
67
|
+
if (refreshMountedRef.current) setLoading(false);
|
|
83
68
|
}
|
|
84
|
-
}, []);
|
|
69
|
+
}, []);
|
|
85
70
|
|
|
86
71
|
useEffect(() => {
|
|
72
|
+
refreshMountedRef.current = true;
|
|
87
73
|
refresh();
|
|
88
|
-
return () => {
|
|
89
|
-
}, [
|
|
74
|
+
return () => { refreshMountedRef.current = false; };
|
|
75
|
+
}, [userId]);
|
|
90
76
|
|
|
91
77
|
const startCheckout = useCallback(async (params: CheckoutParams) => {
|
|
78
|
+
if (!isValidProductId(params.productId)) {
|
|
79
|
+
throw new Error('[polar-billing] Invalid productId: must be a non-empty string');
|
|
80
|
+
}
|
|
81
|
+
|
|
92
82
|
const uid = userIdRef.current;
|
|
93
83
|
const result = await adapterRef.current.createCheckout({ ...params, userId: uid ?? undefined });
|
|
94
|
-
|
|
95
|
-
|
|
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');
|
|
96
91
|
}
|
|
92
|
+
|
|
97
93
|
window.location.href = result.url;
|
|
98
|
-
}, []);
|
|
94
|
+
}, []);
|
|
99
95
|
|
|
100
96
|
const syncSubscription = useCallback(async (): Promise<SyncResult> => {
|
|
101
97
|
const uid = userIdRef.current;
|
|
@@ -105,13 +101,13 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
105
101
|
const result = await adapterRef.current.syncSubscription(uid, checkoutId);
|
|
106
102
|
if (result.synced) await refresh();
|
|
107
103
|
return result;
|
|
108
|
-
}, [refresh]);
|
|
104
|
+
}, [refresh]);
|
|
109
105
|
|
|
110
106
|
const getBillingHistory = useCallback(async (): Promise<OrderItem[]> => {
|
|
111
107
|
const uid = userIdRef.current;
|
|
112
108
|
if (!uid) return [];
|
|
113
109
|
return adapterRef.current.getBillingHistory(uid);
|
|
114
|
-
}, []);
|
|
110
|
+
}, []);
|
|
115
111
|
|
|
116
112
|
const cancelSubscription = useCallback(
|
|
117
113
|
async (reason?: CancellationReason): Promise<CancelResult> => {
|
|
@@ -119,17 +115,15 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
119
115
|
if (result.success) await refresh();
|
|
120
116
|
return result;
|
|
121
117
|
},
|
|
122
|
-
[refresh],
|
|
118
|
+
[refresh],
|
|
123
119
|
);
|
|
124
120
|
|
|
125
121
|
const getPortalUrl = useCallback(async (): Promise<string> => {
|
|
126
122
|
const uid = userIdRef.current;
|
|
127
123
|
if (!uid) throw new Error('[polar-billing] Cannot get portal URL: No authenticated user');
|
|
128
124
|
return adapterRef.current.getPortalUrl(uid);
|
|
129
|
-
}, []);
|
|
125
|
+
}, []);
|
|
130
126
|
|
|
131
|
-
// Memoized context value - only recreates when status/loading changes
|
|
132
|
-
// All functions are stable, so they don't trigger re-creation
|
|
133
127
|
const value = useMemo(
|
|
134
128
|
() => ({
|
|
135
129
|
status,
|
|
@@ -141,7 +135,7 @@ export function PolarProvider({ adapter, userId, children }: PolarProviderProps)
|
|
|
141
135
|
cancelSubscription,
|
|
142
136
|
getPortalUrl,
|
|
143
137
|
}),
|
|
144
|
-
[status, loading, refresh, syncSubscription],
|
|
138
|
+
[status, loading, refresh, syncSubscription],
|
|
145
139
|
);
|
|
146
140
|
|
|
147
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>');
|