@umituz/web-payment 0.0.0-stage → 1.0.0

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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -2
  3. package/dist/billing-adapter.interface-CDkeeowZ.d.mts +94 -0
  4. package/dist/billing-adapter.interface-m7yE50I0.d.ts +94 -0
  5. package/dist/billing-cycle.enum--hJRYlgP.d.mts +10 -0
  6. package/dist/billing-cycle.enum--hJRYlgP.d.ts +10 -0
  7. package/dist/billing-env-D4YjvA3M.d.mts +8 -0
  8. package/dist/billing-env-D4YjvA3M.d.ts +8 -0
  9. package/dist/checkout-metadata-D7Wp_DAD.d.mts +27 -0
  10. package/dist/checkout-metadata-D7Wp_DAD.d.ts +27 -0
  11. package/dist/chunk-2KV7JFYF.mjs +61 -0
  12. package/dist/chunk-2NH5DOLT.mjs +101 -0
  13. package/dist/chunk-3AXPZOCV.mjs +106 -0
  14. package/dist/chunk-4N3BASZ6.mjs +51 -0
  15. package/dist/chunk-NHKK3ENU.mjs +1128 -0
  16. package/dist/chunk-PIWNUAKW.mjs +30 -0
  17. package/dist/chunk-QFB7AOSQ.mjs +33 -0
  18. package/dist/chunk-RQTMBR74.mjs +30 -0
  19. package/dist/chunk-U45FS4FF.mjs +110 -0
  20. package/dist/chunk-X6R76FZT.mjs +0 -0
  21. package/dist/chunk-XQ2HJTTA.mjs +252 -0
  22. package/dist/core.d.mts +17 -0
  23. package/dist/core.d.ts +17 -0
  24. package/dist/core.js +425 -0
  25. package/dist/core.mjs +66 -0
  26. package/dist/http.d.mts +12 -0
  27. package/dist/http.d.ts +12 -0
  28. package/dist/http.js +115 -0
  29. package/dist/http.mjs +88 -0
  30. package/dist/index.d.mts +17 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.js +1929 -0
  33. package/dist/index.mjs +153 -0
  34. package/dist/memory.d.mts +14 -0
  35. package/dist/memory.d.ts +14 -0
  36. package/dist/memory.js +130 -0
  37. package/dist/memory.mjs +6 -0
  38. package/dist/portal.d.mts +445 -0
  39. package/dist/portal.d.ts +445 -0
  40. package/dist/portal.js +2927 -0
  41. package/dist/portal.mjs +2737 -0
  42. package/dist/product.mapper-CnY1ISOp.d.ts +97 -0
  43. package/dist/product.mapper-noVl-sel.d.mts +97 -0
  44. package/dist/products.d.mts +64 -0
  45. package/dist/products.d.ts +64 -0
  46. package/dist/products.js +370 -0
  47. package/dist/products.mjs +21 -0
  48. package/dist/react.d.mts +182 -0
  49. package/dist/react.d.ts +182 -0
  50. package/dist/react.js +1874 -0
  51. package/dist/react.mjs +105 -0
  52. package/dist/server.d.mts +177 -0
  53. package/dist/server.d.ts +177 -0
  54. package/dist/server.js +622 -0
  55. package/dist/server.mjs +417 -0
  56. package/dist/visual-tokens-BCLVR85G.d.mts +94 -0
  57. package/dist/visual-tokens-BCLVR85G.d.ts +94 -0
  58. package/package.json +122 -4
  59. package/src/core.ts +81 -0
  60. package/src/domain/entities/cancellation.entity.ts +28 -0
  61. package/src/domain/entities/checkout.entity.ts +21 -0
  62. package/src/domain/entities/index.ts +10 -0
  63. package/src/domain/entities/order.entity.ts +15 -0
  64. package/src/domain/entities/product.entity.ts +16 -0
  65. package/src/domain/entities/subscription.entity.ts +24 -0
  66. package/src/domain/entities/sync.entity.ts +4 -0
  67. package/src/domain/enums/amount-type.enum.ts +10 -0
  68. package/src/domain/enums/billing-cycle.enum.ts +30 -0
  69. package/src/domain/enums/index.ts +5 -0
  70. package/src/domain/enums/payment-provider.enum.ts +14 -0
  71. package/src/domain/enums/subscription-status.enum.ts +32 -0
  72. package/src/domain/index.ts +5 -0
  73. package/src/domain/interfaces/billing-adapter.interface.ts +18 -0
  74. package/src/domain/interfaces/index.ts +1 -0
  75. package/src/domain/mappers/product.mapper.ts +176 -0
  76. package/src/domain/utils/minor-units.ts +20 -0
  77. package/src/domain/utils/percent-discount.ts +17 -0
  78. package/src/http.ts +3 -0
  79. package/src/index.ts +12 -0
  80. package/src/infrastructure/browser/hosted-checkout.ts +26 -0
  81. package/src/infrastructure/config/billing-env.ts +31 -0
  82. package/src/infrastructure/constants/billing.constants.ts +10 -0
  83. package/src/infrastructure/http/http-client.ts +83 -0
  84. package/src/infrastructure/pagination/page-iterator.ts +43 -0
  85. package/src/infrastructure/query/query-builder.ts +26 -0
  86. package/src/infrastructure/services/billing.client.ts +300 -0
  87. package/src/infrastructure/services/create-billing-client.ts +23 -0
  88. package/src/infrastructure/services/http/create-http-adapter.ts +123 -0
  89. package/src/infrastructure/services/memory/create-memory-adapter.ts +126 -0
  90. package/src/infrastructure/services/product.repository.ts +198 -0
  91. package/src/infrastructure/utils/checkout-metadata.ts +55 -0
  92. package/src/infrastructure/utils/date-renderer.ts +53 -0
  93. package/src/infrastructure/utils/money-formatter.ts +39 -0
  94. package/src/infrastructure/utils/normalization.util.ts +22 -0
  95. package/src/infrastructure/utils/pagination-clamp.ts +11 -0
  96. package/src/infrastructure/utils/receipt.util.ts +6 -0
  97. package/src/infrastructure/utils/security.util.ts +23 -0
  98. package/src/infrastructure/utils/type-guards.util.ts +23 -0
  99. package/src/infrastructure/utils/validation.util.ts +10 -0
  100. package/src/infrastructure/webhook/parse-event.ts +135 -0
  101. package/src/infrastructure/webhook/verify-webhook.ts +124 -0
  102. package/src/memory.ts +3 -0
  103. package/src/portal.ts +2 -0
  104. package/src/presentation/components/BillingHistoryList.tsx +172 -0
  105. package/src/presentation/components/BillingProvider.tsx +179 -0
  106. package/src/presentation/components/CancelSubscriptionButton.tsx +155 -0
  107. package/src/presentation/components/CheckoutButton.tsx +76 -0
  108. package/src/presentation/components/CustomerPortalLink.tsx +71 -0
  109. package/src/presentation/components/PricingTable.tsx +225 -0
  110. package/src/presentation/components/SubscriptionStatusBadge.tsx +102 -0
  111. package/src/presentation/context/billing-context.ts +30 -0
  112. package/src/presentation/hooks/useBilling.ts +3 -0
  113. package/src/presentation/hooks/useCancelSubscription.ts +47 -0
  114. package/src/presentation/hooks/useCheckoutButton.ts +49 -0
  115. package/src/presentation/hooks/useProducts.ts +76 -0
  116. package/src/presentation/navigation/navigation.strategy.ts +16 -0
  117. package/src/presentation/portal/TurnkeyBillingPage.tsx +144 -0
  118. package/src/presentation/portal/components/BillingLayout.tsx +163 -0
  119. package/src/presentation/portal/components/BillingPage.tsx +22 -0
  120. package/src/presentation/portal/components/BillingPortal.tsx +248 -0
  121. package/src/presentation/portal/components/InvoiceCard.tsx +275 -0
  122. package/src/presentation/portal/components/OverviewTab.tsx +137 -0
  123. package/src/presentation/portal/components/PaymentMethodsList.tsx +268 -0
  124. package/src/presentation/portal/components/PlanComparison.tsx +405 -0
  125. package/src/presentation/portal/components/PlanTab.tsx +197 -0
  126. package/src/presentation/portal/components/PortalButton.tsx +89 -0
  127. package/src/presentation/portal/components/PortalSpinner.tsx +30 -0
  128. package/src/presentation/portal/components/UsageCard.tsx +158 -0
  129. package/src/presentation/portal/components/index.ts +11 -0
  130. package/src/presentation/portal/constants.ts +44 -0
  131. package/src/presentation/portal/create-portal-client.ts +202 -0
  132. package/src/presentation/portal/hooks/index.ts +3 -0
  133. package/src/presentation/portal/hooks/useBilling.ts +240 -0
  134. package/src/presentation/portal/index.ts +97 -0
  135. package/src/presentation/portal/labels.tsx +218 -0
  136. package/src/presentation/portal/styles.ts +70 -0
  137. package/src/presentation/portal/types.ts +213 -0
  138. package/src/presentation/portal/utils/billing.ts +193 -0
  139. package/src/presentation/portal/utils/index.ts +27 -0
  140. package/src/presentation/portal/utils/plans.ts +71 -0
  141. package/src/presentation/theme/visual-tokens.ts +103 -0
  142. package/src/presentation/utils/cancellation-reason-catalog.ts +18 -0
  143. package/src/presentation/utils/checkout-button-visual.ts +50 -0
  144. package/src/presentation/utils/inline-error-visual.ts +21 -0
  145. package/src/presentation/utils/product-pricing.ts +33 -0
  146. package/src/presentation/utils/status-label.ts +23 -0
  147. package/src/presentation/utils/status-tone.ts +25 -0
  148. package/src/products.ts +20 -0
  149. package/src/react.tsx +63 -0
  150. package/src/server.ts +79 -0
