@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,197 @@
1
+
2
+ import { useState } from 'react';
3
+ import { Calendar, CheckCircle2, XCircle, AlertTriangle } from 'lucide-react';
4
+ import { VisualTokens, TONE_PAIRS } from '../../theme/visual-tokens';
5
+ import { useBillingLabels } from '../labels';
6
+ import type { Subscription } from '../types';
7
+ import { PortalButton } from './PortalButton';
8
+ import { alertDanger, card, sectionTitle, stack } from '../styles';
9
+ import {
10
+ formatPrice,
11
+ getPlanPrice,
12
+ getStatusTone,
13
+ } from '../utils/billing';
14
+
15
+ export interface PlanTabProps {
16
+ subscription: Subscription;
17
+ onChangePlan?: () => void;
18
+ onCancelSubscription?: () => void | Promise<void>;
19
+ locale?: string;
20
+ }
21
+
22
+ export const PlanTab = ({
23
+ subscription,
24
+ onChangePlan,
25
+ onCancelSubscription,
26
+ locale = 'en-US',
27
+ }: PlanTabProps) => {
28
+ const labels = useBillingLabels();
29
+ const [confirmingCancel, setConfirmingCancel] = useState(false);
30
+ const [cancelling, setCancelling] = useState(false);
31
+ const [cancelError, setCancelError] = useState<string | null>(null);
32
+
33
+ const handleCancel = async () => {
34
+ if (!onCancelSubscription) return;
35
+ if (!confirmingCancel) {
36
+ setConfirmingCancel(true);
37
+ return;
38
+ }
39
+ setCancelling(true);
40
+ setCancelError(null);
41
+ try {
42
+ await onCancelSubscription();
43
+ } catch {
44
+ setCancelError(labels.plan.cancelFailed);
45
+ } finally {
46
+ setCancelling(false);
47
+ setConfirmingCancel(false);
48
+ }
49
+ };
50
+
51
+ const isActive = subscription.status === 'active' || subscription.status === 'trialing';
52
+ const statusTone = getStatusTone(subscription.status);
53
+
54
+ return (
55
+ <div style={stack}>
56
+ <section style={card} aria-labelledby="plan-current-heading">
57
+ <h3 id="plan-current-heading" style={sectionTitle}>
58
+ {labels.plan.title}
59
+ </h3>
60
+
61
+ <div
62
+ style={{
63
+ display: 'flex',
64
+ alignItems: 'flex-start',
65
+ justifyContent: 'space-between',
66
+ gap: VisualTokens.spacing.xl,
67
+ flexWrap: 'wrap',
68
+ }}
69
+ >
70
+ <div>
71
+ <p
72
+ style={{
73
+ fontSize: VisualTokens.fontSize.display,
74
+ fontWeight: VisualTokens.fontWeight.strong,
75
+ color: VisualTokens.color.textPrimary,
76
+ margin: 0,
77
+ }}
78
+ >
79
+ {subscription.plan.name}
80
+ </p>
81
+ <p
82
+ style={{
83
+ fontSize: VisualTokens.fontSize.body,
84
+ fontWeight: VisualTokens.fontWeight.medium,
85
+ color: TONE_PAIRS[statusTone].fg,
86
+ margin: 0,
87
+ marginTop: VisualTokens.spacing.xs,
88
+ }}
89
+ >
90
+ {labels.status[subscription.status]}
91
+ </p>
92
+ <p
93
+ style={{
94
+ fontSize: VisualTokens.fontSize.title,
95
+ fontWeight: VisualTokens.fontWeight.strong,
96
+ color: VisualTokens.color.textPrimary,
97
+ margin: 0,
98
+ marginTop: VisualTokens.spacing.lg,
99
+ }}
100
+ >
101
+ {formatPrice(
102
+ getPlanPrice(subscription.plan, subscription.cycle),
103
+ subscription.plan.currency,
104
+ )}
105
+ <span
106
+ style={{
107
+ fontSize: VisualTokens.fontSize.body,
108
+ fontWeight: VisualTokens.fontWeight.regular,
109
+ color: VisualTokens.color.textMuted,
110
+ }}
111
+ >
112
+ /{subscription.cycle}
113
+ </span>
114
+ </p>
115
+ <p
116
+ style={{
117
+ display: 'flex',
118
+ alignItems: 'center',
119
+ gap: 6,
120
+ fontSize: VisualTokens.fontSize.caption,
121
+ color: VisualTokens.color.textMuted,
122
+ margin: 0,
123
+ marginTop: VisualTokens.spacing.sm,
124
+ }}
125
+ >
126
+ <Calendar size={14} aria-hidden="true" />
127
+ {subscription.currentPeriodStart &&
128
+ new Date(subscription.currentPeriodStart).toLocaleDateString(locale)}
129
+ {' → '}
130
+ {subscription.currentPeriodEnd &&
131
+ new Date(subscription.currentPeriodEnd).toLocaleDateString(locale)}
132
+ </p>
133
+ </div>
134
+
135
+ {isActive ? (
136
+ <CheckCircle2
137
+ size={24}
138
+ color={TONE_PAIRS.active.fg}
139
+ aria-hidden="true"
140
+ />
141
+ ) : (
142
+ <XCircle size={24} color={VisualTokens.color.brandDanger} aria-hidden="true" />
143
+ )}
144
+ </div>
145
+ </section>
146
+
147
+ <section style={card} aria-labelledby="plan-actions-heading">
148
+ <h4
149
+ id="plan-actions-heading"
150
+ style={{
151
+ fontSize: VisualTokens.fontSize.body,
152
+ fontWeight: VisualTokens.fontWeight.strong,
153
+ color: VisualTokens.color.textPrimary,
154
+ margin: 0,
155
+ marginBottom: VisualTokens.spacing.xl,
156
+ }}
157
+ >
158
+ {labels.plan.actionsTitle}
159
+ </h4>
160
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: VisualTokens.spacing.lg }}>
161
+ {onChangePlan && (
162
+ <PortalButton onClick={onChangePlan} variant="default">
163
+ {labels.plan.changePlan}
164
+ </PortalButton>
165
+ )}
166
+ {onCancelSubscription && isActive && (
167
+ <PortalButton
168
+ onClick={handleCancel}
169
+ variant={confirmingCancel ? 'destructive' : 'outline'}
170
+ disabled={cancelling}
171
+ >
172
+ {cancelling
173
+ ? labels.plan.cancelling
174
+ : confirmingCancel
175
+ ? labels.plan.confirmCancel
176
+ : labels.plan.cancelSubscription}
177
+ </PortalButton>
178
+ )}
179
+ </div>
180
+
181
+ {confirmingCancel && !cancelError && (
182
+ <div role="alert" style={{ ...alertDanger, marginTop: VisualTokens.spacing.xl }}>
183
+ <AlertTriangle size={16} style={{ marginTop: 2, flexShrink: 0 }} aria-hidden="true" />
184
+ <span>{labels.plan.cancelConfirmMessage}</span>
185
+ </div>
186
+ )}
187
+
188
+ {cancelError && (
189
+ <div role="alert" style={{ ...alertDanger, marginTop: VisualTokens.spacing.xl }}>
190
+ <AlertTriangle size={16} style={{ marginTop: 2, flexShrink: 0 }} aria-hidden="true" />
191
+ <span>{cancelError}</span>
192
+ </div>
193
+ )}
194
+ </section>
195
+ </div>
196
+ );
197
+ };
@@ -0,0 +1,89 @@
1
+
2
+ import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from 'react';
3
+ import { VisualTokens } from '../../theme/visual-tokens';
4
+
5
+ export type PortalButtonVariant = 'default' | 'outline' | 'destructive' | 'ghost';
6
+ export type PortalButtonSize = 'sm' | 'md';
7
+
8
+ export interface PortalButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
9
+ variant?: PortalButtonVariant;
10
+ size?: PortalButtonSize;
11
+ fullWidth?: boolean;
12
+ pill?: boolean;
13
+ icon?: ReactNode;
14
+ children?: ReactNode;
15
+ }
16
+
17
+ const VARIANT_STYLES: Record<PortalButtonVariant, CSSProperties> = {
18
+ default: {
19
+ background: VisualTokens.color.brandPrimary,
20
+ color: VisualTokens.color.textOnPrimary,
21
+ border: '1px solid transparent',
22
+ },
23
+ outline: {
24
+ background: 'transparent',
25
+ color: VisualTokens.color.textPrimary,
26
+ border: `1px solid ${VisualTokens.color.borderStrong}`,
27
+ },
28
+ destructive: {
29
+ background: VisualTokens.color.brandDanger,
30
+ color: VisualTokens.color.textOnError,
31
+ border: '1px solid transparent',
32
+ },
33
+ ghost: {
34
+ background: 'transparent',
35
+ color: VisualTokens.color.textMuted,
36
+ border: '1px solid transparent',
37
+ },
38
+ };
39
+
40
+ const SIZE_STYLES: Record<PortalButtonSize, CSSProperties> = {
41
+ sm: {
42
+ padding: `${VisualTokens.spacing.xs}px ${VisualTokens.spacing.lg}px`,
43
+ fontSize: VisualTokens.fontSize.caption,
44
+ },
45
+ md: {
46
+ padding: `${VisualTokens.spacing.md}px ${VisualTokens.spacing.xxl}px`,
47
+ fontSize: VisualTokens.fontSize.body,
48
+ },
49
+ };
50
+
51
+ export const PortalButton = ({
52
+ variant = 'default',
53
+ size = 'md',
54
+ fullWidth = false,
55
+ pill = false,
56
+ icon,
57
+ children,
58
+ disabled,
59
+ style,
60
+ type = 'button',
61
+ ...rest
62
+ }: PortalButtonProps) => {
63
+ return (
64
+ <button
65
+ type={type}
66
+ disabled={disabled}
67
+ style={{
68
+ display: 'inline-flex',
69
+ alignItems: 'center',
70
+ justifyContent: 'center',
71
+ gap: VisualTokens.spacing.sm,
72
+ borderRadius: pill ? VisualTokens.radius.pill : VisualTokens.radius.md,
73
+ fontWeight: VisualTokens.fontWeight.medium,
74
+ lineHeight: VisualTokens.lineHeight.normal,
75
+ cursor: disabled ? 'not-allowed' : 'pointer',
76
+ opacity: disabled ? VisualTokens.opacity.disabled : 1,
77
+ width: fullWidth ? '100%' : undefined,
78
+ boxSizing: 'border-box',
79
+ ...VARIANT_STYLES[variant],
80
+ ...SIZE_STYLES[size],
81
+ ...style,
82
+ }}
83
+ {...rest}
84
+ >
85
+ {icon}
86
+ {children}
87
+ </button>
88
+ );
89
+ };
@@ -0,0 +1,30 @@
1
+
2
+ export const PortalSpinner = ({ size = 24 }: { size?: number }) => (
3
+ <svg
4
+ width={size}
5
+ height={size}
6
+ viewBox="0 0 24 24"
7
+ fill="none"
8
+ aria-hidden="true"
9
+ style={{ display: 'inline-block', verticalAlign: 'middle' }}
10
+ >
11
+ <circle
12
+ cx="12"
13
+ cy="12"
14
+ r="10"
15
+ stroke="currentColor"
16
+ strokeWidth="2.5"
17
+ strokeLinecap="round"
18
+ strokeDasharray="44 19"
19
+ >
20
+ <animateTransform
21
+ attributeName="transform"
22
+ type="rotate"
23
+ from="0 12 12"
24
+ to="360 12 12"
25
+ dur="0.8s"
26
+ repeatCount="indefinite"
27
+ />
28
+ </circle>
29
+ </svg>
30
+ );
@@ -0,0 +1,158 @@
1
+
2
+ import { AlertTriangle, TrendingUp } from 'lucide-react';
3
+ import { VisualTokens, TONE_PAIRS } from '../../theme/visual-tokens';
4
+ import { useBillingLabels } from '../labels';
5
+ import type { UsageCardProps } from '../types';
6
+ import { card, mutedText, row } from '../styles';
7
+ import { calculateUsagePercentage, formatNumber, isNearLimit } from '../utils/billing';
8
+
9
+ export const UsageCard = ({
10
+ metric,
11
+ locale = 'en-US',
12
+ showProgress = true,
13
+ showLimit = true,
14
+ }: UsageCardProps) => {
15
+ const labels = useBillingLabels();
16
+ const { percentage, isOverLimit, nearLimit, isUnlimited } = evaluateUsage(metric);
17
+
18
+ const tone = isOverLimit ? 'danger' : nearLimit ? 'warning' : 'active';
19
+ const toneColor = TONE_PAIRS[tone].fg;
20
+ const barColor = isOverLimit
21
+ ? VisualTokens.color.brandDanger
22
+ : nearLimit
23
+ ? VisualTokens.color.badgeWarningFg
24
+ : VisualTokens.color.brandPrimary;
25
+
26
+ return (
27
+ <div style={{ ...card, display: 'flex', flexDirection: 'column', gap: VisualTokens.spacing.lg }}>
28
+ <div style={{ ...row, justifyContent: 'space-between', gap: VisualTokens.spacing.lg }}>
29
+ <div>
30
+ <p style={{ ...mutedText, fontSize: VisualTokens.fontSize.caption, margin: 0, marginBottom: VisualTokens.spacing.xs }}>
31
+ {metric.name}
32
+ </p>
33
+ <p
34
+ style={{
35
+ fontSize: VisualTokens.fontSize.display,
36
+ fontWeight: VisualTokens.fontWeight.strong,
37
+ color: VisualTokens.color.textPrimary,
38
+ margin: 0,
39
+ }}
40
+ >
41
+ {formatNumber(metric.current, locale)}
42
+ <span
43
+ style={{
44
+ fontSize: VisualTokens.fontSize.body,
45
+ fontWeight: VisualTokens.fontWeight.regular,
46
+ color: VisualTokens.color.textMuted,
47
+ marginLeft: VisualTokens.spacing.xs,
48
+ }}
49
+ >
50
+ {metric.unit}
51
+ </span>
52
+ </p>
53
+ </div>
54
+
55
+ {(nearLimit || isOverLimit) && (
56
+ <div
57
+ style={{
58
+ width: 40,
59
+ height: 40,
60
+ borderRadius: VisualTokens.radius.md,
61
+ display: 'flex',
62
+ alignItems: 'center',
63
+ justifyContent: 'center',
64
+ color: toneColor,
65
+ background: TONE_PAIRS[tone].bg,
66
+ flexShrink: 0,
67
+ }}
68
+ aria-hidden="true"
69
+ >
70
+ {isOverLimit ? (
71
+ <AlertTriangle size={20} />
72
+ ) : (
73
+ <TrendingUp size={20} />
74
+ )}
75
+ </div>
76
+ )}
77
+ </div>
78
+
79
+ {showProgress && !isUnlimited && (
80
+ <div style={{ display: 'flex', flexDirection: 'column', gap: VisualTokens.spacing.sm }}>
81
+ <div
82
+ style={{
83
+ ...row,
84
+ justifyContent: 'space-between',
85
+ fontSize: VisualTokens.fontSize.caption,
86
+ }}
87
+ >
88
+ {showLimit && (
89
+ <span style={{ color: VisualTokens.color.textMuted }}>
90
+ {formatNumber(metric.limit, locale)} {metric.unit} {labels.usage.limit}
91
+ </span>
92
+ )}
93
+ <span
94
+ style={{
95
+ fontWeight: VisualTokens.fontWeight.medium,
96
+ color: isOverLimit || nearLimit ? toneColor : VisualTokens.color.textPrimary,
97
+ }}
98
+ >
99
+ {percentage.toFixed(0)}%
100
+ </span>
101
+ </div>
102
+
103
+ <div
104
+ style={{
105
+ width: '100%',
106
+ height: 8,
107
+ background: VisualTokens.color.surfaceSubtle,
108
+ borderRadius: VisualTokens.radius.pill,
109
+ overflow: 'hidden',
110
+ }}
111
+ role="progressbar"
112
+ aria-label={`${metric.name} ${labels.common.usage}`}
113
+ aria-valuemin={0}
114
+ aria-valuemax={Math.max(metric.limit, metric.current)}
115
+ aria-valuenow={metric.current}
116
+ aria-valuetext={`${formatNumber(metric.current, locale)} / ${formatNumber(metric.limit, locale)} ${metric.unit}`}
117
+ >
118
+ <div
119
+ style={{
120
+ height: '100%',
121
+ background: barColor,
122
+ width: `${Math.min(percentage, 100)}%`,
123
+ }}
124
+ />
125
+ </div>
126
+
127
+ {metric.resetAt && (
128
+ <p style={{ ...mutedText, fontSize: VisualTokens.fontSize.caption, margin: 0 }}>
129
+ {labels.usage.resets}{' '}
130
+ {new Date(metric.resetAt).toLocaleDateString(locale)}
131
+ </p>
132
+ )}
133
+ </div>
134
+ )}
135
+ </div>
136
+ );
137
+ };
138
+
139
+ interface UsageEvaluation {
140
+ percentage: number;
141
+ isOverLimit: boolean;
142
+ nearLimit: boolean;
143
+ isUnlimited: boolean;
144
+ }
145
+
146
+ const evaluateUsage = (metric: UsageCardProps['metric']): UsageEvaluation => {
147
+ if (metric.limit <= 0) {
148
+ return { percentage: 0, isOverLimit: false, nearLimit: false, isUnlimited: true };
149
+ }
150
+ return {
151
+ percentage: calculateUsagePercentage(metric),
152
+ isOverLimit: metric.current > metric.limit,
153
+ nearLimit: isNearLimit(metric),
154
+ isUnlimited: false,
155
+ };
156
+ };
157
+
158
+ export default UsageCard;
@@ -0,0 +1,11 @@
1
+
2
+ export { BillingPortal } from './BillingPortal';
3
+ export { BillingLayout } from './BillingLayout';
4
+ export { BillingPage } from './BillingPage';
5
+ export { PlanComparison, RADIO_GROUP_NAME } from './PlanComparison';
6
+ export { PaymentMethodsList } from './PaymentMethodsList';
7
+ export { InvoiceCard } from './InvoiceCard';
8
+ export { UsageCard } from './UsageCard';
9
+ export { PortalButton } from './PortalButton';
10
+ export type { PortalButtonProps, PortalButtonVariant, PortalButtonSize } from './PortalButton';
11
+ export { PortalSpinner } from './PortalSpinner';
@@ -0,0 +1,44 @@
1
+
2
+ export const SUBSCRIPTION_STATUS = {
3
+ ACTIVE: 'active',
4
+ TRIALING: 'trialing',
5
+ PAST_DUE: 'past_due',
6
+ CANCELED: 'canceled',
7
+ UNPAID: 'unpaid',
8
+ INCOMPLETE: 'incomplete',
9
+ REVOKED: 'revoked',
10
+ } as const;
11
+
12
+ export const INVOICE_STATUS = {
13
+ DRAFT: 'draft',
14
+ OPEN: 'open',
15
+ PAID: 'paid',
16
+ VOID: 'void',
17
+ UNCOLLECTIBLE: 'uncollectible',
18
+ REFUNDED: 'refunded',
19
+ } as const;
20
+
21
+ export const CURRENCY = {
22
+ USD: 'USD',
23
+ EUR: 'EUR',
24
+ GBP: 'GBP',
25
+ TRY: 'TRY',
26
+ JPY: 'JPY',
27
+ } as const;
28
+
29
+ export type Currency = typeof CURRENCY[keyof typeof CURRENCY];
30
+
31
+ export const PLAN_TYPE = {
32
+ STANDARD: 'standard',
33
+ PRO: 'pro',
34
+ BUSINESS: 'business',
35
+ ENTERPRISE: 'enterprise',
36
+ } as const;
37
+
38
+ export const DEFAULT_BILLING_CONFIG = {
39
+ enableYearlyDiscount: true,
40
+ yearlyDiscount: 20,
41
+ currencies: [CURRENCY.USD, CURRENCY.EUR, CURRENCY.GBP] as Currency[],
42
+ defaultCurrency: CURRENCY.USD,
43
+ taxRate: 0,
44
+ } as const;