@umituz/web-payment 0.0.0-stage → 1.0.1
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/LICENSE +21 -0
- package/README.md +349 -2
- package/dist/billing-adapter.interface-CDkeeowZ.d.mts +94 -0
- package/dist/billing-adapter.interface-m7yE50I0.d.ts +94 -0
- package/dist/billing-cycle.enum--hJRYlgP.d.mts +10 -0
- package/dist/billing-cycle.enum--hJRYlgP.d.ts +10 -0
- package/dist/billing-env-D4YjvA3M.d.mts +8 -0
- package/dist/billing-env-D4YjvA3M.d.ts +8 -0
- package/dist/checkout-metadata-D7Wp_DAD.d.mts +27 -0
- package/dist/checkout-metadata-D7Wp_DAD.d.ts +27 -0
- package/dist/chunk-2KV7JFYF.mjs +61 -0
- package/dist/chunk-2NH5DOLT.mjs +101 -0
- package/dist/chunk-3AXPZOCV.mjs +106 -0
- package/dist/chunk-4N3BASZ6.mjs +51 -0
- package/dist/chunk-NHKK3ENU.mjs +1128 -0
- package/dist/chunk-PIWNUAKW.mjs +30 -0
- package/dist/chunk-QFB7AOSQ.mjs +33 -0
- package/dist/chunk-RQTMBR74.mjs +30 -0
- package/dist/chunk-U45FS4FF.mjs +110 -0
- package/dist/chunk-X6R76FZT.mjs +0 -0
- package/dist/chunk-XQ2HJTTA.mjs +252 -0
- package/dist/core.d.mts +17 -0
- package/dist/core.d.ts +17 -0
- package/dist/core.js +425 -0
- package/dist/core.mjs +66 -0
- package/dist/http.d.mts +12 -0
- package/dist/http.d.ts +12 -0
- package/dist/http.js +115 -0
- package/dist/http.mjs +88 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +1929 -0
- package/dist/index.mjs +153 -0
- package/dist/memory.d.mts +14 -0
- package/dist/memory.d.ts +14 -0
- package/dist/memory.js +130 -0
- package/dist/memory.mjs +6 -0
- package/dist/portal.d.mts +445 -0
- package/dist/portal.d.ts +445 -0
- package/dist/portal.js +2927 -0
- package/dist/portal.mjs +2737 -0
- package/dist/product.mapper-CnY1ISOp.d.ts +97 -0
- package/dist/product.mapper-noVl-sel.d.mts +97 -0
- package/dist/products.d.mts +64 -0
- package/dist/products.d.ts +64 -0
- package/dist/products.js +370 -0
- package/dist/products.mjs +21 -0
- package/dist/react.d.mts +182 -0
- package/dist/react.d.ts +182 -0
- package/dist/react.js +1874 -0
- package/dist/react.mjs +105 -0
- package/dist/server.d.mts +177 -0
- package/dist/server.d.ts +177 -0
- package/dist/server.js +622 -0
- package/dist/server.mjs +417 -0
- package/dist/visual-tokens-BCLVR85G.d.mts +94 -0
- package/dist/visual-tokens-BCLVR85G.d.ts +94 -0
- package/package.json +122 -4
- package/src/core.ts +81 -0
- package/src/domain/entities/cancellation.entity.ts +28 -0
- package/src/domain/entities/checkout.entity.ts +21 -0
- package/src/domain/entities/index.ts +10 -0
- package/src/domain/entities/order.entity.ts +15 -0
- package/src/domain/entities/product.entity.ts +16 -0
- package/src/domain/entities/subscription.entity.ts +24 -0
- package/src/domain/entities/sync.entity.ts +4 -0
- package/src/domain/enums/amount-type.enum.ts +10 -0
- package/src/domain/enums/billing-cycle.enum.ts +30 -0
- package/src/domain/enums/index.ts +5 -0
- package/src/domain/enums/payment-provider.enum.ts +14 -0
- package/src/domain/enums/subscription-status.enum.ts +32 -0
- package/src/domain/index.ts +5 -0
- package/src/domain/interfaces/billing-adapter.interface.ts +18 -0
- package/src/domain/interfaces/index.ts +1 -0
- package/src/domain/mappers/product.mapper.ts +176 -0
- package/src/domain/utils/minor-units.ts +20 -0
- package/src/domain/utils/percent-discount.ts +17 -0
- package/src/http.ts +3 -0
- package/src/index.ts +12 -0
- package/src/infrastructure/browser/hosted-checkout.ts +26 -0
- package/src/infrastructure/config/billing-env.ts +31 -0
- package/src/infrastructure/constants/billing.constants.ts +10 -0
- package/src/infrastructure/http/http-client.ts +83 -0
- package/src/infrastructure/pagination/page-iterator.ts +43 -0
- package/src/infrastructure/query/query-builder.ts +26 -0
- package/src/infrastructure/services/billing.client.ts +300 -0
- package/src/infrastructure/services/create-billing-client.ts +23 -0
- package/src/infrastructure/services/http/create-http-adapter.ts +123 -0
- package/src/infrastructure/services/memory/create-memory-adapter.ts +126 -0
- package/src/infrastructure/services/product.repository.ts +198 -0
- package/src/infrastructure/utils/checkout-metadata.ts +55 -0
- package/src/infrastructure/utils/date-renderer.ts +53 -0
- package/src/infrastructure/utils/money-formatter.ts +39 -0
- package/src/infrastructure/utils/normalization.util.ts +22 -0
- package/src/infrastructure/utils/pagination-clamp.ts +11 -0
- package/src/infrastructure/utils/receipt.util.ts +6 -0
- package/src/infrastructure/utils/security.util.ts +23 -0
- package/src/infrastructure/utils/type-guards.util.ts +23 -0
- package/src/infrastructure/utils/validation.util.ts +10 -0
- package/src/infrastructure/webhook/parse-event.ts +135 -0
- package/src/infrastructure/webhook/verify-webhook.ts +124 -0
- package/src/memory.ts +3 -0
- package/src/portal.ts +2 -0
- package/src/presentation/components/BillingHistoryList.tsx +172 -0
- package/src/presentation/components/BillingProvider.tsx +179 -0
- package/src/presentation/components/CancelSubscriptionButton.tsx +155 -0
- package/src/presentation/components/CheckoutButton.tsx +76 -0
- package/src/presentation/components/CustomerPortalLink.tsx +71 -0
- package/src/presentation/components/PricingTable.tsx +225 -0
- package/src/presentation/components/SubscriptionStatusBadge.tsx +102 -0
- package/src/presentation/context/billing-context.ts +30 -0
- package/src/presentation/hooks/useBilling.ts +3 -0
- package/src/presentation/hooks/useCancelSubscription.ts +47 -0
- package/src/presentation/hooks/useCheckoutButton.ts +49 -0
- package/src/presentation/hooks/useProducts.ts +76 -0
- package/src/presentation/navigation/navigation.strategy.ts +16 -0
- package/src/presentation/portal/TurnkeyBillingPage.tsx +144 -0
- package/src/presentation/portal/components/BillingLayout.tsx +163 -0
- package/src/presentation/portal/components/BillingPage.tsx +22 -0
- package/src/presentation/portal/components/BillingPortal.tsx +248 -0
- package/src/presentation/portal/components/InvoiceCard.tsx +275 -0
- package/src/presentation/portal/components/OverviewTab.tsx +137 -0
- package/src/presentation/portal/components/PaymentMethodsList.tsx +268 -0
- package/src/presentation/portal/components/PlanComparison.tsx +405 -0
- package/src/presentation/portal/components/PlanTab.tsx +197 -0
- package/src/presentation/portal/components/PortalButton.tsx +89 -0
- package/src/presentation/portal/components/PortalSpinner.tsx +30 -0
- package/src/presentation/portal/components/UsageCard.tsx +158 -0
- package/src/presentation/portal/components/index.ts +11 -0
- package/src/presentation/portal/constants.ts +44 -0
- package/src/presentation/portal/create-portal-client.ts +202 -0
- package/src/presentation/portal/hooks/index.ts +3 -0
- package/src/presentation/portal/hooks/useBilling.ts +240 -0
- package/src/presentation/portal/index.ts +97 -0
- package/src/presentation/portal/labels.tsx +218 -0
- package/src/presentation/portal/styles.ts +70 -0
- package/src/presentation/portal/types.ts +213 -0
- package/src/presentation/portal/utils/billing.ts +193 -0
- package/src/presentation/portal/utils/index.ts +27 -0
- package/src/presentation/portal/utils/plans.ts +71 -0
- package/src/presentation/theme/visual-tokens.ts +103 -0
- package/src/presentation/utils/cancellation-reason-catalog.ts +18 -0
- package/src/presentation/utils/checkout-button-visual.ts +50 -0
- package/src/presentation/utils/inline-error-visual.ts +21 -0
- package/src/presentation/utils/product-pricing.ts +33 -0
- package/src/presentation/utils/status-label.ts +23 -0
- package/src/presentation/utils/status-tone.ts +25 -0
- package/src/products.ts +20 -0
- package/src/react.tsx +63 -0
- package/src/server.ts +79 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
import type { BillingAdapter } from '../../domain/interfaces';
|
|
3
|
+
import type {
|
|
4
|
+
BillingCycle,
|
|
5
|
+
OrderItem,
|
|
6
|
+
SubscriptionStatus as CoreSubscriptionStatus,
|
|
7
|
+
} from '../../domain/entities';
|
|
8
|
+
import type { Product } from '../../domain/entities';
|
|
9
|
+
import { WindowNavigationStrategy } from '../navigation/navigation.strategy';
|
|
10
|
+
import type { NavigationStrategy } from '../navigation/navigation.strategy';
|
|
11
|
+
import { transformProducts } from './utils';
|
|
12
|
+
import type { BillingApiClient } from './hooks';
|
|
13
|
+
import type {
|
|
14
|
+
BillingSummary,
|
|
15
|
+
Invoice,
|
|
16
|
+
PaymentMethod,
|
|
17
|
+
PlanTier,
|
|
18
|
+
Subscription,
|
|
19
|
+
} from './types';
|
|
20
|
+
import { CURRENCY, INVOICE_STATUS, SUBSCRIPTION_STATUS } from './constants';
|
|
21
|
+
|
|
22
|
+
export interface PortalClientOptions {
|
|
23
|
+
adapter: BillingAdapter;
|
|
24
|
+
products: Product[];
|
|
25
|
+
userId?: string;
|
|
26
|
+
defaultCycle?: BillingCycle;
|
|
27
|
+
successUrl?: string;
|
|
28
|
+
navigation?: NavigationStrategy;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PortalClient extends BillingApiClient {
|
|
32
|
+
selectPlan: (productId: string, cycle?: BillingCycle) => Promise<void>;
|
|
33
|
+
openPortal: () => Promise<string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function portalStatus(core: CoreSubscriptionStatus['subscriptionStatus']): Subscription['status'] {
|
|
37
|
+
switch (core) {
|
|
38
|
+
case 'active':
|
|
39
|
+
return SUBSCRIPTION_STATUS.ACTIVE;
|
|
40
|
+
case 'trialing':
|
|
41
|
+
return SUBSCRIPTION_STATUS.TRIALING;
|
|
42
|
+
case 'past_due':
|
|
43
|
+
return SUBSCRIPTION_STATUS.PAST_DUE;
|
|
44
|
+
case 'unpaid':
|
|
45
|
+
return SUBSCRIPTION_STATUS.UNPAID;
|
|
46
|
+
case 'incomplete':
|
|
47
|
+
case 'incomplete_expired':
|
|
48
|
+
return SUBSCRIPTION_STATUS.INCOMPLETE;
|
|
49
|
+
case 'revoked':
|
|
50
|
+
return SUBSCRIPTION_STATUS.REVOKED;
|
|
51
|
+
default:
|
|
52
|
+
return SUBSCRIPTION_STATUS.CANCELED;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function freeTier(planKey: string): PlanTier {
|
|
57
|
+
return {
|
|
58
|
+
id: planKey,
|
|
59
|
+
name: planKey === 'free' ? 'Free' : planKey,
|
|
60
|
+
description: '',
|
|
61
|
+
monthlyPrice: 0,
|
|
62
|
+
yearlyPrice: 0,
|
|
63
|
+
currency: CURRENCY.USD,
|
|
64
|
+
features: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function orderToInvoice(order: OrderItem): Invoice {
|
|
69
|
+
const paid = order.paid || order.status === 'paid';
|
|
70
|
+
const date = order.createdAt.length >= 10 ? order.createdAt.slice(0, 10) : order.createdAt;
|
|
71
|
+
return {
|
|
72
|
+
id: order.id,
|
|
73
|
+
number: order.id.slice(-8).toUpperCase(),
|
|
74
|
+
amount: order.amount,
|
|
75
|
+
currency: order.currency.toUpperCase(),
|
|
76
|
+
status: paid ? INVOICE_STATUS.PAID : INVOICE_STATUS.OPEN,
|
|
77
|
+
date,
|
|
78
|
+
dueDate: date,
|
|
79
|
+
...(paid ? { paidAt: date } : {}),
|
|
80
|
+
...(order.invoiceUrl ? { invoiceUrl: order.invoiceUrl } : {}),
|
|
81
|
+
items: [
|
|
82
|
+
{
|
|
83
|
+
description: order.productName ?? order.itemType ?? 'Subscription',
|
|
84
|
+
quantity: 1,
|
|
85
|
+
unitPrice: order.amount,
|
|
86
|
+
amount: order.amount,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const HOSTED_PORTAL_METHOD: PaymentMethod = {
|
|
93
|
+
id: 'hosted-portal',
|
|
94
|
+
type: 'card',
|
|
95
|
+
isDefault: true,
|
|
96
|
+
createdAt: new Date(0).toISOString(),
|
|
97
|
+
card: { last4: '••••', brand: 'Managed in the billing portal', expiryMonth: 0, expiryYear: 0 },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export function createPortalClient(options: PortalClientOptions): PortalClient {
|
|
101
|
+
const { adapter, products, userId, successUrl } = options;
|
|
102
|
+
const defaultCycle: BillingCycle = options.defaultCycle ?? 'monthly';
|
|
103
|
+
const navigation: NavigationStrategy = options.navigation ?? new WindowNavigationStrategy();
|
|
104
|
+
const tiers = transformProducts(products);
|
|
105
|
+
|
|
106
|
+
const productByPlanKey = new Map<string, Product>();
|
|
107
|
+
for (const product of products) {
|
|
108
|
+
const key = product.metadata?.planId;
|
|
109
|
+
if (typeof key === 'string' && key.length > 0) productByPlanKey.set(key, product);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function checkout(productId: string, cycle?: BillingCycle): Promise<void> {
|
|
113
|
+
const { url } = await adapter.createCheckout({
|
|
114
|
+
productId,
|
|
115
|
+
billingCycle: cycle ?? defaultCycle,
|
|
116
|
+
...(successUrl ? { successUrl } : {}),
|
|
117
|
+
...(userId ? { userId } : {}),
|
|
118
|
+
});
|
|
119
|
+
navigation.redirect(url);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function openPortal(): Promise<string> {
|
|
123
|
+
const url = await adapter.getPortalUrl(userId ?? '');
|
|
124
|
+
navigation.redirect(url);
|
|
125
|
+
return url;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
async loadBilling(): Promise<BillingSummary> {
|
|
130
|
+
const [status, history] = await Promise.all([
|
|
131
|
+
adapter.getStatus(userId ?? ''),
|
|
132
|
+
adapter.getBillingHistory(userId ?? ''),
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
const tierById = tiers.find((tier) => tier.id === status.plan);
|
|
136
|
+
const product = productByPlanKey.get(status.plan);
|
|
137
|
+
const tier =
|
|
138
|
+
tierById ??
|
|
139
|
+
(product ? tiers.find((t) => t.id === product.id) : undefined) ??
|
|
140
|
+
freeTier(status.plan);
|
|
141
|
+
|
|
142
|
+
const subscription: Subscription = {
|
|
143
|
+
id: status.subscriptionId ?? 'none',
|
|
144
|
+
planId: status.plan,
|
|
145
|
+
plan: tier,
|
|
146
|
+
status: portalStatus(status.subscriptionStatus),
|
|
147
|
+
cycle: status.billingCycle ?? defaultCycle,
|
|
148
|
+
currentPeriodStart: '',
|
|
149
|
+
currentPeriodEnd: status.currentPeriodEnd ?? '',
|
|
150
|
+
...(status.cancelAtPeriodEnd !== undefined ? { cancelAtPeriodEnd: status.cancelAtPeriodEnd } : {}),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const price =
|
|
154
|
+
subscription.cycle === 'yearly' ? tier.yearlyPrice : tier.monthlyPrice;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
subscription,
|
|
158
|
+
paymentMethods: [],
|
|
159
|
+
usage: [],
|
|
160
|
+
recentInvoices: history.map(orderToInvoice),
|
|
161
|
+
...(subscription.status === SUBSCRIPTION_STATUS.ACTIVE && subscription.currentPeriodEnd
|
|
162
|
+
? {
|
|
163
|
+
upcomingInvoice: {
|
|
164
|
+
amount: price,
|
|
165
|
+
currency: tier.currency,
|
|
166
|
+
date: subscription.currentPeriodEnd,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
: {}),
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
updatePlan: (productId: string) => checkout(productId),
|
|
174
|
+
selectPlan: (productId: string, cycle?: BillingCycle) => checkout(productId, cycle),
|
|
175
|
+
|
|
176
|
+
updateCycle: async (cycle: BillingCycle) => {
|
|
177
|
+
const status = await adapter.getStatus(userId ?? '');
|
|
178
|
+
const product = productByPlanKey.get(status.plan) ?? tiers.find((t) => t.id === status.plan);
|
|
179
|
+
if (!product) throw new Error('Cannot switch cycle without a purchasable plan catalog');
|
|
180
|
+
await checkout(product.id, cycle);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
cancelSubscription: async () => {
|
|
184
|
+
await adapter.cancelSubscription();
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
addPaymentMethod: async () => {
|
|
188
|
+
await openPortal();
|
|
189
|
+
return HOSTED_PORTAL_METHOD;
|
|
190
|
+
},
|
|
191
|
+
removePaymentMethod: () => openPortal().then(() => undefined),
|
|
192
|
+
setDefaultPaymentMethod: () => openPortal().then(() => undefined),
|
|
193
|
+
|
|
194
|
+
getInvoiceUrl: async (invoiceId: string) => {
|
|
195
|
+
const history = await adapter.getBillingHistory(userId ?? '');
|
|
196
|
+
const order = history.find((item) => item.id === invoiceId);
|
|
197
|
+
return order?.invoiceUrl ?? (await openPortal());
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
openPortal,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
|
|
2
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
3
|
+
import type {
|
|
4
|
+
BillingSummary,
|
|
5
|
+
BillingCycle,
|
|
6
|
+
PaymentMethod,
|
|
7
|
+
PaymentMethodInput,
|
|
8
|
+
Invoice,
|
|
9
|
+
} from '../types';
|
|
10
|
+
import { DEFAULT_BILLING_CONFIG } from '../constants';
|
|
11
|
+
|
|
12
|
+
export type { Invoice };
|
|
13
|
+
|
|
14
|
+
export interface BillingApiClient {
|
|
15
|
+
loadBilling: () => Promise<BillingSummary>;
|
|
16
|
+
updatePlan: (planId: string) => Promise<void>;
|
|
17
|
+
cancelSubscription: () => Promise<void>;
|
|
18
|
+
updateCycle: (cycle: BillingCycle) => Promise<void>;
|
|
19
|
+
addPaymentMethod: (input: PaymentMethodInput) => Promise<PaymentMethod>;
|
|
20
|
+
removePaymentMethod: (methodId: string) => Promise<void>;
|
|
21
|
+
setDefaultPaymentMethod: (methodId: string) => Promise<void>;
|
|
22
|
+
getInvoiceUrl: (invoiceId: string) => Promise<string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseBillingOptions {
|
|
26
|
+
initialData?: BillingSummary;
|
|
27
|
+
apiClient: BillingApiClient;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UseBillingReturn {
|
|
31
|
+
billing: BillingSummary | null;
|
|
32
|
+
isLoading: boolean;
|
|
33
|
+
error: string | null;
|
|
34
|
+
loadBilling: () => Promise<void>;
|
|
35
|
+
updatePlan: (planId: string) => Promise<void>;
|
|
36
|
+
cancelSubscription: () => Promise<void>;
|
|
37
|
+
updateCycle: (cycle: BillingCycle) => Promise<void>;
|
|
38
|
+
addPaymentMethod: (input: PaymentMethodInput) => Promise<PaymentMethod | null>;
|
|
39
|
+
removePaymentMethod: (methodId: string) => Promise<void>;
|
|
40
|
+
setDefaultPaymentMethod: (methodId: string) => Promise<void>;
|
|
41
|
+
getInvoiceUrl: (invoiceId: string) => Promise<string | null>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const toErrorMessage = (err: unknown, fallback: string): string => {
|
|
45
|
+
if (err instanceof Error && err.message) return err.message;
|
|
46
|
+
return fallback;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const executeBillingAction = async <T>(
|
|
50
|
+
setIsLoading: (loading: boolean) => void,
|
|
51
|
+
setError: (message: string | null) => void,
|
|
52
|
+
fallbackMessage: string,
|
|
53
|
+
action: () => Promise<T>,
|
|
54
|
+
): Promise<T | null> => {
|
|
55
|
+
setIsLoading(true);
|
|
56
|
+
setError(null);
|
|
57
|
+
try {
|
|
58
|
+
return await action();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
setError(toErrorMessage(err, fallbackMessage));
|
|
61
|
+
return null;
|
|
62
|
+
} finally {
|
|
63
|
+
setIsLoading(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function useBilling(options: UseBillingOptions): UseBillingReturn {
|
|
68
|
+
const { initialData, apiClient } = options;
|
|
69
|
+
|
|
70
|
+
const [billing, setBilling] = useState<BillingSummary | null>(initialData ?? null);
|
|
71
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
72
|
+
const [error, setError] = useState<string | null>(null);
|
|
73
|
+
|
|
74
|
+
const loadBilling = useCallback(async () => {
|
|
75
|
+
const result = await executeBillingAction(setIsLoading, setError, 'Failed to load billing', () =>
|
|
76
|
+
apiClient.loadBilling(),
|
|
77
|
+
);
|
|
78
|
+
if (result) setBilling(result);
|
|
79
|
+
}, [apiClient]);
|
|
80
|
+
|
|
81
|
+
const updatePlan = useCallback(
|
|
82
|
+
async (planId: string) => {
|
|
83
|
+
await executeBillingAction(setIsLoading, setError, 'Failed to update plan', () =>
|
|
84
|
+
apiClient.updatePlan(planId).then(() => {
|
|
85
|
+
setBilling((prev) =>
|
|
86
|
+
prev
|
|
87
|
+
? { ...prev, subscription: { ...prev.subscription, planId } }
|
|
88
|
+
: prev,
|
|
89
|
+
);
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
[apiClient],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const cancelSubscription = useCallback(async () => {
|
|
97
|
+
await executeBillingAction(setIsLoading, setError, 'Failed to cancel subscription', () =>
|
|
98
|
+
apiClient.cancelSubscription().then(() => {
|
|
99
|
+
setBilling((prev) =>
|
|
100
|
+
prev
|
|
101
|
+
? {
|
|
102
|
+
...prev,
|
|
103
|
+
subscription: {
|
|
104
|
+
...prev.subscription,
|
|
105
|
+
status: 'canceled',
|
|
106
|
+
cancelAtPeriodEnd: true,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
: prev,
|
|
110
|
+
);
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
}, [apiClient]);
|
|
114
|
+
|
|
115
|
+
const updateCycle = useCallback(
|
|
116
|
+
async (cycle: BillingCycle) => {
|
|
117
|
+
await executeBillingAction(setIsLoading, setError, 'Failed to update cycle', () =>
|
|
118
|
+
apiClient.updateCycle(cycle).then(() => {
|
|
119
|
+
setBilling((prev) =>
|
|
120
|
+
prev
|
|
121
|
+
? { ...prev, subscription: { ...prev.subscription, cycle } }
|
|
122
|
+
: prev,
|
|
123
|
+
);
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
[apiClient],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const addPaymentMethod = useCallback(
|
|
131
|
+
async (input: PaymentMethodInput): Promise<PaymentMethod | null> => {
|
|
132
|
+
return executeBillingAction(setIsLoading, setError, 'Failed to add payment method', () =>
|
|
133
|
+
apiClient.addPaymentMethod(input).then((method) => {
|
|
134
|
+
setBilling((prev) =>
|
|
135
|
+
prev ? { ...prev, paymentMethods: [...prev.paymentMethods, method] } : prev,
|
|
136
|
+
);
|
|
137
|
+
return method;
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
[apiClient],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const removePaymentMethod = useCallback(
|
|
145
|
+
async (methodId: string) => {
|
|
146
|
+
await executeBillingAction(setIsLoading, setError, 'Failed to remove payment method', () =>
|
|
147
|
+
apiClient.removePaymentMethod(methodId).then(() => {
|
|
148
|
+
setBilling((prev) =>
|
|
149
|
+
prev
|
|
150
|
+
? {
|
|
151
|
+
...prev,
|
|
152
|
+
paymentMethods: prev.paymentMethods.filter((pm) => pm.id !== methodId),
|
|
153
|
+
}
|
|
154
|
+
: prev,
|
|
155
|
+
);
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
[apiClient],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const setDefaultPaymentMethod = useCallback(
|
|
163
|
+
async (methodId: string) => {
|
|
164
|
+
await executeBillingAction(setIsLoading, setError, 'Failed to set default payment method', () =>
|
|
165
|
+
apiClient.setDefaultPaymentMethod(methodId).then(() => {
|
|
166
|
+
setBilling((prev) => {
|
|
167
|
+
if (!prev) return prev;
|
|
168
|
+
const methods = prev.paymentMethods.map((pm) => ({
|
|
169
|
+
...pm,
|
|
170
|
+
isDefault: pm.id === methodId,
|
|
171
|
+
}));
|
|
172
|
+
const defaultPaymentMethod = methods.find((pm) => pm.id === methodId);
|
|
173
|
+
return { ...prev, paymentMethods: methods, defaultPaymentMethod };
|
|
174
|
+
});
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
[apiClient],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const getInvoiceUrl = useCallback(
|
|
182
|
+
async (invoiceId: string): Promise<string | null> => {
|
|
183
|
+
const result = await executeBillingAction(
|
|
184
|
+
setIsLoading,
|
|
185
|
+
setError,
|
|
186
|
+
'Failed to get invoice URL',
|
|
187
|
+
() => apiClient.getInvoiceUrl(invoiceId),
|
|
188
|
+
);
|
|
189
|
+
return result;
|
|
190
|
+
},
|
|
191
|
+
[apiClient],
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return useMemo(
|
|
195
|
+
() => ({
|
|
196
|
+
billing,
|
|
197
|
+
isLoading,
|
|
198
|
+
error,
|
|
199
|
+
loadBilling,
|
|
200
|
+
updatePlan,
|
|
201
|
+
cancelSubscription,
|
|
202
|
+
updateCycle,
|
|
203
|
+
addPaymentMethod,
|
|
204
|
+
removePaymentMethod,
|
|
205
|
+
setDefaultPaymentMethod,
|
|
206
|
+
getInvoiceUrl,
|
|
207
|
+
}),
|
|
208
|
+
[
|
|
209
|
+
billing,
|
|
210
|
+
isLoading,
|
|
211
|
+
error,
|
|
212
|
+
loadBilling,
|
|
213
|
+
updatePlan,
|
|
214
|
+
cancelSubscription,
|
|
215
|
+
updateCycle,
|
|
216
|
+
addPaymentMethod,
|
|
217
|
+
removePaymentMethod,
|
|
218
|
+
setDefaultPaymentMethod,
|
|
219
|
+
getInvoiceUrl,
|
|
220
|
+
],
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const createStubBillingApiClient = (
|
|
225
|
+
overrides?: Partial<BillingApiClient>,
|
|
226
|
+
): BillingApiClient => {
|
|
227
|
+
const notConfigured: BillingApiClient = {
|
|
228
|
+
loadBilling: () => Promise.reject(new Error('BillingApiClient not configured')),
|
|
229
|
+
updatePlan: () => Promise.reject(new Error('BillingApiClient not configured')),
|
|
230
|
+
cancelSubscription: () => Promise.reject(new Error('BillingApiClient not configured')),
|
|
231
|
+
updateCycle: () => Promise.reject(new Error('BillingApiClient not configured')),
|
|
232
|
+
addPaymentMethod: () => Promise.reject(new Error('BillingApiClient not configured')),
|
|
233
|
+
removePaymentMethod: () => Promise.reject(new Error('BillingApiClient not configured')),
|
|
234
|
+
setDefaultPaymentMethod: () => Promise.reject(new Error('BillingApiClient not configured')),
|
|
235
|
+
getInvoiceUrl: () => Promise.reject(new Error('BillingApiClient not configured')),
|
|
236
|
+
};
|
|
237
|
+
return { ...notConfigured, ...overrides };
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export { DEFAULT_BILLING_CONFIG };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
|
|
2
|
+
export {
|
|
3
|
+
BillingPortal,
|
|
4
|
+
BillingLayout,
|
|
5
|
+
BillingPage,
|
|
6
|
+
PlanComparison,
|
|
7
|
+
PaymentMethodsList,
|
|
8
|
+
InvoiceCard,
|
|
9
|
+
UsageCard,
|
|
10
|
+
PortalButton,
|
|
11
|
+
PortalSpinner,
|
|
12
|
+
RADIO_GROUP_NAME,
|
|
13
|
+
} from './components';
|
|
14
|
+
export type {
|
|
15
|
+
PortalButtonProps,
|
|
16
|
+
PortalButtonVariant,
|
|
17
|
+
PortalButtonSize,
|
|
18
|
+
} from './components';
|
|
19
|
+
|
|
20
|
+
export { useBilling, createStubBillingApiClient } from './hooks';
|
|
21
|
+
export type { BillingApiClient, UseBillingOptions, UseBillingReturn } from './hooks';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
DEFAULT_BILLING_LABELS,
|
|
25
|
+
BillingLabelsProvider,
|
|
26
|
+
useBillingLabels,
|
|
27
|
+
mergeLabels,
|
|
28
|
+
formatLabel,
|
|
29
|
+
} from './labels';
|
|
30
|
+
export type { BillingLabels, BillingLabelsOverrides } from './labels';
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
formatNumber,
|
|
34
|
+
formatPrice,
|
|
35
|
+
calculateDiscount,
|
|
36
|
+
calculateProratedAmount,
|
|
37
|
+
getPlanPrice,
|
|
38
|
+
calculateUsagePercentage,
|
|
39
|
+
isNearLimit,
|
|
40
|
+
getStatusTone,
|
|
41
|
+
getStatusLabel,
|
|
42
|
+
getInvoiceStatusTone,
|
|
43
|
+
getInvoiceStatusLabel,
|
|
44
|
+
formatCardNumber,
|
|
45
|
+
formatExpiry,
|
|
46
|
+
getDaysRemaining,
|
|
47
|
+
isTrialExpiringSoon,
|
|
48
|
+
getNextBillingDate,
|
|
49
|
+
groupInvoicesByStatus,
|
|
50
|
+
calculateInvoiceTotal,
|
|
51
|
+
sortInvoicesByDate,
|
|
52
|
+
formatFeature,
|
|
53
|
+
isPopularPlan,
|
|
54
|
+
getTrialDaysText,
|
|
55
|
+
transformProductToPlan,
|
|
56
|
+
transformProducts,
|
|
57
|
+
} from './utils';
|
|
58
|
+
|
|
59
|
+
export type {
|
|
60
|
+
BillingCycle,
|
|
61
|
+
SubscriptionStatus,
|
|
62
|
+
PlanType,
|
|
63
|
+
Currency,
|
|
64
|
+
PlanTier,
|
|
65
|
+
Subscription,
|
|
66
|
+
PaymentMethodType,
|
|
67
|
+
PaymentMethod,
|
|
68
|
+
PaymentMethodInput,
|
|
69
|
+
InvoiceStatus,
|
|
70
|
+
InvoiceItem,
|
|
71
|
+
Invoice,
|
|
72
|
+
UsageMetric,
|
|
73
|
+
BillingSummary,
|
|
74
|
+
PlanComparisonProps,
|
|
75
|
+
PaymentMethodsListProps,
|
|
76
|
+
InvoiceCardProps,
|
|
77
|
+
UsageCardProps,
|
|
78
|
+
BillingPortalProps,
|
|
79
|
+
BillingTabId,
|
|
80
|
+
BillingLayoutProps,
|
|
81
|
+
BillingConfig,
|
|
82
|
+
BillingPageProps,
|
|
83
|
+
} from './types';
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
SUBSCRIPTION_STATUS,
|
|
87
|
+
INVOICE_STATUS,
|
|
88
|
+
CURRENCY,
|
|
89
|
+
PLAN_TYPE,
|
|
90
|
+
DEFAULT_BILLING_CONFIG,
|
|
91
|
+
} from './constants';
|
|
92
|
+
export { BILLING_CYCLE } from './types';
|
|
93
|
+
|
|
94
|
+
export { createPortalClient } from './create-portal-client';
|
|
95
|
+
export type { PortalClient, PortalClientOptions } from './create-portal-client';
|
|
96
|
+
export { TurnkeyBillingPage } from './TurnkeyBillingPage';
|
|
97
|
+
export type { TurnkeyBillingPageProps } from './TurnkeyBillingPage';
|