@@ -0,0 +1,47 @@
1
+
2
+ import { useCallback, useState } from 'react';
3
+ import type { CancellationReason, CancelResult } from '../../domain/entities/cancellation.entity';
4
+ import { useBilling } from '../context/billing-context';
5
+
6
+ export interface UseCancelSubscriptionOptions {
7
+ onError?: (error: Error) => void;
8
+ onSuccess?: (result: CancelResult) => void;
9
+ }
10
+
11
+ export interface UseCancelSubscriptionResult {
12
+ cancel: (reason?: CancellationReason) => Promise<CancelResult | null>;
13
+ loading: boolean;
14
+ error: Error | null;
15
+ }
16
+
17
+ export function useCancelSubscription(
18
+ options: UseCancelSubscriptionOptions = {},
19
+ ): UseCancelSubscriptionResult {
20
+ const { cancelSubscription } = useBilling();
21
+ const [loading, setLoading] = useState(false);
22
+ const [error, setError] = useState<Error | null>(null);
23
+
24
+ const { onSuccess, onError } = options;
25
+
26
+ const cancel = useCallback(
27
+ async (reason?: CancellationReason): Promise<CancelResult | null> => {
28
+ setLoading(true);
29
+ setError(null);
30
+ try {
31
+ const result = await cancelSubscription(reason);
32
+ onSuccess?.(result);
33
+ return result;
34
+ } catch (err) {
35
+ const normalized = err instanceof Error ? err : new Error(String(err));
36
+ setError(normalized);
37
+ onError?.(normalized);
38
+ return null;
39
+ } finally {
40
+ setLoading(false);
41
+ }
42
+ },
43
+ [cancelSubscription, onSuccess, onError],
44
+ );
45
+
46
+ return { cancel, loading, error };
47
+ }
@@ -0,0 +1,49 @@
1
+
2
+ import { useCallback, useState } from 'react';
3
+ import type { CheckoutParams } from '../../domain/entities/checkout.entity';
4
+ import { useBilling } from '../context/billing-context';
5
+
6
+ export interface UseCheckoutButtonOptions {
7
+ productId: string;
8
+ billingCycle?: CheckoutParams['billingCycle'];
9
+ successUrl?: string;
10
+ planKey?: string;
11
+ onError?: (error: Error) => void;
12
+ }
13
+
14
+ export interface UseCheckoutButtonResult {
15
+ start: () => Promise<void>;
16
+ loading: boolean;
17
+ error: Error | null;
18
+ }
19
+
20
+ export function useCheckoutButton(
21
+ options: UseCheckoutButtonOptions,
22
+ ): UseCheckoutButtonResult {
23
+ const { startCheckout } = useBilling();
24
+ const [loading, setLoading] = useState(false);
25
+ const [error, setError] = useState<Error | null>(null);
26
+
27
+ const { productId, billingCycle, successUrl, planKey, onError } = options;
28
+
29
+ const start = useCallback(async (): Promise<void> => {
30
+ setLoading(true);
31
+ setError(null);
32
+ try {
33
+ await startCheckout({
34
+ productId,
35
+ billingCycle,
36
+ successUrl,
37
+ planKey,
38
+ });
39
+ } catch (err) {
40
+ const normalized = err instanceof Error ? err : new Error(String(err));
41
+ setError(normalized);
42
+ onError?.(normalized);
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }, [startCheckout, productId, billingCycle, successUrl, planKey, onError]);
47
+
48
+ return { start, loading, error };
49
+ }
@@ -0,0 +1,76 @@
1
+
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { getAllProducts } from '../../infrastructure/services/product.repository';
4
+ import type { Product, ProductListParams } from '../../domain/entities/product.entity';
5
+
6
+ export interface UseProductsOptions {
7
+ apiToken: string;
8
+ organizationId?: string;
9
+ isRecurring?: boolean;
10
+ enabled?: boolean;
11
+ }
12
+
13
+ export interface UseProductsResult {
14
+ products: Product[];
15
+ loading: boolean;
16
+ error: Error | null;
17
+ refetch: () => Promise<void>;
18
+ }
19
+
20
+ export function useProducts(options: UseProductsOptions): UseProductsResult {
21
+ const { apiToken, organizationId, isRecurring = true, enabled = true } = options;
22
+
23
+ const [products, setProducts] = useState<Product[]>([]);
24
+ const [loading, setLoading] = useState(enabled);
25
+ const [error, setError] = useState<Error | null>(null);
26
+
27
+ const optionsRef = useRef({ apiToken, organizationId, isRecurring, enabled });
28
+ optionsRef.current = { apiToken, organizationId, isRecurring, enabled };
29
+
30
+ const abortRef = useRef<AbortController | null>(null);
31
+
32
+ const fetchProducts = useCallback(async (): Promise<void> => {
33
+ const { apiToken: token, organizationId: org, isRecurring: recurring, enabled: isEnabled } =
34
+ optionsRef.current;
35
+
36
+ abortRef.current?.abort();
37
+
38
+ if (!isEnabled) {
39
+ setLoading(false);
40
+ return;
41
+ }
42
+
43
+ const controller = new AbortController();
44
+ abortRef.current = controller;
45
+ setLoading(true);
46
+ setError(null);
47
+
48
+ try {
49
+ const params: ProductListParams = { isRecurring: recurring };
50
+ if (org) params.organizationId = org;
51
+
52
+ const data = await getAllProducts({ apiToken: token }, params, {
53
+ signal: controller.signal,
54
+ });
55
+
56
+ if (controller.signal.aborted) return;
57
+ setProducts(data);
58
+ } catch (err) {
59
+ if (controller.signal.aborted) return;
60
+ setError(err instanceof Error ? err : new Error('Failed to fetch products'));
61
+ } finally {
62
+ if (!controller.signal.aborted) {
63
+ setLoading(false);
64
+ }
65
+ }
66
+ }, []);
67
+
68
+ useEffect(() => {
69
+ void fetchProducts();
70
+ return () => {
71
+ abortRef.current?.abort();
72
+ };
73
+ }, [apiToken, organizationId, isRecurring, enabled, fetchProducts]);
74
+
75
+ return { products, loading, error, refetch: fetchProducts };
76
+ }
@@ -0,0 +1,16 @@
1
+
2
+ export interface NavigationStrategy {
3
+ redirect(url: string): void;
4
+ }
5
+
6
+ export class WindowNavigationStrategy implements NavigationStrategy {
7
+ redirect(url: string): void {
8
+ window.location.href = url;
9
+ }
10
+ }
11
+
12
+ export class NoopNavigationStrategy implements NavigationStrategy {
13
+ redirect(_url: string): void {
14
+ /* intentionally no-op */
15
+ }
16
+ }
@@ -0,0 +1,144 @@
1
+
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import type { BillingAdapter } from '../../domain/interfaces';
4
+ import type { BillingCycle } from '../../domain/entities';
5
+ import type { Product } from '../../domain/entities';
6
+ import { NoopNavigationStrategy } from '../navigation/navigation.strategy';
7
+ import type { NavigationStrategy } from '../navigation/navigation.strategy';
8
+ import { useBilling } from './hooks';
9
+ import { BillingPage, PlanComparison, PortalButton, PortalSpinner } from './components';
10
+ import { transformProducts } from './utils';
11
+ import type { BillingLabelsOverrides } from './labels';
12
+ import type { BillingConfig, PlanTier } from './types';
13
+ import { createPortalClient } from './create-portal-client';
14
+
15
+ export interface TurnkeyBillingPageProps {
16
+ adapter: BillingAdapter;
17
+ products: Product[];
18
+ config: Omit<BillingConfig, 'plans'> & { plans?: PlanTier[] };
19
+ userId?: string;
20
+ defaultCycle?: BillingCycle;
21
+ successUrl?: string;
22
+ navigation?: NavigationStrategy;
23
+ locale?: string;
24
+ labels?: BillingLabelsOverrides;
25
+ onError?: (error: Error) => void;
26
+ }
27
+
28
+ function browserNavigation(): NavigationStrategy {
29
+ if (typeof window === 'undefined') return new NoopNavigationStrategy();
30
+ return {
31
+ redirect(url: string): void {
32
+ window.location.href = url;
33
+ },
34
+ };
35
+ }
36
+
37
+ export function TurnkeyBillingPage({
38
+ adapter,
39
+ products,
40
+ config,
41
+ userId,
42
+ defaultCycle,
43
+ successUrl,
44
+ navigation,
45
+ locale,
46
+ labels,
47
+ onError,
48
+ }: TurnkeyBillingPageProps) {
49
+ const [cycle, setCycle] = useState<BillingCycle>(defaultCycle ?? 'monthly');
50
+ const [selecting, setSelecting] = useState(false);
51
+
52
+ const client = useMemo(
53
+ () =>
54
+ createPortalClient({
55
+ adapter,
56
+ products,
57
+ ...(userId !== undefined ? { userId } : {}),
58
+ ...(defaultCycle !== undefined ? { defaultCycle } : {}),
59
+ ...(successUrl !== undefined ? { successUrl } : {}),
60
+ navigation: navigation ?? browserNavigation(),
61
+ }),
62
+ [adapter, products, userId, defaultCycle, successUrl, navigation],
63
+ );
64
+
65
+ const { billing, isLoading, error, loadBilling, cancelSubscription } = useBilling({ apiClient: client });
66
+
67
+ useEffect(() => {
68
+ void loadBilling();
69
+ }, [loadBilling]);
70
+
71
+ const plans = config.plans ?? transformProducts(products);
72
+
73
+ const selectPlan = async (productId: string) => {
74
+ setSelecting(true);
75
+ try {
76
+ await client.selectPlan(productId, cycle);
77
+ } catch (err) {
78
+ onError?.(err instanceof Error ? err : new Error(String(err)));
79
+ } finally {
80
+ setSelecting(false);
81
+ }
82
+ };
83
+
84
+ const openPortalSafely = async () => {
85
+ try {
86
+ await client.openPortal();
87
+ } catch (err) {
88
+ onError?.(err instanceof Error ? err : new Error(String(err)));
89
+ }
90
+ };
91
+
92
+ const viewInvoice = async (invoiceId: string) => {
93
+ try {
94
+ await client.getInvoiceUrl(invoiceId);
95
+ } catch (err) {
96
+ onError?.(err instanceof Error ? err : new Error(String(err)));
97
+ }
98
+ };
99
+
100
+ const mergedConfig: BillingConfig = { ...config, plans };
101
+
102
+ return (
103
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 24, width: '100%' }}>
104
+ <PlanComparison
105
+ plans={plans}
106
+ selectedPlan={billing?.subscription.plan.id}
107
+ cycle={cycle}
108
+ onPlanSelect={(productId) => void selectPlan(productId)}
109
+ onCycleChange={setCycle}
110
+ loading={selecting}
111
+ labels={labels}
112
+ />
113
+
114
+ {billing ? (
115
+ <BillingPage
116
+ config={mergedConfig}
117
+ billing={billing}
118
+ loading={isLoading}
119
+ error={error ?? undefined}
120
+ locale={locale}
121
+ labels={labels}
122
+ onAddPaymentMethod={() => void openPortalSafely()}
123
+ onSetDefaultPaymentMethod={() => void openPortalSafely()}
124
+ onRemovePaymentMethod={() => void openPortalSafely()}
125
+ onViewInvoice={(invoice) => void viewInvoice(invoice.id)}
126
+ onCancelSubscription={() => cancelSubscription().then(() => loadBilling())}
127
+ />
128
+ ) : (
129
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12, padding: 32 }}>
130
+ {isLoading ? (
131
+ <PortalSpinner />
132
+ ) : error ? (
133
+ <>
134
+ <span>{error}</span>
135
+ <PortalButton onClick={() => void loadBilling()}>Retry</PortalButton>
136
+ </>
137
+ ) : (
138
+ <PortalSpinner />
139
+ )}
140
+ </div>
141
+ )}
142
+ </div>
143
+ );
144
+ }
@@ -0,0 +1,163 @@
1
+
2
+ import { VisualTokens } from '../../theme/visual-tokens';
3
+ import { useBillingLabels, formatLabel } from '../labels';
4
+ import type { BillingLayoutProps } from '../types';
5
+
6
+ const BRAND_LOGO_SIZE = 32;
7
+
8
+ const DefaultBrandMark = ({ initial }: { initial: string }) => (
9
+ <span
10
+ aria-hidden="true"
11
+ style={{
12
+ width: BRAND_LOGO_SIZE,
13
+ height: BRAND_LOGO_SIZE,
14
+ borderRadius: VisualTokens.radius.md,
15
+ background: VisualTokens.color.brandPrimary,
16
+ color: VisualTokens.color.textOnPrimary,
17
+ display: 'inline-flex',
18
+ alignItems: 'center',
19
+ justifyContent: 'center',
20
+ fontWeight: VisualTokens.fontWeight.strong,
21
+ fontSize: VisualTokens.fontSize.body,
22
+ flexShrink: 0,
23
+ }}
24
+ >
25
+ {initial}
26
+ </span>
27
+ );
28
+
29
+ const container = {
30
+ maxWidth: 1152,
31
+ marginLeft: 'auto',
32
+ marginRight: 'auto',
33
+ paddingLeft: VisualTokens.spacing.section,
34
+ paddingRight: VisualTokens.spacing.section,
35
+ } as const;
36
+
37
+ export const BillingLayout = ({ config, children, onBack }: BillingLayoutProps) => {
38
+ const labels = useBillingLabels();
39
+ const dashboardRoute = config.cancelRoute ?? '/dashboard';
40
+ const contactAria = formatLabel(labels.page.contactAria, { email: config.supportEmail ?? '' });
41
+
42
+ const backControl = () =>
43
+ onBack ? (
44
+ <button
45
+ type="button"
46
+ onClick={onBack}
47
+ style={{
48
+ fontSize: VisualTokens.fontSize.body,
49
+ color: VisualTokens.color.textMuted,
50
+ background: 'transparent',
51
+ border: 'none',
52
+ padding: 0,
53
+ cursor: 'pointer',
54
+ }}
55
+ >
56
+ {labels.page.backToDashboard}
57
+ </button>
58
+ ) : (
59
+ <a
60
+ href={dashboardRoute}
61
+ style={{
62
+ fontSize: VisualTokens.fontSize.body,
63
+ color: VisualTokens.color.textMuted,
64
+ textDecoration: 'none',
65
+ }}
66
+ >
67
+ {labels.page.backToDashboard}
68
+ </a>
69
+ );
70
+
71
+ return (
72
+ <div
73
+ style={{
74
+ minHeight: '100vh',
75
+ background: VisualTokens.color.surfaceMuted,
76
+ display: 'flex',
77
+ flexDirection: 'column',
78
+ }}
79
+ >
80
+ <header
81
+ style={{
82
+ borderBottom: `1px solid ${VisualTokens.color.borderSubtle}`,
83
+ padding: `${VisualTokens.spacing.xl}px ${VisualTokens.spacing.section}px`,
84
+ display: 'flex',
85
+ alignItems: 'center',
86
+ justifyContent: 'space-between',
87
+ background: VisualTokens.color.surfacePrimary,
88
+ }}
89
+ >
90
+ <a
91
+ href={dashboardRoute}
92
+ onClick={onBack ? (e) => { e.preventDefault(); onBack(); } : undefined}
93
+ style={{ display: 'flex', alignItems: 'center', gap: VisualTokens.spacing.sm, textDecoration: 'none' }}
94
+ aria-label={config.brandName}
95
+ >
96
+ {config.logo ?? <DefaultBrandMark initial={config.brandName.charAt(0).toUpperCase() || '?'} />}
97
+ <span
98
+ style={{
99
+ fontWeight: VisualTokens.fontWeight.strong,
100
+ fontSize: VisualTokens.fontSize.title,
101
+ color: VisualTokens.color.textPrimary,
102
+ }}
103
+ >
104
+ {config.brandName}
105
+ </span>
106
+ </a>
107
+ {backControl()}
108
+ </header>
109
+
110
+ <main style={{ flex: 1, ...container, paddingTop: 48, paddingBottom: 48, width: '100%', boxSizing: 'border-box' }}>
111
+ <div style={{ marginBottom: VisualTokens.spacing.page * 1.5 }}>
112
+ <h1
113
+ style={{
114
+ fontSize: 32,
115
+ fontWeight: VisualTokens.fontWeight.strong,
116
+ color: VisualTokens.color.textPrimary,
117
+ margin: 0,
118
+ marginBottom: VisualTokens.spacing.sm,
119
+ }}
120
+ >
121
+ {labels.page.title}
122
+ </h1>
123
+ <p style={{ color: VisualTokens.color.textMuted, margin: 0 }}>
124
+ {labels.page.description}
125
+ </p>
126
+ </div>
127
+
128
+ {children}
129
+ </main>
130
+
131
+ {config.supportEmail && (
132
+ <footer
133
+ style={{
134
+ borderTop: `1px solid ${VisualTokens.color.borderSubtle}`,
135
+ padding: `${VisualTokens.spacing.xl}px ${VisualTokens.spacing.section}px`,
136
+ background: VisualTokens.color.surfacePrimary,
137
+ }}
138
+ >
139
+ <div style={{ ...container, textAlign: 'center' }}>
140
+ <p
141
+ style={{
142
+ fontSize: VisualTokens.fontSize.body,
143
+ color: VisualTokens.color.textMuted,
144
+ margin: 0,
145
+ }}
146
+ >
147
+ {labels.page.needHelp}{' '}
148
+ <a
149
+ href={`mailto:${config.supportEmail}`}
150
+ style={{ color: VisualTokens.color.brandPrimary }}
151
+ aria-label={contactAria}
152
+ >
153
+ {config.supportEmail}
154
+ </a>
155
+ </p>
156
+ </div>
157
+ </footer>
158
+ )}
159
+ </div>
160
+ );
161
+ };
162
+
163
+ export default BillingLayout;
@@ -0,0 +1,22 @@
1
+
2
+ import { BillingLayout, BillingPortal } from '.';
3
+ import type { BillingConfig, BillingPageProps } from '../types';
4
+ import { DEFAULT_BILLING_CONFIG } from '../constants';
5
+ import { BillingLabelsProvider } from '../labels';
6
+
7
+ export const BillingPage = ({ config, labels, ...portalProps }: BillingPageProps) => {
8
+ const mergedConfig: BillingConfig = {
9
+ ...DEFAULT_BILLING_CONFIG,
10
+ ...config,
11
+ };
12
+
13
+ return (
14
+ <BillingLabelsProvider labels={labels}>
15
+ <BillingLayout config={mergedConfig}>
16
+ <BillingPortal showTabs {...portalProps} />
17
+ </BillingLayout>
18
+ </BillingLabelsProvider>
19
+ );
20
+ };
21
+
22
+ export default BillingPage;