@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,218 @@
|
|
|
1
|
+
|
|
2
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import type { InvoiceStatus, SubscriptionStatus } from './types';
|
|
5
|
+
|
|
6
|
+
export function formatLabel(template: string, params: Record<string, string | number>): string {
|
|
7
|
+
return template.replace(/\{(\w+)\}/g, (match, key: string) =>
|
|
8
|
+
key in params ? String(params[key]) : match,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BillingLabels {
|
|
13
|
+
common: {
|
|
14
|
+
loading: string;
|
|
15
|
+
usage: string;
|
|
16
|
+
};
|
|
17
|
+
page: {
|
|
18
|
+
backToDashboard: string;
|
|
19
|
+
title: string;
|
|
20
|
+
description: string;
|
|
21
|
+
needHelp: string;
|
|
22
|
+
contactAria: string;
|
|
23
|
+
};
|
|
24
|
+
tabs: {
|
|
25
|
+
overview: string;
|
|
26
|
+
paymentMethods: string;
|
|
27
|
+
invoices: string;
|
|
28
|
+
plan: string;
|
|
29
|
+
};
|
|
30
|
+
overview: {
|
|
31
|
+
currentSubscription: string;
|
|
32
|
+
trialDaysLeft: string;
|
|
33
|
+
nextBilling: string;
|
|
34
|
+
upcomingInvoice: string;
|
|
35
|
+
};
|
|
36
|
+
invoices: {
|
|
37
|
+
empty: string;
|
|
38
|
+
moreItems: string;
|
|
39
|
+
due: string;
|
|
40
|
+
viewInvoice: string;
|
|
41
|
+
downloadPdf: string;
|
|
42
|
+
};
|
|
43
|
+
plan: {
|
|
44
|
+
title: string;
|
|
45
|
+
actionsTitle: string;
|
|
46
|
+
changePlan: string;
|
|
47
|
+
cancelSubscription: string;
|
|
48
|
+
confirmCancel: string;
|
|
49
|
+
cancelling: string;
|
|
50
|
+
cancelConfirmMessage: string;
|
|
51
|
+
cancelFailed: string;
|
|
52
|
+
};
|
|
53
|
+
planComparison: {
|
|
54
|
+
monthly: string;
|
|
55
|
+
yearly: string;
|
|
56
|
+
monthlyShort: string;
|
|
57
|
+
yearlyShort: string;
|
|
58
|
+
save: string;
|
|
59
|
+
saveWithYearly: string;
|
|
60
|
+
mostPopular: string;
|
|
61
|
+
currentPlan: string;
|
|
62
|
+
selectPlan: string;
|
|
63
|
+
processing: string;
|
|
64
|
+
billingCycleLabel: string;
|
|
65
|
+
radiogroupLabel: string;
|
|
66
|
+
};
|
|
67
|
+
paymentMethods: {
|
|
68
|
+
none: string;
|
|
69
|
+
add: string;
|
|
70
|
+
bankAccount: string;
|
|
71
|
+
expires: string;
|
|
72
|
+
default: string;
|
|
73
|
+
setDefault: string;
|
|
74
|
+
remove: string;
|
|
75
|
+
removeConfirm: string;
|
|
76
|
+
confirmRemove: string;
|
|
77
|
+
cancelRemove: string;
|
|
78
|
+
};
|
|
79
|
+
usage: {
|
|
80
|
+
limit: string;
|
|
81
|
+
resets: string;
|
|
82
|
+
};
|
|
83
|
+
status: Record<SubscriptionStatus, string>;
|
|
84
|
+
invoiceStatus: Record<InvoiceStatus, string>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type BillingLabelsOverrides = {
|
|
88
|
+
[K in keyof BillingLabels]?: BillingLabels[K] extends string
|
|
89
|
+
? string
|
|
90
|
+
: { [K2 in keyof BillingLabels[K]]?: BillingLabels[K][K2] };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const DEFAULT_BILLING_LABELS: Readonly<BillingLabels> = Object.freeze({
|
|
94
|
+
common: Object.freeze({
|
|
95
|
+
loading: 'Loading…',
|
|
96
|
+
usage: 'usage',
|
|
97
|
+
}),
|
|
98
|
+
page: Object.freeze({
|
|
99
|
+
backToDashboard: 'Back to dashboard',
|
|
100
|
+
title: 'Billing',
|
|
101
|
+
description: 'Manage your subscription, payment methods, and invoices.',
|
|
102
|
+
needHelp: 'Need help?',
|
|
103
|
+
contactAria: 'Contact {email}',
|
|
104
|
+
}),
|
|
105
|
+
tabs: Object.freeze({
|
|
106
|
+
overview: 'Overview',
|
|
107
|
+
paymentMethods: 'Payment Methods',
|
|
108
|
+
invoices: 'Invoices',
|
|
109
|
+
plan: 'Plan',
|
|
110
|
+
}),
|
|
111
|
+
overview: Object.freeze({
|
|
112
|
+
currentSubscription: 'Current Subscription',
|
|
113
|
+
trialDaysLeft: 'days left in trial',
|
|
114
|
+
nextBilling: 'Next billing:',
|
|
115
|
+
upcomingInvoice: 'Upcoming Invoice',
|
|
116
|
+
}),
|
|
117
|
+
invoices: Object.freeze({
|
|
118
|
+
empty: 'No invoices yet',
|
|
119
|
+
moreItems: 'more items',
|
|
120
|
+
due: 'Due',
|
|
121
|
+
viewInvoice: 'View invoice',
|
|
122
|
+
downloadPdf: 'Download PDF',
|
|
123
|
+
}),
|
|
124
|
+
plan: Object.freeze({
|
|
125
|
+
title: 'Current Plan',
|
|
126
|
+
actionsTitle: 'Plan Actions',
|
|
127
|
+
changePlan: 'Change Plan',
|
|
128
|
+
cancelSubscription: 'Cancel Subscription',
|
|
129
|
+
confirmCancel: 'Click again to confirm',
|
|
130
|
+
cancelling: 'Cancelling…',
|
|
131
|
+
cancelConfirmMessage:
|
|
132
|
+
'Your subscription stays active until the end of the current billing period.',
|
|
133
|
+
cancelFailed: 'Failed to cancel subscription. Please try again.',
|
|
134
|
+
}),
|
|
135
|
+
planComparison: Object.freeze({
|
|
136
|
+
monthly: 'Monthly',
|
|
137
|
+
yearly: 'Yearly',
|
|
138
|
+
monthlyShort: 'mo',
|
|
139
|
+
yearlyShort: 'yr',
|
|
140
|
+
save: 'Save {percent}%',
|
|
141
|
+
saveWithYearly: 'Save {percent}% with yearly billing',
|
|
142
|
+
mostPopular: 'Most Popular',
|
|
143
|
+
currentPlan: 'Current Plan',
|
|
144
|
+
selectPlan: 'Select Plan',
|
|
145
|
+
processing: 'Processing…',
|
|
146
|
+
billingCycleLabel: 'Billing cycle',
|
|
147
|
+
radiogroupLabel: 'Available plans',
|
|
148
|
+
}),
|
|
149
|
+
paymentMethods: Object.freeze({
|
|
150
|
+
none: 'No payment methods on file',
|
|
151
|
+
add: 'Add Payment Method',
|
|
152
|
+
bankAccount: 'Bank Account',
|
|
153
|
+
expires: 'Expires',
|
|
154
|
+
default: 'Default',
|
|
155
|
+
setDefault: 'Set as default',
|
|
156
|
+
remove: 'Remove',
|
|
157
|
+
removeConfirm: 'Remove this payment method?',
|
|
158
|
+
confirmRemove: 'Remove',
|
|
159
|
+
cancelRemove: 'Cancel',
|
|
160
|
+
}),
|
|
161
|
+
usage: Object.freeze({
|
|
162
|
+
limit: 'limit',
|
|
163
|
+
resets: 'Resets',
|
|
164
|
+
}),
|
|
165
|
+
status: Object.freeze({
|
|
166
|
+
active: 'Active',
|
|
167
|
+
trialing: 'Trial',
|
|
168
|
+
past_due: 'Past Due',
|
|
169
|
+
canceled: 'Canceled',
|
|
170
|
+
unpaid: 'Unpaid',
|
|
171
|
+
incomplete: 'Incomplete',
|
|
172
|
+
revoked: 'Revoked',
|
|
173
|
+
}),
|
|
174
|
+
invoiceStatus: Object.freeze({
|
|
175
|
+
draft: 'Draft',
|
|
176
|
+
open: 'Open',
|
|
177
|
+
paid: 'Paid',
|
|
178
|
+
void: 'Void',
|
|
179
|
+
uncollectible: 'Uncollectible',
|
|
180
|
+
refunded: 'Refunded',
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
export function mergeLabels(overrides?: BillingLabelsOverrides): BillingLabels {
|
|
185
|
+
if (!overrides) return { ...DEFAULT_BILLING_LABELS } as BillingLabels;
|
|
186
|
+
const merged = { ...DEFAULT_BILLING_LABELS } as BillingLabels;
|
|
187
|
+
for (const key of Object.keys(overrides) as (keyof BillingLabelsOverrides)[]) {
|
|
188
|
+
const override = overrides[key];
|
|
189
|
+
if (override === undefined) continue;
|
|
190
|
+
const base = DEFAULT_BILLING_LABELS[key];
|
|
191
|
+
const target = merged as unknown as Record<string, unknown>;
|
|
192
|
+
if (typeof base === 'string' || typeof override === 'string') {
|
|
193
|
+
target[key] = override;
|
|
194
|
+
} else {
|
|
195
|
+
target[key] = { ...base, ...override };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return merged;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const BillingLabelsContext = createContext<BillingLabels>(
|
|
202
|
+
DEFAULT_BILLING_LABELS as BillingLabels,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
export function BillingLabelsProvider({
|
|
206
|
+
labels,
|
|
207
|
+
children,
|
|
208
|
+
}: {
|
|
209
|
+
labels?: BillingLabelsOverrides;
|
|
210
|
+
children: ReactNode;
|
|
211
|
+
}) {
|
|
212
|
+
const value = useMemo(() => mergeLabels(labels), [labels]);
|
|
213
|
+
return <BillingLabelsContext.Provider value={value}>{children}</BillingLabelsContext.Provider>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function useBillingLabels(): BillingLabels {
|
|
217
|
+
return useContext(BillingLabelsContext);
|
|
218
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
import { VisualTokens } from '../theme/visual-tokens';
|
|
4
|
+
|
|
5
|
+
export const visuallyHidden: CSSProperties = {
|
|
6
|
+
position: 'absolute',
|
|
7
|
+
width: 1,
|
|
8
|
+
height: 1,
|
|
9
|
+
padding: 0,
|
|
10
|
+
margin: -1,
|
|
11
|
+
overflow: 'hidden',
|
|
12
|
+
clip: 'rect(0 0 0 0)',
|
|
13
|
+
whiteSpace: 'nowrap',
|
|
14
|
+
border: 0,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const card: CSSProperties = {
|
|
18
|
+
background: VisualTokens.color.surfacePrimary,
|
|
19
|
+
border: `1px solid ${VisualTokens.color.borderSubtle}`,
|
|
20
|
+
borderRadius: VisualTokens.radius.lg,
|
|
21
|
+
padding: VisualTokens.spacing.section,
|
|
22
|
+
boxSizing: 'border-box',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const cardInteractive: CSSProperties = {
|
|
26
|
+
...card,
|
|
27
|
+
cursor: 'pointer',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const sectionTitle: CSSProperties = {
|
|
31
|
+
fontSize: VisualTokens.fontSize.title,
|
|
32
|
+
fontWeight: VisualTokens.fontWeight.strong,
|
|
33
|
+
color: VisualTokens.color.textPrimary,
|
|
34
|
+
margin: 0,
|
|
35
|
+
marginBottom: VisualTokens.spacing.xl,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const mutedText: CSSProperties = {
|
|
39
|
+
color: VisualTokens.color.textMuted,
|
|
40
|
+
fontSize: VisualTokens.fontSize.body,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const stack: CSSProperties = {
|
|
44
|
+
display: 'flex',
|
|
45
|
+
flexDirection: 'column',
|
|
46
|
+
gap: VisualTokens.spacing.xl,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const row: CSSProperties = {
|
|
50
|
+
display: 'flex',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const autoGrid = (minColumnWidth = 260): CSSProperties => ({
|
|
55
|
+
display: 'grid',
|
|
56
|
+
gridTemplateColumns: `repeat(auto-fit, minmax(${minColumnWidth}px, 1fr))`,
|
|
57
|
+
gap: VisualTokens.spacing.section,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const alertDanger: CSSProperties = {
|
|
61
|
+
display: 'flex',
|
|
62
|
+
alignItems: 'flex-start',
|
|
63
|
+
gap: VisualTokens.spacing.md,
|
|
64
|
+
padding: VisualTokens.spacing.lg,
|
|
65
|
+
borderRadius: VisualTokens.radius.md,
|
|
66
|
+
border: `1px solid ${VisualTokens.color.borderWarning}`,
|
|
67
|
+
background: VisualTokens.color.surfaceDangerMuted,
|
|
68
|
+
color: VisualTokens.color.brandDangerText,
|
|
69
|
+
fontSize: VisualTokens.fontSize.body,
|
|
70
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { BILLING_CYCLE } from '../../domain/enums/billing-cycle.enum';
|
|
4
|
+
import type { BillingCycle } from '../../domain/enums/billing-cycle.enum';
|
|
5
|
+
import { SUBSCRIPTION_STATUS, INVOICE_STATUS, CURRENCY } from './constants';
|
|
6
|
+
import type { BillingLabelsOverrides } from './labels';
|
|
7
|
+
|
|
8
|
+
export { BILLING_CYCLE };
|
|
9
|
+
export type { BillingCycle } from '../../domain/enums/billing-cycle.enum';
|
|
10
|
+
|
|
11
|
+
export type SubscriptionStatus = typeof SUBSCRIPTION_STATUS[keyof typeof SUBSCRIPTION_STATUS];
|
|
12
|
+
|
|
13
|
+
export type InvoiceStatus = typeof INVOICE_STATUS[keyof typeof INVOICE_STATUS];
|
|
14
|
+
|
|
15
|
+
export type PlanType = string;
|
|
16
|
+
|
|
17
|
+
export type Currency = typeof CURRENCY[keyof typeof CURRENCY];
|
|
18
|
+
|
|
19
|
+
export type PlanFeature = string | { text: string; bold?: boolean; included?: boolean };
|
|
20
|
+
|
|
21
|
+
export interface PlanTier {
|
|
22
|
+
id: string;
|
|
23
|
+
type?: PlanType;
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
badge?: string;
|
|
27
|
+
badgeColor?: string;
|
|
28
|
+
monthlyPrice: number;
|
|
29
|
+
yearlyPrice: number;
|
|
30
|
+
currency: Currency;
|
|
31
|
+
features: PlanFeature[];
|
|
32
|
+
highlight?: boolean;
|
|
33
|
+
maxUsers?: number;
|
|
34
|
+
storageLimit?: number;
|
|
35
|
+
apiLimit?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Subscription {
|
|
39
|
+
id: string;
|
|
40
|
+
planId: string;
|
|
41
|
+
plan: PlanTier;
|
|
42
|
+
status: SubscriptionStatus;
|
|
43
|
+
cycle: BillingCycle;
|
|
44
|
+
currentPeriodStart: string;
|
|
45
|
+
currentPeriodEnd: string;
|
|
46
|
+
cancelAtPeriodEnd?: boolean;
|
|
47
|
+
trialEnd?: string;
|
|
48
|
+
seats?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type PaymentMethodType = 'card' | 'bank_account' | 'wallet';
|
|
52
|
+
|
|
53
|
+
export interface PaymentMethod {
|
|
54
|
+
id: string;
|
|
55
|
+
type: PaymentMethodType;
|
|
56
|
+
isDefault: boolean;
|
|
57
|
+
card?: {
|
|
58
|
+
last4: string;
|
|
59
|
+
brand: string;
|
|
60
|
+
expiryMonth: number;
|
|
61
|
+
expiryYear: number;
|
|
62
|
+
name?: string;
|
|
63
|
+
};
|
|
64
|
+
bankAccount?: {
|
|
65
|
+
last4: string;
|
|
66
|
+
bankName: string;
|
|
67
|
+
accountType?: 'checking' | 'savings';
|
|
68
|
+
};
|
|
69
|
+
createdAt: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface InvoiceItem {
|
|
73
|
+
description: string;
|
|
74
|
+
quantity: number;
|
|
75
|
+
unitPrice: number;
|
|
76
|
+
amount: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface Invoice {
|
|
80
|
+
id: string;
|
|
81
|
+
number: string;
|
|
82
|
+
amount: number;
|
|
83
|
+
currency: string;
|
|
84
|
+
status: InvoiceStatus;
|
|
85
|
+
date: string;
|
|
86
|
+
dueDate: string;
|
|
87
|
+
paidAt?: string;
|
|
88
|
+
invoiceUrl?: string;
|
|
89
|
+
pdfUrl?: string;
|
|
90
|
+
items?: InvoiceItem[];
|
|
91
|
+
subtotal?: number;
|
|
92
|
+
tax?: number;
|
|
93
|
+
total?: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface UsageMetric {
|
|
97
|
+
id: string;
|
|
98
|
+
name: string;
|
|
99
|
+
current: number;
|
|
100
|
+
limit: number;
|
|
101
|
+
unit: string;
|
|
102
|
+
resetPeriod: 'daily' | 'monthly' | 'yearly';
|
|
103
|
+
resetAt?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface BillingSummary {
|
|
107
|
+
subscription: Subscription;
|
|
108
|
+
paymentMethods: PaymentMethod[];
|
|
109
|
+
defaultPaymentMethod?: PaymentMethod;
|
|
110
|
+
upcomingInvoice?: {
|
|
111
|
+
amount: number;
|
|
112
|
+
currency: Currency;
|
|
113
|
+
date: string;
|
|
114
|
+
};
|
|
115
|
+
usage: UsageMetric[];
|
|
116
|
+
recentInvoices: Invoice[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type PaymentMethodInput =
|
|
120
|
+
| {
|
|
121
|
+
type: 'card';
|
|
122
|
+
last4: string;
|
|
123
|
+
brand: string;
|
|
124
|
+
expiryMonth: number;
|
|
125
|
+
expiryYear: number;
|
|
126
|
+
name?: string;
|
|
127
|
+
}
|
|
128
|
+
| {
|
|
129
|
+
type: 'bank_account';
|
|
130
|
+
last4: string;
|
|
131
|
+
bankName: string;
|
|
132
|
+
accountType?: 'checking' | 'savings';
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export interface PlanComparisonProps {
|
|
136
|
+
plans: PlanTier[];
|
|
137
|
+
selectedPlan?: string;
|
|
138
|
+
cycle: BillingCycle;
|
|
139
|
+
showCycleToggle?: boolean;
|
|
140
|
+
showFeatures?: boolean;
|
|
141
|
+
onPlanSelect?: (planId: string) => void;
|
|
142
|
+
onCycleChange?: (cycle: BillingCycle) => void;
|
|
143
|
+
loading?: boolean;
|
|
144
|
+
labels?: BillingLabelsOverrides;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface PaymentMethodsListProps {
|
|
148
|
+
paymentMethods: PaymentMethod[];
|
|
149
|
+
loading?: boolean;
|
|
150
|
+
busyId?: string | null;
|
|
151
|
+
onSetDefault?: (methodId: string) => void;
|
|
152
|
+
onRemove?: (methodId: string) => void;
|
|
153
|
+
onAddNew?: () => void;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface InvoiceCardProps {
|
|
157
|
+
invoice: Invoice;
|
|
158
|
+
compact?: boolean;
|
|
159
|
+
onClick?: (invoice: Invoice) => void;
|
|
160
|
+
onDownload?: (invoice: Invoice) => void;
|
|
161
|
+
locale?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface UsageCardProps {
|
|
165
|
+
metric: UsageMetric;
|
|
166
|
+
locale?: string;
|
|
167
|
+
showProgress?: boolean;
|
|
168
|
+
showLimit?: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export type BillingTabId = 'overview' | 'payment-methods' | 'invoices' | 'plan';
|
|
172
|
+
|
|
173
|
+
export interface BillingPortalProps {
|
|
174
|
+
billing: BillingSummary;
|
|
175
|
+
loading?: boolean;
|
|
176
|
+
error?: string;
|
|
177
|
+
showTabs?: boolean;
|
|
178
|
+
activeTab?: BillingTabId;
|
|
179
|
+
locale?: string;
|
|
180
|
+
onTabChange?: (tab: BillingTabId) => void;
|
|
181
|
+
onAddPaymentMethod?: () => void;
|
|
182
|
+
onSetDefaultPaymentMethod?: (methodId: string) => void;
|
|
183
|
+
onRemovePaymentMethod?: (methodId: string) => void;
|
|
184
|
+
onViewInvoice?: (invoice: Invoice) => void;
|
|
185
|
+
onChangePlan?: () => void;
|
|
186
|
+
onCancelSubscription?: () => void | Promise<void>;
|
|
187
|
+
labels?: BillingLabelsOverrides;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface BillingLayoutProps {
|
|
191
|
+
config: BillingConfig;
|
|
192
|
+
children?: ReactNode;
|
|
193
|
+
onBack?: () => void;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface BillingConfig {
|
|
197
|
+
brandName: string;
|
|
198
|
+
logo?: ReactNode;
|
|
199
|
+
plans: PlanTier[];
|
|
200
|
+
currencies?: Currency[];
|
|
201
|
+
defaultCurrency?: Currency;
|
|
202
|
+
enableYearlyDiscount?: boolean;
|
|
203
|
+
yearlyDiscount?: number;
|
|
204
|
+
taxRate?: number;
|
|
205
|
+
supportEmail?: string;
|
|
206
|
+
cancelRoute?: string;
|
|
207
|
+
updatePaymentRoute?: string;
|
|
208
|
+
invoiceHistoryRoute?: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface BillingPageProps extends Omit<BillingPortalProps, 'showTabs'> {
|
|
212
|
+
config: BillingConfig;
|
|
213
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
|
|
2
|
+
import type { VisualTone } from '../../theme/visual-tokens';
|
|
3
|
+
import { toMajorUnits } from '../../../domain/utils/minor-units';
|
|
4
|
+
import type {
|
|
5
|
+
BillingCycle,
|
|
6
|
+
Invoice,
|
|
7
|
+
InvoiceStatus,
|
|
8
|
+
PlanTier,
|
|
9
|
+
SubscriptionStatus,
|
|
10
|
+
UsageMetric,
|
|
11
|
+
} from '../types';
|
|
12
|
+
|
|
13
|
+
export function formatNumber(num: number, locale: string = 'en-US'): string {
|
|
14
|
+
return new Intl.NumberFormat(locale).format(num);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function formatPrice(amount: number, currency: string = 'USD'): string {
|
|
18
|
+
const localeMap: Record<string, string> = {
|
|
19
|
+
USD: 'en-US',
|
|
20
|
+
EUR: 'de-DE',
|
|
21
|
+
GBP: 'en-GB',
|
|
22
|
+
TRY: 'tr-TR',
|
|
23
|
+
JPY: 'ja-JP',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return new Intl.NumberFormat(localeMap[currency] ?? 'en-US', {
|
|
27
|
+
style: 'currency',
|
|
28
|
+
currency,
|
|
29
|
+
}).format(toMajorUnits(amount, currency));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function calculateDiscount(monthlyPrice: number, yearlyPrice: number): number {
|
|
33
|
+
if (monthlyPrice <= 0) return 0;
|
|
34
|
+
const yearlyMonthly = yearlyPrice / 12;
|
|
35
|
+
const discount = ((monthlyPrice - yearlyMonthly) / monthlyPrice) * 100;
|
|
36
|
+
return Math.round(Math.max(discount, 0));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function calculateProratedAmount(
|
|
40
|
+
amount: number,
|
|
41
|
+
daysUsed: number,
|
|
42
|
+
totalDays: number,
|
|
43
|
+
): number {
|
|
44
|
+
if (totalDays === 0) return 0;
|
|
45
|
+
return (amount / totalDays) * daysUsed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getPlanPrice(plan: PlanTier, cycle: BillingCycle): number {
|
|
49
|
+
return cycle === 'monthly' ? plan.monthlyPrice : plan.yearlyPrice;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function calculateUsagePercentage(metric: UsageMetric): number {
|
|
53
|
+
if (metric.limit === 0) return 0;
|
|
54
|
+
return Math.min((metric.current / metric.limit) * 100, 100);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isNearLimit(metric: UsageMetric, threshold: number = 80): boolean {
|
|
58
|
+
return calculateUsagePercentage(metric) >= threshold;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getStatusTone(status: SubscriptionStatus): VisualTone {
|
|
62
|
+
const toneMap: Record<SubscriptionStatus, VisualTone> = {
|
|
63
|
+
active: 'active',
|
|
64
|
+
trialing: 'active',
|
|
65
|
+
past_due: 'warning',
|
|
66
|
+
canceled: 'neutral',
|
|
67
|
+
unpaid: 'danger',
|
|
68
|
+
incomplete: 'warning',
|
|
69
|
+
revoked: 'danger',
|
|
70
|
+
};
|
|
71
|
+
return toneMap[status] ?? 'neutral';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getStatusLabel(status: SubscriptionStatus): string {
|
|
75
|
+
const labelMap: Record<SubscriptionStatus, string> = {
|
|
76
|
+
active: 'Active',
|
|
77
|
+
trialing: 'Trial',
|
|
78
|
+
past_due: 'Past Due',
|
|
79
|
+
canceled: 'Canceled',
|
|
80
|
+
unpaid: 'Unpaid',
|
|
81
|
+
incomplete: 'Incomplete',
|
|
82
|
+
revoked: 'Revoked',
|
|
83
|
+
};
|
|
84
|
+
return labelMap[status] || status;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getInvoiceStatusTone(status: InvoiceStatus): VisualTone {
|
|
88
|
+
const toneMap: Record<InvoiceStatus, VisualTone> = {
|
|
89
|
+
draft: 'neutral',
|
|
90
|
+
open: 'warning',
|
|
91
|
+
paid: 'active',
|
|
92
|
+
void: 'neutral',
|
|
93
|
+
uncollectible: 'danger',
|
|
94
|
+
refunded: 'neutral',
|
|
95
|
+
};
|
|
96
|
+
return toneMap[status] ?? 'neutral';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getInvoiceStatusLabel(status: InvoiceStatus): string {
|
|
100
|
+
const labelMap: Record<InvoiceStatus, string> = {
|
|
101
|
+
draft: 'Draft',
|
|
102
|
+
open: 'Open',
|
|
103
|
+
paid: 'Paid',
|
|
104
|
+
void: 'Void',
|
|
105
|
+
uncollectible: 'Uncollectible',
|
|
106
|
+
refunded: 'Refunded',
|
|
107
|
+
};
|
|
108
|
+
return labelMap[status] || status;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatCardNumber(last4: string, brand: string): string {
|
|
112
|
+
return `${brand.toUpperCase()} •••• ${last4}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function formatExpiry(month: number, year: number): string {
|
|
116
|
+
return `${String(month).padStart(2, '0')}/${String(year).slice(-2)}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getDaysRemaining(endDate: string): number {
|
|
120
|
+
const end = new Date(endDate);
|
|
121
|
+
const now = new Date();
|
|
122
|
+
const diff = end.getTime() - now.getTime();
|
|
123
|
+
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isTrialExpiringSoon(trialEnd: string, daysThreshold: number = 7): boolean {
|
|
127
|
+
const daysRemaining = getDaysRemaining(trialEnd);
|
|
128
|
+
return daysRemaining > 0 && daysRemaining <= daysThreshold;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getNextBillingDate(currentPeriodEnd: string, cycle: BillingCycle): Date {
|
|
132
|
+
const nextDate = new Date(currentPeriodEnd);
|
|
133
|
+
|
|
134
|
+
if (cycle === 'monthly') {
|
|
135
|
+
nextDate.setMonth(nextDate.getMonth() + 1);
|
|
136
|
+
} else {
|
|
137
|
+
nextDate.setFullYear(nextDate.getFullYear() + 1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return nextDate;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function groupInvoicesByStatus(invoices: Invoice[]): Record<InvoiceStatus, Invoice[]> {
|
|
144
|
+
const grouped: Record<InvoiceStatus, Invoice[]> = {
|
|
145
|
+
draft: [],
|
|
146
|
+
open: [],
|
|
147
|
+
paid: [],
|
|
148
|
+
void: [],
|
|
149
|
+
uncollectible: [],
|
|
150
|
+
refunded: [],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
invoices.forEach((invoice) => {
|
|
154
|
+
grouped[invoice.status].push(invoice);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return grouped;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function calculateInvoiceTotal(invoices: Invoice[], status?: InvoiceStatus): number {
|
|
161
|
+
const filtered = status ? invoices.filter((inv) => inv.status === status) : invoices;
|
|
162
|
+
return filtered.reduce((sum, inv) => sum + inv.amount, 0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function sortInvoicesByDate(invoices: Invoice[], order: 'asc' | 'desc' = 'desc'): Invoice[] {
|
|
166
|
+
return [...invoices].sort((a, b) => {
|
|
167
|
+
const dateA = new Date(a.date).getTime();
|
|
168
|
+
const dateB = new Date(b.date).getTime();
|
|
169
|
+
return order === 'asc' ? dateA - dateB : dateB - dateA;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function formatFeature(
|
|
174
|
+
feature: string | { text: string; bold?: boolean; included?: boolean },
|
|
175
|
+
): { text: string; bold?: boolean; included?: boolean } {
|
|
176
|
+
if (typeof feature === 'string') {
|
|
177
|
+
return { text: feature, included: true };
|
|
178
|
+
}
|
|
179
|
+
return feature;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function isPopularPlan(plan: PlanTier, plans: PlanTier[]): boolean {
|
|
183
|
+
const middleIndex = Math.floor(plans.length / 2);
|
|
184
|
+
return plans[middleIndex]?.id === plan.id;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function getTrialDaysText(trialEnd: string): string {
|
|
188
|
+
const days = getDaysRemaining(trialEnd);
|
|
189
|
+
|
|
190
|
+
if (days === 0) return 'Trial ends today';
|
|
191
|
+
if (days === 1) return '1 day left';
|
|
192
|
+
return `${days} days left`;
|
|
193
|
+
}
|