@datalayer/core 1.0.20 → 1.0.22

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 (138) hide show
  1. package/README.md +27 -0
  2. package/lib/api/DatalayerApi.d.ts +5 -1
  3. package/lib/api/DatalayerApi.js +55 -8
  4. package/lib/api/iam/oauth2.d.ts +3 -3
  5. package/lib/api/iam/oauth2.js +2 -2
  6. package/lib/api/iam/profile.d.ts +9 -1
  7. package/lib/api/iam/profile.js +15 -0
  8. package/lib/api/runtimes/checkpoints.js +1 -1
  9. package/lib/api/runtimes/snapshots.d.ts +7 -7
  10. package/lib/api/runtimes/snapshots.js +9 -9
  11. package/lib/client/auth/storage.js +103 -12
  12. package/lib/client/index.d.ts +7 -7
  13. package/lib/client/index.js +1 -1
  14. package/lib/client/mixins/RuntimesMixin.d.ts +6 -6
  15. package/lib/client/mixins/RuntimesMixin.js +5 -5
  16. package/lib/client/mixins/SpacerMixin.js +22 -1
  17. package/lib/collaboration/DatalayerCollaborationProvider.d.ts +4 -3
  18. package/lib/components/avatars/DLAvatar.d.ts +24 -0
  19. package/lib/components/avatars/DLAvatar.js +26 -0
  20. package/lib/components/avatars/UserProfileAvatar.js +4 -4
  21. package/lib/components/avatars/index.d.ts +1 -0
  22. package/lib/components/avatars/index.js +1 -0
  23. package/lib/components/billing/BillableAccountSelect.d.ts +35 -0
  24. package/lib/components/billing/BillableAccountSelect.js +340 -0
  25. package/lib/components/billing/index.d.ts +1 -0
  26. package/lib/components/billing/index.js +5 -0
  27. package/lib/components/checkout/StripeCheckout.d.ts +24 -3
  28. package/lib/components/checkout/StripeCheckout.js +1116 -55
  29. package/lib/components/display/DatalayerBox.js +6 -1
  30. package/lib/components/index.d.ts +2 -0
  31. package/lib/components/index.js +2 -0
  32. package/lib/components/principal/Principal.d.ts +37 -0
  33. package/lib/components/principal/Principal.js +54 -0
  34. package/lib/components/principal/PrincipalAvatar.d.ts +10 -0
  35. package/lib/components/principal/PrincipalAvatar.js +55 -0
  36. package/lib/components/principal/PrincipalBadge.d.ts +20 -0
  37. package/lib/components/principal/PrincipalBadge.js +208 -0
  38. package/lib/components/principal/PrincipalBanner.d.ts +7 -0
  39. package/lib/components/principal/PrincipalBanner.js +84 -0
  40. package/lib/components/principal/PrincipalDetailsOverlay.d.ts +23 -0
  41. package/lib/components/principal/PrincipalDetailsOverlay.js +86 -0
  42. package/lib/components/principal/PrincipalSwitcherMenu.d.ts +14 -0
  43. package/lib/components/principal/PrincipalSwitcherMenu.js +287 -0
  44. package/lib/components/principal/index.d.ts +7 -0
  45. package/lib/components/principal/index.js +10 -0
  46. package/lib/components/runtimes/RuntimeLauncherDialog.d.ts +2 -2
  47. package/lib/components/sharing/ShareAccessComponent.d.ts +18 -0
  48. package/lib/components/sharing/ShareAccessComponent.js +1368 -0
  49. package/lib/components/sharing/SharingEditor.d.ts +20 -0
  50. package/lib/components/sharing/SharingEditor.js +65 -0
  51. package/lib/components/sharing/index.d.ts +9 -0
  52. package/lib/components/sharing/index.js +12 -0
  53. package/lib/components/snapshots/{RuntimeSnapshotMenu.d.ts → SandboxSnapshotMenu.d.ts} +4 -4
  54. package/lib/components/snapshots/{RuntimeSnapshotMenu.js → SandboxSnapshotMenu.js} +33 -33
  55. package/lib/components/snapshots/index.d.ts +1 -1
  56. package/lib/components/snapshots/index.js +1 -1
  57. package/lib/components/storage/ContentsBrowser.js +2 -2
  58. package/lib/components/tokens/SpaceVariantToken.d.ts +1 -1
  59. package/lib/components/tokens/SpaceVariantToken.js +7 -4
  60. package/lib/components/users/PeerIndicator.js +5 -4
  61. package/lib/hooks/index.d.ts +5 -0
  62. package/lib/hooks/index.js +5 -0
  63. package/lib/hooks/useBillableAccountStore.d.ts +31 -0
  64. package/lib/hooks/useBillableAccountStore.js +53 -0
  65. package/lib/hooks/useCache.d.ts +125 -18
  66. package/lib/hooks/useCache.js +893 -120
  67. package/lib/hooks/useDatalayer.js +31 -3
  68. package/lib/hooks/useIAM.js +1 -8
  69. package/lib/hooks/usePrincipalStore.d.ts +41 -0
  70. package/lib/hooks/usePrincipalStore.js +101 -0
  71. package/lib/hooks/useProjects.d.ts +44 -0
  72. package/lib/hooks/useProjects.js +26 -2
  73. package/lib/hooks/useSelectedBillableAccount.d.ts +14 -0
  74. package/lib/hooks/useSelectedBillableAccount.js +36 -0
  75. package/lib/hooks/useSelectedPrincipal.d.ts +17 -0
  76. package/lib/hooks/useSelectedPrincipal.js +29 -0
  77. package/lib/hooks/useUsageRefreshStore.d.ts +8 -0
  78. package/lib/hooks/useUsageRefreshStore.js +16 -0
  79. package/lib/index.d.ts +2 -2
  80. package/lib/index.js +1 -1
  81. package/lib/mocks/components/FlashMock.js +8 -2
  82. package/lib/models/{RuntimeSnapshot.d.ts → CodeSandboxSnapshot.d.ts} +5 -5
  83. package/lib/models/{RuntimeSnapshot.js → CodeSandboxSnapshot.js} +1 -1
  84. package/lib/models/{RuntimeSnapshotDTO.d.ts → CodeSandboxSnapshotDTO.d.ts} +30 -30
  85. package/lib/models/{RuntimeSnapshotDTO.js → CodeSandboxSnapshotDTO.js} +6 -6
  86. package/lib/models/Environment.d.ts +4 -0
  87. package/lib/models/IAM.d.ts +66 -0
  88. package/lib/models/Organization.d.ts +1 -0
  89. package/lib/models/Organization.js +1 -0
  90. package/lib/models/Page.d.ts +2 -2
  91. package/lib/models/Page.js +2 -2
  92. package/lib/models/Profile.d.ts +1 -1
  93. package/lib/models/RuntimeDTO.d.ts +2 -2
  94. package/lib/models/Space.d.ts +1 -0
  95. package/lib/models/Space.js +29 -5
  96. package/lib/models/SpaceDTO.d.ts +4 -0
  97. package/lib/models/SpaceDTO.js +5 -0
  98. package/lib/models/Usage.d.ts +15 -0
  99. package/lib/models/Usage.js +9 -0
  100. package/lib/models/User.d.ts +21 -0
  101. package/lib/models/User.js +6 -2
  102. package/lib/models/index.d.ts +2 -2
  103. package/lib/models/index.js +2 -2
  104. package/lib/otel/auth.js +0 -4
  105. package/lib/services/index.d.ts +1 -1
  106. package/lib/services/index.js +1 -1
  107. package/lib/state/substates/IAMState.js +0 -7
  108. package/lib/state/substates/RuntimesState.d.ts +9 -9
  109. package/lib/state/substates/RuntimesState.js +3 -3
  110. package/lib/stateful/runtimes/actions.d.ts +17 -17
  111. package/lib/stateful/runtimes/actions.js +35 -55
  112. package/lib/stateful/runtimes/apis.d.ts +3 -3
  113. package/lib/stateful/runtimes/snapshots.d.ts +2 -2
  114. package/lib/stateful/runtimes/snapshots.js +7 -7
  115. package/lib/utils/Format.d.ts +6 -0
  116. package/lib/utils/Format.js +17 -0
  117. package/lib/utils/Handles.d.ts +3 -0
  118. package/lib/utils/Handles.js +53 -0
  119. package/lib/utils/Snapshot.d.ts +1 -1
  120. package/lib/utils/Snapshot.js +1 -1
  121. package/lib/utils/index.d.ts +1 -0
  122. package/lib/utils/index.js +1 -0
  123. package/lib/views/datasources/DatasourceDetail.d.ts +7 -1
  124. package/lib/views/datasources/DatasourceDetail.js +61 -23
  125. package/lib/views/datasources/DatasourceNew.d.ts +8 -1
  126. package/lib/views/datasources/DatasourceNew.js +23 -10
  127. package/lib/views/datasources/Datasources.d.ts +5 -1
  128. package/lib/views/datasources/Datasources.js +38 -30
  129. package/lib/views/iam-tokens/IAMTokenNew.d.ts +3 -1
  130. package/lib/views/iam-tokens/IAMTokenNew.js +2 -2
  131. package/lib/views/iam-tokens/IAMTokens.d.ts +3 -1
  132. package/lib/views/iam-tokens/IAMTokens.js +2 -2
  133. package/lib/views/otel/OtelHeader.d.ts +5 -0
  134. package/lib/views/otel/OtelHeader.js +3 -3
  135. package/lib/views/profile/UserBadge.d.ts +7 -2
  136. package/lib/views/profile/UserBadge.js +11 -2
  137. package/lib/views/secrets/Secrets.js +21 -21
  138. package/package.json +7 -8
@@ -1,38 +1,221 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /*
3
3
  * Copyright (c) 2023-2025 Datalayer, Inc.
4
4
  * Distributed under the terms of the Modified BSD License.
5
5
  */
6
- import { createElement, useCallback, useEffect, useState } from 'react';
7
- import { Button, Flash, FormControl, Spinner, Text } from '@primer/react';
6
+ import { createElement, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
7
+ import { CardElement, Elements, useElements, useStripe, } from '@stripe/react-stripe-js';
8
+ import { Button, Flash, FormControl, Label, ProgressBar, Spinner, Text, useTheme, } from '@primer/react';
9
+ import { DotFillIcon } from '@primer/octicons-react';
8
10
  import { Box } from '@datalayer/primer-addons';
9
11
  import { useCache } from '../../hooks/index.js';
12
+ const PLAN_INCLUDED_RUNS_DEFAULTS = {
13
+ starter: 500,
14
+ free: 500,
15
+ team: 5000,
16
+ pro: 5000,
17
+ enterprise: 50000,
18
+ };
19
+ const asNumber = (value) => {
20
+ const parsed = Number(value);
21
+ return Number.isFinite(parsed) ? parsed : null;
22
+ };
23
+ const asPositiveNumber = (value) => {
24
+ const parsed = asNumber(value);
25
+ return parsed !== null && parsed > 0 ? parsed : null;
26
+ };
27
+ const asNonNegativeNumber = (value) => {
28
+ const parsed = asNumber(value);
29
+ return parsed !== null && parsed >= 0 ? parsed : null;
30
+ };
31
+ const buildUsageSegments = (used, total) => {
32
+ const safeUsed = Math.max(0, used);
33
+ const safeTotal = Math.max(0, total);
34
+ const denominator = Math.max(1, safeUsed, safeTotal);
35
+ const inQuota = Math.min(safeUsed, safeTotal);
36
+ const remaining = Math.max(0, safeTotal - safeUsed);
37
+ const over = Math.max(0, safeUsed - safeTotal);
38
+ return {
39
+ inQuotaPct: (inQuota / denominator) * 100,
40
+ remainingPct: (remaining / denominator) * 100,
41
+ overPct: (over / denominator) * 100,
42
+ };
43
+ };
44
+ function StripePaymentForm({ clientSecret, intentType = 'payment', onPaymentSucceeded, }) {
45
+ const stripe = useStripe();
46
+ const elements = useElements();
47
+ const { colorScheme, resolvedColorScheme } = useTheme();
48
+ const [isSubmitting, setIsSubmitting] = useState(false);
49
+ const [errorMessage, setErrorMessage] = useState(null);
50
+ const themeIsDark = useMemo(() => {
51
+ const scheme = (resolvedColorScheme || colorScheme || '').toLowerCase();
52
+ if (scheme.includes('dark')) {
53
+ return true;
54
+ }
55
+ if (scheme.includes('light')) {
56
+ return false;
57
+ }
58
+ return false;
59
+ }, [colorScheme, resolvedColorScheme]);
60
+ const cardOptions = useMemo(() => {
61
+ const rootStyles = typeof window !== 'undefined'
62
+ ? getComputedStyle(document.documentElement)
63
+ : null;
64
+ const detectDarkMode = () => {
65
+ if (themeIsDark) {
66
+ return true;
67
+ }
68
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
69
+ return false;
70
+ }
71
+ const candidates = [
72
+ document.documentElement,
73
+ document.body,
74
+ ];
75
+ for (const el of candidates) {
76
+ const mode = el?.getAttribute('data-color-mode');
77
+ if (mode === 'dark') {
78
+ return true;
79
+ }
80
+ if (mode === 'light') {
81
+ return false;
82
+ }
83
+ }
84
+ for (const el of candidates) {
85
+ const darkTheme = el?.getAttribute('data-dark-theme');
86
+ if (darkTheme &&
87
+ darkTheme !== 'light' &&
88
+ (el?.getAttribute('data-color-mode') || 'auto') === 'auto' &&
89
+ window.matchMedia('(prefers-color-scheme: dark)').matches) {
90
+ return true;
91
+ }
92
+ }
93
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
94
+ };
95
+ const isDarkMode = detectDarkMode();
96
+ const fontFamily = rootStyles?.getPropertyValue('--base-text-font-family')?.trim() ||
97
+ 'system-ui, -apple-system, Segoe UI, sans-serif';
98
+ // Stripe's CardElement renders in an iframe that does not inherit our CSS
99
+ // variables, so reading them from documentElement does not always reflect
100
+ // the active theme. Use explicit dark/light values so typed characters are
101
+ // always readable.
102
+ const baseColor = isDarkMode ? '#f0f6fc' : '#1f2328';
103
+ const mutedColor = isDarkMode ? '#8b949e' : '#59636e';
104
+ return {
105
+ style: {
106
+ base: {
107
+ color: baseColor,
108
+ iconColor: mutedColor,
109
+ fontFamily,
110
+ fontSize: '16px',
111
+ '::placeholder': {
112
+ color: mutedColor,
113
+ },
114
+ ':-webkit-autofill': {
115
+ color: baseColor,
116
+ },
117
+ },
118
+ invalid: {
119
+ color: '#d1242f',
120
+ iconColor: '#d1242f',
121
+ },
122
+ },
123
+ };
124
+ }, [themeIsDark]);
125
+ const handleSubmit = useCallback(async (event) => {
126
+ event.preventDefault();
127
+ if (!stripe || !elements) {
128
+ return;
129
+ }
130
+ const card = elements.getElement(CardElement);
131
+ if (!card) {
132
+ setErrorMessage('Payment form is not ready yet. Please try again.');
133
+ return;
134
+ }
135
+ setIsSubmitting(true);
136
+ setErrorMessage(null);
137
+ if (intentType === 'setup') {
138
+ const result = await stripe.confirmCardSetup(clientSecret, {
139
+ payment_method: {
140
+ card,
141
+ },
142
+ });
143
+ if (result.error) {
144
+ setErrorMessage(result.error.message || 'Payment failed. Please try again.');
145
+ setIsSubmitting(false);
146
+ return;
147
+ }
148
+ if (result.setupIntent?.status === 'succeeded') {
149
+ onPaymentSucceeded();
150
+ }
151
+ }
152
+ else {
153
+ const result = await stripe.confirmCardPayment(clientSecret, {
154
+ payment_method: {
155
+ card,
156
+ },
157
+ });
158
+ if (result.error) {
159
+ setErrorMessage(result.error.message || 'Payment failed. Please try again.');
160
+ setIsSubmitting(false);
161
+ return;
162
+ }
163
+ if (result.paymentIntent?.status === 'succeeded') {
164
+ onPaymentSucceeded();
165
+ }
166
+ }
167
+ setIsSubmitting(false);
168
+ }, [clientSecret, elements, onPaymentSucceeded, stripe]);
169
+ return (_jsxs(Box, { as: "form", onSubmit: handleSubmit, sx: { display: 'grid', gap: 3 }, children: [_jsx(CardElement, { options: cardOptions, onReady: element => {
170
+ try {
171
+ element.focus();
172
+ }
173
+ catch {
174
+ // no-op: focus may fail if the element is unmounted.
175
+ }
176
+ } }), errorMessage && _jsx(Flash, { variant: "danger", children: errorMessage }), _jsx(Button, { variant: "primary", type: "submit", disabled: !stripe || isSubmitting, children: isSubmitting ? 'Processing payment...' : 'Pay now' })] }));
177
+ }
10
178
  /**
11
179
  * Stripe checkout.
12
180
  */
13
- export function StripeCheckout({ checkoutPortal, }) {
14
- const { useCreateCheckoutSession, useStripePrices } = useCache();
181
+ export function StripeCheckout({ checkoutPortal, appearance, accountUid, showStatusUsageSummary = false, onCheckoutSuccess, }) {
182
+ const { useCreateTopUpPaymentIntent, useCreateSubscriptionPaymentIntent, useSubscriptionPlans, useTopUpPrices, useSubscriptionStatus, useCancelSubscription, useResumeSubscription, } = useCache();
15
183
  const [stripe, setStripe] = useState(null);
16
- const [components, setComponents] = useState(null);
17
- const [items, setItems] = useState(null);
184
+ const [paymentClientSecret, setPaymentClientSecret] = useState(null);
18
185
  const [product, setProduct] = useState(null);
186
+ const [subscriptionPlan, setSubscriptionPlan] = useState(null);
19
187
  const [checkout, setCheckout] = useState(false);
188
+ const [checkoutType, setCheckoutType] = useState('topup');
189
+ const [cancelViewOpen, setCancelViewOpen] = useState(false);
190
+ const [isConfirmingCancel, setIsConfirmingCancel] = useState(false);
191
+ const [isResumingTransition, setIsResumingTransition] = useState(false);
192
+ const [paymentMessage, setPaymentMessage] = useState(null);
193
+ const [resumeConfirmationMessage, setResumeConfirmationMessage] = useState(null);
194
+ const [isReturningFromCheckout, setIsReturningFromCheckout] = useState(false);
195
+ const [topUpConfirmation, setTopUpConfirmation] = useState(null);
196
+ const [pendingTopUpTarget, setPendingTopUpTarget] = useState(null);
197
+ const topUpPurchaseRef = useRef(null);
20
198
  // Get Stripe prices using TanStack Query hook
21
- const { data: pricesData } = useStripePrices();
22
- // Update items when prices data changes
23
- useEffect(() => {
24
- if (pricesData) {
25
- setItems(pricesData);
199
+ const { data: pricesData, isPending: isTopUpPricesPending, isError: isTopUpPricesError, error: topUpPricesError, } = useTopUpPrices();
200
+ const items = useMemo(() => {
201
+ if (Array.isArray(pricesData)) {
202
+ return pricesData;
26
203
  }
204
+ return [];
27
205
  }, [pricesData]);
206
+ const sortedTopUpItems = useMemo(() => [...(items ?? [])].sort((left, right) => Number(left.amount || 0) - Number(right.amount || 0)), [items]);
207
+ const { data: plansData } = useSubscriptionPlans({ accountUid });
208
+ const plans = useMemo(() => plansData ?? [], [plansData]);
209
+ const { data: subscriptionResp, refetch: refetchSubscriptionStatus, isFetching: isSubscriptionStatusRefreshing, } = useSubscriptionStatus({ accountUid });
210
+ const cancelSubscriptionMutation = useCancelSubscription({ accountUid });
211
+ const resumeSubscriptionMutation = useResumeSubscription({ accountUid });
28
212
  // Get checkout session mutation
29
- const checkoutSessionMutation = useCreateCheckoutSession();
30
- // Load stripe components.
31
- useEffect(() => {
32
- import('@stripe/react-stripe-js').then(module => {
33
- setComponents(module);
34
- });
35
- }, []);
213
+ const topUpPaymentIntentMutation = useCreateTopUpPaymentIntent({
214
+ accountUid,
215
+ });
216
+ const subscriptionPaymentIntentMutation = useCreateSubscriptionPaymentIntent({
217
+ accountUid,
218
+ });
36
219
  // Load stripe API
37
220
  useEffect(() => {
38
221
  if (checkoutPortal?.metadata?.stripe_key) {
@@ -41,51 +224,929 @@ export function StripeCheckout({ checkoutPortal, }) {
41
224
  });
42
225
  }
43
226
  }, [checkoutPortal?.metadata?.stripe_key]);
44
- const fetchClientSecret = useCallback(async () => {
45
- const location = document.location;
46
- // Create a Checkout Session using TanStack Query mutation
47
- const result = await checkoutSessionMutation.mutateAsync({
48
- product,
49
- location,
50
- });
51
- return result;
52
- }, [checkoutSessionMutation, product]);
53
- const options = { fetchClientSecret };
227
+ const paymentOptions = useMemo(() => {
228
+ if (!paymentClientSecret) {
229
+ return null;
230
+ }
231
+ return {
232
+ clientSecret: paymentClientSecret,
233
+ ...(appearance ? { appearance } : {}),
234
+ };
235
+ }, [appearance, paymentClientSecret]);
236
+ const elementsAppearanceKey = useMemo(() => {
237
+ if (!appearance) {
238
+ return 'default';
239
+ }
240
+ try {
241
+ return JSON.stringify(appearance);
242
+ }
243
+ catch {
244
+ return String(appearance?.theme ?? 'default');
245
+ }
246
+ }, [appearance]);
247
+ const onPaymentSucceeded = useCallback(async () => {
248
+ setCheckout(false);
249
+ setPaymentClientSecret(null);
250
+ setProduct(null);
251
+ setSubscriptionPlan(null);
252
+ setPaymentMessage(null);
253
+ setIsReturningFromCheckout(true);
254
+ if (checkoutType === 'resume') {
255
+ try {
256
+ const resp = await resumeSubscriptionMutation.mutateAsync();
257
+ setPaymentMessage(resp?.message || 'Payment confirmed and plan resumed successfully.');
258
+ onCheckoutSuccess?.({ checkoutType: 'resume' });
259
+ }
260
+ catch (error) {
261
+ setPaymentMessage(error instanceof Error
262
+ ? error.message
263
+ : 'Payment confirmed, but unable to resume your plan right now.');
264
+ }
265
+ setIsReturningFromCheckout(false);
266
+ return;
267
+ }
268
+ if (checkoutType === 'subscription') {
269
+ // Poll plan status briefly so the UI flips to paid state as soon
270
+ // as Stripe + backend snapshot are ready.
271
+ for (let attempt = 0; attempt < 5; attempt += 1) {
272
+ try {
273
+ await refetchSubscriptionStatus();
274
+ }
275
+ catch {
276
+ // Keep the success message even if refresh fails transiently.
277
+ }
278
+ if (attempt < 4) {
279
+ await new Promise(resolve => setTimeout(resolve, 1200));
280
+ }
281
+ }
282
+ setPaymentMessage('Plan payment confirmed. Your plan status may take a few seconds to refresh.');
283
+ onCheckoutSuccess?.({ checkoutType: 'subscription' });
284
+ }
285
+ else {
286
+ const topUpPurchase = topUpPurchaseRef.current;
287
+ const purchasedCredits = topUpPurchase?.purchasedCredits || 0;
288
+ if (topUpPurchase && topUpPurchase.purchasedCredits > 0) {
289
+ const targetWalletBalance = topUpPurchase.oldWalletBalance + topUpPurchase.purchasedCredits;
290
+ setTopUpConfirmation({
291
+ purchasedCredits: topUpPurchase.purchasedCredits,
292
+ oldWalletBalance: topUpPurchase.oldWalletBalance,
293
+ newWalletBalance: targetWalletBalance,
294
+ oldAvailableCredits: topUpPurchase.oldAvailableCredits,
295
+ newAvailableCredits: topUpPurchase.oldAvailableCredits + topUpPurchase.purchasedCredits,
296
+ });
297
+ setPendingTopUpTarget({
298
+ targetWalletBalance,
299
+ });
300
+ }
301
+ for (let attempt = 0; attempt < 5; attempt += 1) {
302
+ try {
303
+ await refetchSubscriptionStatus();
304
+ }
305
+ catch {
306
+ // Keep confirmation visible even if refresh fails transiently.
307
+ }
308
+ if (attempt < 4) {
309
+ await new Promise(resolve => setTimeout(resolve, 800));
310
+ }
311
+ }
312
+ setPaymentMessage('Payment confirmed. Credits update may take a few seconds.');
313
+ onCheckoutSuccess?.({
314
+ checkoutType: 'topup',
315
+ purchasedCredits,
316
+ });
317
+ topUpPurchaseRef.current = null;
318
+ }
319
+ setIsReturningFromCheckout(false);
320
+ }, [
321
+ checkoutType,
322
+ onCheckoutSuccess,
323
+ refetchSubscriptionStatus,
324
+ resumeSubscriptionMutation,
325
+ ]);
326
+ const subscription = subscriptionResp?.plan || null;
327
+ const availablePlans = useMemo(() => {
328
+ const byId = new Map();
329
+ const add = (plan) => {
330
+ const id = plan?.id;
331
+ if (!id || typeof id !== 'string') {
332
+ return;
333
+ }
334
+ byId.set(id, {
335
+ id,
336
+ name: String(plan?.name || id),
337
+ amount: Number(plan?.amount || 0),
338
+ currency: String(plan?.currency || 'usd'),
339
+ interval: plan?.interval,
340
+ included_runs: typeof plan?.included_runs === 'number'
341
+ ? plan.included_runs
342
+ : undefined,
343
+ });
344
+ };
345
+ plans.forEach(add);
346
+ (subscriptionResp?.available_plans || []).forEach(add);
347
+ return Array.from(byId.values());
348
+ }, [plans, subscriptionResp?.available_plans]);
349
+ const subscriptionStatus = subscription?.status || 'unknown';
350
+ const normalizedSubscriptionStatus = String(subscriptionStatus).toLowerCase();
351
+ const isPendingSubscriptionCheckout = normalizedSubscriptionStatus === 'incomplete';
352
+ const rawCurrentSubscriptionPlan = isPendingSubscriptionCheckout
353
+ ? 'Free'
354
+ : subscription?.plan_name || 'Free';
355
+ const currentSubscriptionPlan = useMemo(() => {
356
+ const raw = String(rawCurrentSubscriptionPlan || 'Free');
357
+ const byId = availablePlans.find(plan => plan.id === raw);
358
+ const resolved = byId?.name || raw;
359
+ // Always present plan names with a trailing " Plan" for consistency with
360
+ // other surfaces (e.g. /settings/plans/overview).
361
+ return /\bplan$/i.test(resolved) ? resolved : `${resolved} Plan`;
362
+ }, [availablePlans, rawCurrentSubscriptionPlan]);
363
+ const currentPlanDetails = useMemo(() => {
364
+ const byPlanId = availablePlans.find(plan => plan.id && plan.id === subscription?.plan_id);
365
+ if (byPlanId) {
366
+ return byPlanId;
367
+ }
368
+ const normalizedCurrentPlanName = String(currentSubscriptionPlan).toLowerCase();
369
+ return (availablePlans.find(plan => plan.name &&
370
+ normalizedCurrentPlanName.includes(plan.name.toLowerCase())) || null);
371
+ }, [availablePlans, currentSubscriptionPlan, subscription?.plan_id]);
372
+ const currentPlanPriceLabel = useMemo(() => {
373
+ if (!currentPlanDetails) {
374
+ return 'N/A';
375
+ }
376
+ const amount = Number(currentPlanDetails.amount || 0);
377
+ const currency = currentPlanDetails.currency || 'usd';
378
+ const formatted = new Intl.NumberFormat(undefined, {
379
+ style: 'currency',
380
+ currency,
381
+ }).format(amount / 100);
382
+ return `${formatted}${currentPlanDetails.interval ? ` / ${currentPlanDetails.interval}` : ''}`;
383
+ }, [currentPlanDetails]);
384
+ const subscriptionPeriodEndLabel = useMemo(() => {
385
+ const rawEnd = subscription?.current_period_end;
386
+ if (!rawEnd) {
387
+ return 'N/A';
388
+ }
389
+ const parsed = new Date(rawEnd);
390
+ if (Number.isNaN(parsed.getTime())) {
391
+ return String(rawEnd);
392
+ }
393
+ return new Intl.DateTimeFormat(undefined, {
394
+ year: 'numeric',
395
+ month: 'short',
396
+ day: '2-digit',
397
+ }).format(parsed);
398
+ }, [subscription?.current_period_end]);
399
+ const subscriptionPortalUrl = subscriptionResp?.portal?.url || checkoutPortal?.url;
400
+ const isStripeTestConfiguration = useMemo(() => {
401
+ const stripeKey = String(checkoutPortal?.metadata?.stripe_key || '').trim();
402
+ if (stripeKey.startsWith('pk_test_')) {
403
+ return true;
404
+ }
405
+ const portalUrl = String(checkoutPortal?.url || '').toLowerCase();
406
+ return portalUrl.includes('/test_');
407
+ }, [checkoutPortal]);
408
+ const isCancellationScheduled = Boolean(subscription?.cancel_at_period_end);
409
+ const isIncompleteSubscription = normalizedSubscriptionStatus === 'incomplete';
410
+ const displaySubscriptionStatus = isCancellationScheduled
411
+ ? 'cancelled'
412
+ : String(currentSubscriptionPlan).toLowerCase().includes('free') ||
413
+ String(currentSubscriptionPlan).toLowerCase().includes('trial') ||
414
+ String(currentSubscriptionPlan).toLowerCase() === 'unknown' ||
415
+ String(currentSubscriptionPlan).toLowerCase() === 'none'
416
+ ? null
417
+ : normalizedSubscriptionStatus &&
418
+ normalizedSubscriptionStatus !== 'unknown'
419
+ ? String(subscriptionStatus).replaceAll('_', ' ')
420
+ : null;
421
+ const normalizedPlanName = String(currentSubscriptionPlan).toLowerCase();
422
+ const planIncludedRuns = asPositiveNumber(plans.find(plan => plan.name && normalizedPlanName.includes(plan.name.toLowerCase()))?.included_runs);
423
+ const defaultIncludedRuns = planIncludedRuns ||
424
+ Object.entries(PLAN_INCLUDED_RUNS_DEFAULTS).find(([planKey]) => normalizedPlanName.includes(planKey))?.[1] ||
425
+ null;
426
+ const resolvedIncludedRuns = asPositiveNumber(subscription?.included_runs) || defaultIncludedRuns;
427
+ const usedRuns = asNonNegativeNumber(subscription?.current_runs) ||
428
+ asNonNegativeNumber(subscription?.used_runs) ||
429
+ 0;
430
+ const walletIsQuota = Boolean(subscription?.wallet_is_quota);
431
+ const walletBalanceRaw = asNonNegativeNumber(subscription?.wallet_balance) || 0;
432
+ const walletQuota = asPositiveNumber(subscription?.wallet_quota);
433
+ const usedCredits = asNonNegativeNumber(subscription?.current_credits) ||
434
+ asNonNegativeNumber(subscription?.used_credits) ||
435
+ 0;
436
+ const includedCredits = walletIsQuota
437
+ ? (walletQuota ?? 0)
438
+ : Math.max(0, usedCredits + walletBalanceRaw);
439
+ const remainingCredits = walletIsQuota
440
+ ? Math.max(0, includedCredits - usedCredits)
441
+ : Math.max(0, walletBalanceRaw);
442
+ const runsTotal = resolvedIncludedRuns ?? 0;
443
+ const runsSegments = useMemo(() => buildUsageSegments(usedRuns, runsTotal), [usedRuns, runsTotal]);
444
+ const periodStartDate = useMemo(() => {
445
+ const rawStart = subscription?.current_period_start;
446
+ if (!rawStart) {
447
+ return null;
448
+ }
449
+ const parsed = new Date(rawStart);
450
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
451
+ }, [subscription?.current_period_start]);
452
+ const periodEndDate = useMemo(() => {
453
+ const rawEnd = subscription?.current_period_end;
454
+ if (!rawEnd) {
455
+ return null;
456
+ }
457
+ const parsed = new Date(rawEnd);
458
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
459
+ }, [subscription?.current_period_end]);
460
+ const periodProgress = useMemo(() => {
461
+ if (!periodStartDate || !periodEndDate) {
462
+ return null;
463
+ }
464
+ const startMs = periodStartDate.getTime();
465
+ const endMs = periodEndDate.getTime();
466
+ if (endMs <= startMs) {
467
+ return null;
468
+ }
469
+ const nowMs = Date.now();
470
+ const totalMs = endMs - startMs;
471
+ const elapsedMs = Math.min(Math.max(nowMs - startMs, 0), totalMs);
472
+ const remainingMs = Math.max(endMs - nowMs, 0);
473
+ const msPerDay = 24 * 60 * 60 * 1000;
474
+ const totalDays = Math.max(1, Math.ceil(totalMs / msPerDay));
475
+ const elapsedDays = Math.min(totalDays, Math.floor(elapsedMs / msPerDay));
476
+ const remainingDays = Math.max(0, Math.ceil(remainingMs / msPerDay));
477
+ return {
478
+ elapsedPct: (elapsedMs / totalMs) * 100,
479
+ remainingPct: (remainingMs / totalMs) * 100,
480
+ totalDays,
481
+ elapsedDays,
482
+ remainingDays,
483
+ };
484
+ }, [periodEndDate, periodStartDate]);
485
+ const walletBalance = walletIsQuota
486
+ ? Math.max(0, remainingCredits)
487
+ : Math.max(0, walletBalanceRaw);
488
+ const displayedWalletBalance = pendingTopUpTarget
489
+ ? Math.max(walletBalance, pendingTopUpTarget.targetWalletBalance)
490
+ : walletBalance;
491
+ const displayedAvailableCredits = pendingTopUpTarget
492
+ ? Math.max(remainingCredits, pendingTopUpTarget.targetWalletBalance)
493
+ : remainingCredits;
494
+ const isRunsOverQuota = runsTotal > 0 && usedRuns > runsTotal;
495
+ const hasBillablePlan = useMemo(() => {
496
+ const normalizedPlan = String(currentSubscriptionPlan).toLowerCase();
497
+ const freeLike = normalizedPlan.includes('free') ||
498
+ normalizedPlan.includes('trial') ||
499
+ normalizedPlan === 'unknown' ||
500
+ normalizedPlan === 'none';
501
+ return !freeLike;
502
+ }, [currentSubscriptionPlan]);
503
+ const isPaidSubscription = useMemo(() => {
504
+ const normalizedStatus = String(subscriptionStatus).toLowerCase();
505
+ if (!hasBillablePlan) {
506
+ return false;
507
+ }
508
+ if (normalizedStatus.includes('incomplete') ||
509
+ normalizedStatus.includes('canceled') ||
510
+ normalizedStatus.includes('cancelled') ||
511
+ normalizedStatus.includes('free') ||
512
+ normalizedStatus === 'unknown') {
513
+ return false;
514
+ }
515
+ return ['active', 'trialing', 'past_due', 'paid'].includes(normalizedStatus);
516
+ }, [hasBillablePlan, subscriptionStatus]);
517
+ const canCancelSubscription = useMemo(() => {
518
+ if (!hasBillablePlan) {
519
+ return false;
520
+ }
521
+ const status = String(subscriptionStatus).toLowerCase();
522
+ const nonCancelable = isCancellationScheduled ||
523
+ status.includes('incomplete_expired') ||
524
+ status.includes('canceled') ||
525
+ status.includes('cancelled') ||
526
+ status.includes('free') ||
527
+ status === 'unknown';
528
+ return !nonCancelable;
529
+ }, [hasBillablePlan, subscriptionStatus, isCancellationScheduled]);
530
+ const isCancelActionPending = cancelSubscriptionMutation.isPending || isConfirmingCancel;
531
+ const isResumeActionPending = resumeSubscriptionMutation.isPending || isResumingTransition;
532
+ const showResumeAction = isCancellationScheduled && !isCancelActionPending;
533
+ useEffect(() => {
534
+ if (isResumingTransition && !isCancellationScheduled) {
535
+ setIsResumingTransition(false);
536
+ }
537
+ }, [isCancellationScheduled, isResumingTransition]);
538
+ useEffect(() => {
539
+ if (isPaidSubscription && paymentMessage) {
540
+ setPaymentMessage(null);
541
+ }
542
+ }, [isPaidSubscription, paymentMessage]);
543
+ useEffect(() => {
544
+ if (!isPaidSubscription && !subscriptionPlan && plans.length > 0) {
545
+ setSubscriptionPlan(plans[0]);
546
+ }
547
+ }, [isPaidSubscription, plans, subscriptionPlan]);
548
+ useEffect(() => {
549
+ if (!product && sortedTopUpItems.length > 0) {
550
+ const secondCard = sortedTopUpItems.length > 1 ? sortedTopUpItems[1] : sortedTopUpItems[0];
551
+ setProduct(secondCard);
552
+ }
553
+ }, [product, sortedTopUpItems]);
554
+ useEffect(() => {
555
+ if (!pendingTopUpTarget) {
556
+ return;
557
+ }
558
+ if (walletBalance >= pendingTopUpTarget.targetWalletBalance) {
559
+ setPendingTopUpTarget(null);
560
+ }
561
+ }, [pendingTopUpTarget, walletBalance]);
562
+ // Auto-open the in-app cancel/downgrade view when the page is opened with
563
+ // `?action=downgrade` (e.g. from the Plan Overview "Downgrade" CTA).
564
+ // When opened with `?action=resume`, immediately trigger the resume flow.
565
+ const autoActionTriggeredRef = useRef(false);
566
+ useEffect(() => {
567
+ if (typeof window === 'undefined') {
568
+ return;
569
+ }
570
+ if (autoActionTriggeredRef.current) {
571
+ return;
572
+ }
573
+ try {
574
+ const params = new URLSearchParams(window.location.search);
575
+ const action = params.get('action');
576
+ if (action === 'downgrade' && isPaidSubscription) {
577
+ autoActionTriggeredRef.current = true;
578
+ setCancelViewOpen(true);
579
+ }
580
+ else if (action === 'resume' && isCancellationScheduled) {
581
+ autoActionTriggeredRef.current = true;
582
+ void onResumeSubscription();
583
+ }
584
+ }
585
+ catch (_error) {
586
+ // Ignore malformed URLs.
587
+ }
588
+ }, [isPaidSubscription, isCancellationScheduled]);
589
+ const startCheckout = useCallback(async () => {
590
+ if (!product) {
591
+ return;
592
+ }
593
+ topUpPurchaseRef.current = {
594
+ purchasedCredits: Math.max(0, Number(product.credits || 0)),
595
+ oldWalletBalance: displayedWalletBalance,
596
+ oldAvailableCredits: displayedAvailableCredits,
597
+ };
598
+ setTopUpConfirmation(null);
599
+ setPaymentMessage(null);
600
+ setCheckoutType('topup');
601
+ setCheckout(true);
602
+ try {
603
+ const clientSecret = await topUpPaymentIntentMutation.mutateAsync({
604
+ product,
605
+ });
606
+ if (!clientSecret) {
607
+ setPaymentClientSecret(null);
608
+ setCheckout(false);
609
+ setPaymentMessage('Unable to initialize Stripe checkout. Please try again.');
610
+ return;
611
+ }
612
+ setPaymentClientSecret(clientSecret);
613
+ }
614
+ catch (error) {
615
+ const detail = error instanceof Error
616
+ ? error.message
617
+ : 'Unable to initialize Stripe checkout. Please try again.';
618
+ topUpPurchaseRef.current = null;
619
+ setPaymentClientSecret(null);
620
+ setCheckout(false);
621
+ setPaymentMessage(detail);
622
+ }
623
+ }, [
624
+ displayedAvailableCredits,
625
+ displayedWalletBalance,
626
+ topUpPaymentIntentMutation,
627
+ product,
628
+ ]);
629
+ const startSubscriptionCheckout = useCallback(async (planOverride) => {
630
+ const selectedPlan = planOverride ?? subscriptionPlan;
631
+ if (!selectedPlan) {
632
+ setPaymentMessage('Select a monthly plan first.');
633
+ return;
634
+ }
635
+ setPaymentMessage(null);
636
+ try {
637
+ const clientSecret = await subscriptionPaymentIntentMutation.mutateAsync({
638
+ plan: selectedPlan,
639
+ });
640
+ if (!clientSecret) {
641
+ setPaymentClientSecret(null);
642
+ setCheckout(false);
643
+ setPaymentMessage('Unable to initialize Stripe checkout. Please try again.');
644
+ return;
645
+ }
646
+ setSubscriptionPlan(selectedPlan);
647
+ setPaymentClientSecret(clientSecret);
648
+ setCheckoutType('subscription');
649
+ setCheckout(true);
650
+ }
651
+ catch (error) {
652
+ const detail = error instanceof Error
653
+ ? error.message
654
+ : 'Unable to initialize Stripe checkout. Please try again.';
655
+ setPaymentClientSecret(null);
656
+ setCheckout(false);
657
+ setPaymentMessage(detail);
658
+ }
659
+ }, [subscriptionPaymentIntentMutation, subscriptionPlan]);
660
+ const pendingSubscriptionPlan = useMemo(() => {
661
+ const teamLikePlan = plans.find(plan => String(plan?.name || '')
662
+ .toLowerCase()
663
+ .includes('team'));
664
+ return teamLikePlan || plans[0] || null;
665
+ }, [plans]);
666
+ const startPendingSubscriptionCheckout = useCallback(() => {
667
+ if (!pendingSubscriptionPlan) {
668
+ setPaymentMessage('Select a monthly plan first.');
669
+ return;
670
+ }
671
+ void startSubscriptionCheckout(pendingSubscriptionPlan);
672
+ }, [pendingSubscriptionPlan, startSubscriptionCheckout]);
673
+ const openPortal = useCallback((url) => {
674
+ if (!url) {
675
+ return;
676
+ }
677
+ window.open(url, '_blank', 'noreferrer');
678
+ }, []);
679
+ const cancelStripeCheckout = useCallback(() => {
680
+ setCheckout(false);
681
+ setPaymentClientSecret(null);
682
+ setPaymentMessage(null);
683
+ }, []);
684
+ const onCancelSubscription = useCallback(() => {
685
+ setPaymentMessage(null);
686
+ setResumeConfirmationMessage(null);
687
+ setCancelViewOpen(true);
688
+ }, []);
689
+ const onAbortCancelView = useCallback(() => {
690
+ setCancelViewOpen(false);
691
+ }, []);
692
+ const onConfirmCancelSubscription = useCallback(async () => {
693
+ setPaymentMessage(null);
694
+ setIsConfirmingCancel(true);
695
+ try {
696
+ const resp = await cancelSubscriptionMutation.mutateAsync();
697
+ if (resp?.success === false) {
698
+ throw new Error(resp?.message || 'Unable to update your plan right now.');
699
+ }
700
+ const responseStatus = String(resp?.status || '').toLowerCase();
701
+ const responseCancelAtPeriodEnd = Boolean(resp?.cancel_at_period_end);
702
+ const isNowCanceled = responseStatus.includes('canceled') ||
703
+ responseStatus.includes('cancelled');
704
+ const datedMessage = responseCancelAtPeriodEnd && subscriptionPeriodEndLabel
705
+ ? `Plan will switch to Free at the end of the current period on ${subscriptionPeriodEndLabel}.`
706
+ : null;
707
+ setPaymentMessage((isNowCanceled ? 'Pending plan change canceled.' : datedMessage) ||
708
+ resp?.message ||
709
+ 'Plan change requested successfully.');
710
+ setCancelViewOpen(false);
711
+ setIsConfirmingCancel(false);
712
+ // Refresh plan status in the background so UI feedback is immediate.
713
+ void (async () => {
714
+ for (let attempt = 0; attempt < 5; attempt += 1) {
715
+ try {
716
+ await refetchSubscriptionStatus();
717
+ }
718
+ catch {
719
+ // Ignore transient refetch errors and keep trying.
720
+ }
721
+ if (attempt < 4) {
722
+ await new Promise(resolve => setTimeout(resolve, 800));
723
+ }
724
+ }
725
+ })();
726
+ }
727
+ catch (error) {
728
+ setIsConfirmingCancel(false);
729
+ setPaymentMessage(error instanceof Error
730
+ ? error.message
731
+ : 'Unable to update your plan right now.');
732
+ }
733
+ }, [
734
+ cancelSubscriptionMutation,
735
+ refetchSubscriptionStatus,
736
+ subscriptionPeriodEndLabel,
737
+ ]);
738
+ const onResumeSubscription = useCallback(async () => {
739
+ setPaymentMessage(null);
740
+ setResumeConfirmationMessage(null);
741
+ setIsResumingTransition(true);
742
+ try {
743
+ const resp = await resumeSubscriptionMutation.mutateAsync();
744
+ if (resp?.success === false) {
745
+ throw new Error(resp?.message || 'Unable to resume your plan right now.');
746
+ }
747
+ setCheckout(false);
748
+ setPaymentClientSecret(null);
749
+ setPaymentMessage(null);
750
+ const periodEndText = subscriptionPeriodEndLabel && subscriptionPeriodEndLabel !== 'N/A'
751
+ ? ` through ${subscriptionPeriodEndLabel}`
752
+ : '';
753
+ setResumeConfirmationMessage(`Resume complete. Your plan remains active${periodEndText} and will renew automatically after that date.`);
754
+ setIsResumingTransition(false);
755
+ // Refresh plan status in the background so success feedback appears fast.
756
+ void (async () => {
757
+ for (let attempt = 0; attempt < 5; attempt += 1) {
758
+ try {
759
+ await refetchSubscriptionStatus();
760
+ }
761
+ catch {
762
+ // Ignore transient refetch errors and keep trying.
763
+ }
764
+ if (attempt < 4) {
765
+ await new Promise(resolve => setTimeout(resolve, 800));
766
+ }
767
+ }
768
+ })();
769
+ }
770
+ catch (error) {
771
+ setIsResumingTransition(false);
772
+ setPaymentMessage(error instanceof Error
773
+ ? error.message
774
+ : 'Unable to resume your plan right now.');
775
+ }
776
+ }, [
777
+ refetchSubscriptionStatus,
778
+ resumeSubscriptionMutation,
779
+ subscriptionPeriodEndLabel,
780
+ ]);
781
+ const onRefreshSubscriptionStatus = useCallback(async () => {
782
+ setPaymentMessage(null);
783
+ try {
784
+ await refetchSubscriptionStatus();
785
+ setPaymentMessage('Plan status refreshed.');
786
+ }
787
+ catch (error) {
788
+ setPaymentMessage(error instanceof Error
789
+ ? error.message
790
+ : 'Unable to refresh plan status right now.');
791
+ }
792
+ }, [refetchSubscriptionStatus]);
793
+ const selectedCheckoutLabel = useMemo(() => {
794
+ if (checkoutType === 'subscription' && subscriptionPlan) {
795
+ const amount = new Intl.NumberFormat(undefined, {
796
+ style: 'currency',
797
+ currency: subscriptionPlan.currency,
798
+ }).format((subscriptionPlan.amount || 0) / 100);
799
+ return `${subscriptionPlan.name} (${amount}${subscriptionPlan.interval ? ` / ${subscriptionPlan.interval}` : ''})`;
800
+ }
801
+ if (checkoutType === 'topup' && product) {
802
+ const amount = new Intl.NumberFormat(undefined, {
803
+ style: 'currency',
804
+ currency: product.currency,
805
+ }).format((product.amount || 0) / 100);
806
+ return `${product.name} (${amount}, ${product.credits} credits)`;
807
+ }
808
+ return null;
809
+ }, [checkoutType, product, subscriptionPlan]);
810
+ const sectionTitleSx = {
811
+ fontSize: 2,
812
+ fontWeight: 'bold',
813
+ marginBottom: 'var(--stack-gap-normal)',
814
+ };
815
+ const shouldShowMonthlySubscriptionSection = !isPaidSubscription || isIncompleteSubscription;
816
+ const monthlySubscriptionSection = shouldShowMonthlySubscriptionSection ? (_jsxs(Box, { sx: {
817
+ borderTop: 'none',
818
+ paddingTop: 0,
819
+ }, children: [_jsx(Text, { as: "h3", sx: sectionTitleSx, children: "Choose a monthly plan" }), isIncompleteSubscription ? (_jsxs(_Fragment, { children: [_jsx(Text, { as: "p", sx: { color: 'fg.muted' }, children: "A pending plan change already exists. Complete payment or cancel it from the billing portal before creating a new one." }), _jsxs(Box, { sx: {
820
+ display: 'flex',
821
+ gap: 'var(--stack-gap-condensed)',
822
+ flexWrap: 'wrap',
823
+ marginTop: 'var(--stack-gap-normal)',
824
+ }, children: [_jsx(Button, { variant: "primary", onClick: startPendingSubscriptionCheckout, disabled: subscriptionPaymentIntentMutation.isPending ||
825
+ checkout ||
826
+ !pendingSubscriptionPlan, children: subscriptionPaymentIntentMutation.isPending
827
+ ? 'Preparing checkout...'
828
+ : 'Continue pending payment' }), _jsx(Button, { variant: "danger", onClick: onCancelSubscription, children: "Cancel pending plan change" })] })] })) : !isPaidSubscription ? (_jsxs(_Fragment, { children: [_jsxs(Box, { role: "radiogroup", sx: {
829
+ display: 'grid',
830
+ gap: 'var(--stack-gap-condensed)',
831
+ maxHeight: '260px',
832
+ overflowY: 'auto',
833
+ marginBottom: 'var(--stack-gap-normal)',
834
+ }, children: [plans.map(plan => (_jsx(Box, { role: "radio", "aria-labelledby": `subscription-plan-${plan.id}`, "aria-checked": subscriptionPlan?.id === plan.id, onClick: () => setSubscriptionPlan(plan), sx: {
835
+ borderStyle: 'solid',
836
+ borderRadius: 'var(--borderRadius-medium)',
837
+ borderWidth: 'var(--borderWidth-thick)',
838
+ borderColor: subscriptionPlan?.id === plan.id
839
+ ? 'var(--borderColor-accent-emphasis)'
840
+ : 'var(--borderColor-default)',
841
+ padding: 'var(--stack-padding-condensed)',
842
+ cursor: 'pointer',
843
+ }, children: _jsxs(FormControl, { sx: { alignItems: 'center' }, children: [_jsx(FormControl.Label, { id: `subscription-plan-${plan.id}`, sx: { alignSelf: 'center' }, children: plan.name }), _jsxs(Text, { as: "p", children: [new Intl.NumberFormat(undefined, {
844
+ style: 'currency',
845
+ currency: plan.currency,
846
+ }).format((plan.amount || 0) / 100), plan.interval ? ` / ${plan.interval}` : ''] }), typeof plan.included_runs === 'number' && (_jsxs(Text, { as: "p", children: [plan.included_runs, " included runs"] }))] }) }, plan.id))), plans.length === 0 && (_jsx(Text, { as: "p", sx: { color: 'fg.muted' }, children: "No monthly plans available right now." }))] }), _jsx(Button, { variant: "primary", onClick: () => void startSubscriptionCheckout(), disabled: !subscriptionPlan ||
847
+ subscriptionPaymentIntentMutation.isPending ||
848
+ checkout, children: subscriptionPaymentIntentMutation.isPending
849
+ ? 'Preparing Team plan checkout...'
850
+ : 'Update to Team Plan' })] })) : null] })) : null;
851
+ const topUpSection = (_jsxs(Box, { children: [_jsx(Text, { as: "h3", sx: sectionTitleSx, children: "Topup your wallet with credits" }), _jsx(Box, { role: "radiogroup", sx: {
852
+ display: 'grid',
853
+ gap: 'var(--stack-gap-normal)',
854
+ gridTemplateColumns: Array(sortedTopUpItems.length)
855
+ .fill('1fr')
856
+ .join(' '),
857
+ padding: 0,
858
+ marginBottom: 'var(--stack-gap-normal)',
859
+ }, children: sortedTopUpItems.map(item => (_jsx(Box, { role: "radio", "aria-labelledby": `checkout-price-${item.id}`, "aria-checked": product?.id === item.id, onClick: () => {
860
+ setProduct(item);
861
+ }, sx: {
862
+ borderStyle: 'solid',
863
+ borderRadius: 'var(--borderRadius-medium)',
864
+ borderWidth: 'var(--borderWidth-thick)',
865
+ borderColor: product?.id === item.id
866
+ ? 'var(--borderColor-accent-emphasis)'
867
+ : 'var(--borderColor-default)',
868
+ padding: 'var(--stack-padding-condensed)',
869
+ cursor: 'pointer',
870
+ }, children: _jsxs(FormControl, { sx: {
871
+ alignItems: 'center',
872
+ }, children: [_jsx(FormControl.Label, { id: `checkout-price-${item.id}`, sx: { alignSelf: 'center' }, children: item.name }), _jsx(Text, { as: "p", children: new Intl.NumberFormat(undefined, {
873
+ style: 'currency',
874
+ currency: item.currency,
875
+ }).format(item.amount / 100) }), _jsxs(Text, { as: "p", children: [item.credits, " credits"] })] }) }, item.id))) }), _jsx(Button, { variant: "primary", onClick: () => {
876
+ void startCheckout();
877
+ }, disabled: product === null || topUpPaymentIntentMutation.isPending || checkout, sx: { float: 'right' }, children: topUpPaymentIntentMutation.isPending
878
+ ? 'Preparing top-up checkout...'
879
+ : 'Checkout' }), topUpConfirmation ? (_jsxs(Flash, { variant: "success", sx: { mt: 3 }, children: [_jsxs(Text, { as: "p", sx: { fontWeight: 'bold' }, children: ["Top-up confirmed: +", topUpConfirmation.purchasedCredits.toLocaleString(), " credits"] }), _jsx(Text, { as: "p", children: `Wallet balance: ${topUpConfirmation.oldWalletBalance.toLocaleString()} to ${topUpConfirmation.newWalletBalance.toLocaleString()}` }), _jsx(Text, { as: "p", children: `Available credits: ${topUpConfirmation.oldAvailableCredits.toLocaleString()} to ${topUpConfirmation.newAvailableCredits.toLocaleString()}` })] })) : null] }));
880
+ const topCards = showStatusUsageSummary && !isPaidSubscription ? (_jsx(Box, { sx: {
881
+ marginBottom: 'var(--stack-gap-normal)',
882
+ border: '1px solid',
883
+ borderColor: 'border.default',
884
+ borderRadius: 'var(--borderRadius-medium)',
885
+ backgroundColor: 'canvas.default',
886
+ padding: 'var(--stack-padding-normal)',
887
+ display: 'grid',
888
+ gap: 'var(--stack-gap-normal)',
889
+ }, children: _jsx(Box, { sx: {
890
+ display: 'grid',
891
+ gap: 'var(--stack-gap-normal)',
892
+ gridTemplateColumns: ['1fr'],
893
+ alignItems: 'start',
894
+ }, children: _jsxs(Box, { children: [_jsx(Text, { as: "h3", sx: {
895
+ fontWeight: 'bold',
896
+ marginBottom: 'var(--stack-gap-condensed)',
897
+ }, children: "Plan status" }), _jsxs(Text, { as: "p", children: ["Plan: ", String(currentSubscriptionPlan)] }), isPendingSubscriptionCheckout && (_jsx(Flash, { variant: "warning", sx: { marginTop: 'var(--stack-gap-condensed)' }, children: "Upgrade pending payment. Your Team plan is not active until card payment succeeds." })), currentPlanPriceLabel !== 'N/A' && (_jsxs(Text, { as: "p", children: ["Price: ", currentPlanPriceLabel] })), displaySubscriptionStatus && (_jsxs(Text, { as: "p", sx: { marginBottom: 'var(--stack-gap-condensed)' }, children: ["Status: ", displaySubscriptionStatus] })), _jsxs(Box, { sx: {
898
+ marginBottom: 'var(--stack-gap-normal)',
899
+ border: '1px solid',
900
+ borderColor: 'border.muted',
901
+ borderRadius: 'var(--borderRadius-medium)',
902
+ backgroundColor: 'canvas.subtle',
903
+ padding: 'var(--stack-padding-condensed)',
904
+ display: 'grid',
905
+ gap: 'var(--stack-gap-condensed)',
906
+ }, children: [_jsxs(Box, { sx: {
907
+ display: 'flex',
908
+ alignItems: 'center',
909
+ justifyContent: 'space-between',
910
+ gap: 2,
911
+ }, children: [_jsx(Text, { as: "h4", sx: { fontWeight: 'bold' }, children: "Current usage" }), _jsx(Label, { size: "small", children: String(currentSubscriptionPlan) })] }), _jsxs(Box, { children: [_jsxs(Text, { as: "p", sx: {
912
+ marginBottom: 'var(--stack-gap-condensed)',
913
+ color: isRunsOverQuota ? 'danger.fg' : 'fg.default',
914
+ fontWeight: isRunsOverQuota ? 'bold' : 'normal',
915
+ }, children: ["Runs: ", usedRuns.toLocaleString(), " /", ' ', runsTotal.toLocaleString()] }), _jsxs(ProgressBar, { barSize: "small", "aria-label": "Runs usage", "aria-valuenow": runsSegments.inQuotaPct + runsSegments.overPct, children: [_jsx(ProgressBar.Item, { progress: runsSegments.inQuotaPct, style: {
916
+ backgroundColor: 'var(--bgColor-success-emphasis)',
917
+ }, "aria-label": `Used in quota: ${runsSegments.inQuotaPct.toFixed(1)}%` }), _jsx(ProgressBar.Item, { progress: runsSegments.remainingPct, style: {
918
+ backgroundColor: 'var(--bgColor-accent-emphasis)',
919
+ }, "aria-label": `Remaining: ${runsSegments.remainingPct.toFixed(1)}%` }), _jsx(ProgressBar.Item, { progress: runsSegments.overPct, style: {
920
+ backgroundColor: 'var(--bgColor-danger-emphasis)',
921
+ }, "aria-label": `Over quota: ${runsSegments.overPct.toFixed(1)}%` })] }), _jsxs(Box, { sx: {
922
+ display: 'flex',
923
+ alignItems: 'center',
924
+ gap: 3,
925
+ mt: 1,
926
+ flexWrap: 'wrap',
927
+ }, children: [_jsxs(Box, { sx: {
928
+ display: 'inline-flex',
929
+ alignItems: 'center',
930
+ gap: 1,
931
+ }, children: [_jsx(DotFillIcon, { fill: "var(--bgColor-success-emphasis)" }), _jsx(Text, { sx: { fontSize: 0 }, children: "Used in quota" })] }), _jsxs(Box, { sx: {
932
+ display: 'inline-flex',
933
+ alignItems: 'center',
934
+ gap: 1,
935
+ }, children: [_jsx(DotFillIcon, { fill: "var(--bgColor-accent-emphasis)" }), _jsx(Text, { sx: { fontSize: 0 }, children: "Remaining" })] }), _jsxs(Box, { sx: {
936
+ display: 'inline-flex',
937
+ alignItems: 'center',
938
+ gap: 1,
939
+ }, children: [_jsx(DotFillIcon, { fill: "var(--bgColor-danger-emphasis)" }), _jsx(Text, { sx: { fontSize: 0 }, children: "Over quota" })] })] })] }), periodProgress ? (_jsxs(Box, { children: [_jsxs(Text, { as: "p", sx: { marginBottom: 'var(--stack-gap-condensed)' }, children: ["Usage period days: ", periodProgress.elapsedDays, " /", ' ', periodProgress.totalDays] }), _jsxs(ProgressBar, { barSize: "small", "aria-label": "Usage period progress", "aria-valuenow": periodProgress.elapsedPct, children: [_jsx(ProgressBar.Item, { progress: periodProgress.elapsedPct, style: {
940
+ backgroundColor: 'var(--bgColor-success-emphasis)',
941
+ }, "aria-label": `Elapsed: ${periodProgress.elapsedPct.toFixed(1)}%` }), _jsx(ProgressBar.Item, { progress: periodProgress.remainingPct, style: {
942
+ backgroundColor: 'var(--bgColor-accent-emphasis)',
943
+ }, "aria-label": `Remaining: ${periodProgress.remainingPct.toFixed(1)}%` })] }), _jsxs(Text, { as: "p", sx: {
944
+ color: 'fg.muted',
945
+ fontSize: 0,
946
+ marginTop: 'var(--stack-gap-condensed)',
947
+ }, children: [periodProgress.remainingDays, " day(s) remaining in current period"] })] })) : null, _jsxs(Box, { children: [_jsxs(Text, { as: "p", sx: { marginBottom: 'var(--stack-gap-condensed)' }, children: ["Wallet balance: ", displayedWalletBalance.toLocaleString()] }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted' }, children: ["Spent credits in current period:", ' ', usedCredits.toLocaleString(undefined, {
948
+ minimumFractionDigits: 2,
949
+ maximumFractionDigits: 2,
950
+ })] }), _jsx(Text, { as: "p", sx: { color: 'fg.muted' }, children: "Wallet credits are additive on renewal and top-ups." })] })] }), isCancellationScheduled && (_jsxs(Flash, { variant: "warning", sx: { marginBottom: 'var(--stack-gap-condensed)' }, children: ["Plan will switch to Free at the end of the current period on", ' ', subscriptionPeriodEndLabel, "."] })), _jsxs(Box, { sx: {
951
+ display: 'flex',
952
+ gap: 'var(--stack-gap-condensed)',
953
+ flexWrap: 'wrap',
954
+ }, children: [subscriptionPortalUrl && (_jsx(Button, { variant: "default", onClick: () => openPortal(subscriptionPortalUrl), children: "Open Stripe billing portal" })), _jsx(Button, { variant: "default", onClick: () => void onRefreshSubscriptionStatus(), disabled: isSubscriptionStatusRefreshing, children: isSubscriptionStatusRefreshing
955
+ ? 'Refreshing status...'
956
+ : 'Refresh status' }), canCancelSubscription && !cancelViewOpen && (_jsx(Button, { variant: "danger", onClick: onCancelSubscription, children: "Downgrade to Free Plan" })), isIncompleteSubscription && !cancelViewOpen && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", onClick: startPendingSubscriptionCheckout, disabled: subscriptionPaymentIntentMutation.isPending ||
957
+ checkout ||
958
+ !pendingSubscriptionPlan, children: subscriptionPaymentIntentMutation.isPending
959
+ ? 'Preparing checkout...'
960
+ : 'Continue pending payment' }), _jsx(Button, { variant: "danger", onClick: onCancelSubscription, children: "Cancel pending plan change" })] })), showResumeAction && (_jsx(Button, { variant: "primary", onClick: () => void onResumeSubscription(), disabled: isResumeActionPending, leadingVisual: () => isResumeActionPending ? _jsx(Spinner, { size: "small" }) : undefined, children: isResumeActionPending ? 'Resuming...' : 'Resume plan' }))] }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', marginTop: 'var(--stack-gap-normal)' }, children: ["Next step:", ' ', isCancellationScheduled
961
+ ? 'Your plan is already scheduled to switch at period end. You can keep using it until then.'
962
+ : isIncompleteSubscription
963
+ ? 'Your payment is pending. Open the in-app cancel view below to cancel this plan change or continue with payment.'
964
+ : isPaidSubscription
965
+ ? 'Keep your plan active. You can top-up credits any time.'
966
+ : 'Top-up credits are available on Free and Team plans.'] }), cancelViewOpen && (_jsxs(Box, { sx: {
967
+ marginTop: 'var(--stack-gap-normal)',
968
+ border: '1px solid',
969
+ borderColor: 'border.default',
970
+ borderRadius: 'var(--borderRadius-medium)',
971
+ backgroundColor: 'canvas.subtle',
972
+ padding: 'var(--stack-padding-normal)',
973
+ display: 'grid',
974
+ gap: 'var(--stack-gap-condensed)',
975
+ }, children: [_jsx(Text, { as: "h4", sx: { fontWeight: 'bold' }, children: isIncompleteSubscription
976
+ ? 'Cancel pending plan change'
977
+ : 'Downgrade to Free Plan' }), _jsx(Text, { as: "p", sx: { color: 'fg.muted' }, children: isIncompleteSubscription
978
+ ? 'This pending plan change will be canceled immediately.'
979
+ : 'Your plan will switch at the end of the current usage period.' }), _jsxs(Box, { sx: {
980
+ display: 'flex',
981
+ gap: 'var(--stack-gap-condensed)',
982
+ flexWrap: 'wrap',
983
+ }, children: [_jsx(Button, { variant: "danger", onClick: () => void onConfirmCancelSubscription(), disabled: isCancelActionPending, leadingVisual: () => isCancelActionPending ? (_jsx(Spinner, { size: "small" })) : undefined, children: isCancelActionPending
984
+ ? isIncompleteSubscription
985
+ ? 'Canceling pending plan change...'
986
+ : 'Waiting for confirmation...'
987
+ : isIncompleteSubscription
988
+ ? 'Confirm cancel pending plan change'
989
+ : 'Confirm downgrade' }), _jsx(Button, { variant: "default", onClick: onAbortCancelView, disabled: isCancelActionPending, children: isIncompleteSubscription
990
+ ? 'Keep pending plan change'
991
+ : 'Keep current plan' })] })] }))] }) }) })) : null;
992
+ const currentPlanSection = isPaidSubscription ? (_jsx(Box, { sx: {
993
+ borderRight: ['none', '1px solid'],
994
+ borderColor: 'border.muted',
995
+ paddingRight: ['0', 'var(--stack-gap-normal)'],
996
+ alignSelf: 'stretch',
997
+ }, children: _jsxs(Box, { sx: {
998
+ borderTop: 'none',
999
+ paddingTop: 0,
1000
+ display: 'grid',
1001
+ gap: 'var(--stack-gap-condensed)',
1002
+ }, children: [_jsx(Text, { as: "h3", sx: sectionTitleSx, children: "Current plan" }), _jsx(Text, { as: "h4", sx: { fontSize: 3, fontWeight: 'bold' }, children: String(currentSubscriptionPlan) }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted' }, children: ["You are currently on ", String(currentSubscriptionPlan), "."] }), currentPlanPriceLabel !== 'N/A' && (_jsx(Text, { as: "p", sx: { fontWeight: 'bold', fontSize: 2 }, children: currentPlanPriceLabel })), displaySubscriptionStatus && (_jsx(Box, { sx: { display: 'inline-flex', alignItems: 'center' }, children: _jsx(Label, { variant: "success", children: displaySubscriptionStatus }) })), isCancellationScheduled ? (_jsxs(Flash, { variant: "warning", sx: { marginTop: 'var(--stack-gap-condensed)' }, children: ["Your downgrade to Free Plan is scheduled at period end on", ' ', subscriptionPeriodEndLabel, "."] })) : null, !isCancellationScheduled || showResumeAction ? (_jsx(Text, { as: "p", sx: { color: 'fg.muted', marginTop: 'var(--stack-gap-condensed)' }, children: showResumeAction
1003
+ ? 'Possible action: Resume Team Plan.'
1004
+ : 'Possible action: Downgrade to Free Plan.' })) : null, _jsxs(Box, { sx: {
1005
+ display: 'flex',
1006
+ gap: 'var(--stack-gap-condensed)',
1007
+ flexWrap: 'wrap',
1008
+ }, children: [canCancelSubscription && !cancelViewOpen && (_jsx(Button, { variant: "danger", onClick: onCancelSubscription, children: "Downgrade to Free Plan" })), showResumeAction && (_jsx(Button, { variant: "primary", onClick: () => void onResumeSubscription(), disabled: isResumeActionPending, leadingVisual: () => isResumeActionPending ? _jsx(Spinner, { size: "small" }) : undefined, children: isResumeActionPending ? 'Resuming...' : 'Resume plan' }))] }), cancelViewOpen && (_jsxs(Box, { sx: {
1009
+ marginTop: 'var(--stack-gap-normal)',
1010
+ border: '1px solid',
1011
+ borderColor: 'border.default',
1012
+ borderRadius: 'var(--borderRadius-medium)',
1013
+ backgroundColor: 'canvas.subtle',
1014
+ padding: 'var(--stack-padding-normal)',
1015
+ display: 'grid',
1016
+ gap: 'var(--stack-gap-condensed)',
1017
+ }, children: [_jsx(Text, { as: "h4", sx: { fontWeight: 'bold' }, children: "Downgrade to Free Plan" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted' }, children: "Your plan will switch at the end of the current usage period." }), _jsxs(Box, { sx: {
1018
+ display: 'flex',
1019
+ gap: 'var(--stack-gap-condensed)',
1020
+ flexWrap: 'wrap',
1021
+ }, children: [_jsx(Button, { variant: "danger", onClick: () => void onConfirmCancelSubscription(), disabled: isCancelActionPending, leadingVisual: () => isCancelActionPending ? _jsx(Spinner, { size: "small" }) : undefined, children: isCancelActionPending
1022
+ ? 'Waiting for confirmation...'
1023
+ : 'Confirm downgrade' }), _jsx(Button, { variant: "default", onClick: onAbortCancelView, disabled: isCancelActionPending, children: "Keep current plan" })] })] }))] }) })) : null;
54
1024
  let view = (_jsx(Box, { sx: { minHeight: '40px' }, children: _jsx(Spinner, {}) }));
1025
+ // While the Stripe payment form is shown, disable interaction with the
1026
+ // status / plan picker cards behind it so the only available action is the
1027
+ // "Cancel" button next to the form.
1028
+ const disabledTopCards = topCards ? (_jsx(Box, { "aria-disabled": "true", sx: {
1029
+ pointerEvents: 'none',
1030
+ opacity: 0.5,
1031
+ userSelect: 'none',
1032
+ }, children: topCards })) : null;
55
1033
  if (checkout) {
56
- if (stripe && components) {
57
- view = createElement(Box, { id: 'checkout', sx: { flex: '1 1 auto' } }, createElement(components.EmbeddedCheckoutProvider, { stripe, options }, createElement(components.EmbeddedCheckout)));
1034
+ if (stripe && paymentOptions && paymentClientSecret) {
1035
+ view = createElement(Box, { id: 'checkout', sx: { flex: '1 1 auto', display: 'grid', gap: 3 } }, createElement(Box, {
1036
+ sx: {
1037
+ border: '1px solid',
1038
+ borderColor: 'border.default',
1039
+ borderRadius: 'var(--borderRadius-medium)',
1040
+ backgroundColor: 'canvas.default',
1041
+ padding: 'var(--stack-padding-normal)',
1042
+ display: 'flex',
1043
+ gap: 'var(--stack-gap-normal)',
1044
+ alignItems: 'center',
1045
+ justifyContent: 'space-between',
1046
+ flexWrap: 'wrap',
1047
+ },
1048
+ }, createElement(Box, {
1049
+ sx: {
1050
+ display: 'flex',
1051
+ alignItems: 'center',
1052
+ flexWrap: 'wrap',
1053
+ gap: 2,
1054
+ },
1055
+ }, createElement(Text, { as: 'p' }, 'Selected for checkout:'), selectedCheckoutLabel
1056
+ ? createElement(Label, {
1057
+ variant: checkoutType === 'topup' ? 'success' : 'accent',
1058
+ }, selectedCheckoutLabel)
1059
+ : null), createElement(Button, { variant: 'default', onClick: cancelStripeCheckout }, 'Cancel')), null, createElement(Elements, {
1060
+ key: `${checkoutType}-${paymentClientSecret ?? 'none'}-${elementsAppearanceKey}`,
1061
+ stripe,
1062
+ options: paymentOptions,
1063
+ }, createElement(Box, {
1064
+ sx: {
1065
+ border: '1px solid',
1066
+ borderColor: 'border.default',
1067
+ borderRadius: 'var(--borderRadius-medium)',
1068
+ backgroundColor: 'canvas.default',
1069
+ padding: 'var(--stack-padding-normal)',
1070
+ width: '100%',
1071
+ maxWidth: '720px',
1072
+ margin: '0 auto',
1073
+ },
1074
+ }, createElement(StripePaymentForm, {
1075
+ clientSecret: paymentClientSecret,
1076
+ intentType: checkoutType === 'resume' ? 'setup' : 'payment',
1077
+ onPaymentSucceeded,
1078
+ }))), disabledTopCards);
1079
+ }
1080
+ else {
1081
+ view = (_jsxs(Box, { sx: { flex: '1 1 auto', display: 'grid', gap: 3 }, children: [_jsxs(Box, { sx: {
1082
+ border: '1px solid',
1083
+ borderColor: 'border.default',
1084
+ borderRadius: 'var(--borderRadius-medium)',
1085
+ backgroundColor: 'canvas.default',
1086
+ padding: 'var(--stack-padding-normal)',
1087
+ display: 'flex',
1088
+ gap: 'var(--stack-gap-normal)',
1089
+ alignItems: 'center',
1090
+ justifyContent: 'space-between',
1091
+ flexWrap: 'wrap',
1092
+ }, children: [_jsx(Text, { as: "p", children: "Preparing Stripe checkout\u2026" }), _jsx(Button, { variant: "default", onClick: cancelStripeCheckout, children: "Cancel" })] }), disabledTopCards] }));
58
1093
  }
59
1094
  }
60
- else if (items) {
1095
+ else if (isReturningFromCheckout) {
1096
+ view = (_jsxs(Box, { sx: { flex: '1 1 auto', display: 'grid', gap: 3 }, children: [_jsxs(Box, { sx: {
1097
+ border: '1px solid',
1098
+ borderColor: 'border.default',
1099
+ borderRadius: 'var(--borderRadius-medium)',
1100
+ backgroundColor: 'canvas.default',
1101
+ padding: 'var(--stack-padding-normal)',
1102
+ display: 'flex',
1103
+ gap: 'var(--stack-gap-normal)',
1104
+ alignItems: 'center',
1105
+ justifyContent: 'center',
1106
+ flexWrap: 'wrap',
1107
+ }, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { as: "p", children: "Refreshing plan status\u2026" })] }), disabledTopCards] }));
1108
+ }
1109
+ else if (isTopUpPricesPending) {
1110
+ view = (_jsx(Box, { sx: { minHeight: '40px', display: 'grid', placeItems: 'center' }, children: _jsx(Spinner, {}) }));
1111
+ }
1112
+ else if (isTopUpPricesError) {
1113
+ view = (_jsxs(Box, { children: [topCards, _jsx(Flash, { variant: "danger", children: topUpPricesError instanceof Error
1114
+ ? topUpPricesError.message
1115
+ : 'Unable to fetch the available products. Please try again later.' })] }));
1116
+ }
1117
+ else {
61
1118
  view = items.length ? (_jsxs(Box, { sx: { flex: '1 1 auto' }, onKeyDown: event => {
62
1119
  if (product && event.key === 'Enter') {
63
- setCheckout(true);
1120
+ void startCheckout();
64
1121
  }
65
- }, children: [_jsx(Text, { as: "h3", children: "Choose a credits package" }), _jsx(Box, { role: "radiogroup", sx: {
1122
+ }, children: [_jsxs(Box, { sx: {
1123
+ marginTop: 'var(--stack-gap-normal)',
1124
+ border: '1px solid',
1125
+ borderColor: 'border.default',
1126
+ borderRadius: 'var(--borderRadius-medium)',
1127
+ backgroundColor: 'canvas.default',
1128
+ padding: 'var(--stack-padding-normal)',
66
1129
  display: 'grid',
67
1130
  gap: 'var(--stack-gap-normal)',
68
- gridTemplateColumns: Array(items.length).fill('1fr').join(' '),
69
- padding: 'var(--stack-padding-normal) 0',
70
- }, children: items.map(item => (_jsx(Box, { role: "radio", "aria-labelledby": `checkout-price-${item.id}`, "aria-checked": product?.id === item.id, onClick: () => {
71
- setProduct(item);
72
- }, sx: {
73
- borderStyle: 'solid',
74
- borderRadius: 'var(--borderRadius-medium)',
75
- borderWidth: 'var(--borderWidth-thick)',
76
- borderColor: product?.id === item.id
77
- ? 'var(--borderColor-accent-emphasis)'
78
- : 'var(--borderColor-default)',
79
- padding: 'var(--stack-padding-condensed)',
80
- cursor: 'pointer',
81
- }, children: _jsxs(FormControl, { sx: {
82
- alignItems: 'center',
83
- }, children: [_jsx(FormControl.Label, { id: `checkout-price-${item.id}`, sx: { alignSelf: 'center' }, children: item.name }), _jsx(Text, { as: "p", children: new Intl.NumberFormat(undefined, {
84
- style: 'currency',
85
- currency: item.currency,
86
- }).format(item.amount / 100) }), _jsxs(Text, { as: "p", children: [item.credits, " credits"] })] }, item.id) }))) }), _jsx(Button, { variant: "primary", onClick: () => {
87
- setCheckout(true);
88
- }, disabled: product === null, sx: { float: 'right' }, children: "Checkout" })] })) : (_jsx(Box, { children: _jsx(Flash, { variant: "danger", children: "Unable to fetch the available products. Please try again later." }) }));
1131
+ gridTemplateColumns: shouldShowMonthlySubscriptionSection || currentPlanSection
1132
+ ? ['1fr', 'minmax(0, 1fr) minmax(0, 1fr)']
1133
+ : ['1fr'],
1134
+ alignItems: 'stretch',
1135
+ }, children: [shouldShowMonthlySubscriptionSection ? (_jsx(Box, { sx: {
1136
+ borderRight: ['none', '1px solid'],
1137
+ borderColor: 'border.muted',
1138
+ paddingRight: ['0', 'var(--stack-gap-normal)'],
1139
+ alignSelf: 'stretch',
1140
+ }, children: monthlySubscriptionSection })) : null, currentPlanSection, _jsx(Box, { sx: {
1141
+ paddingLeft: shouldShowMonthlySubscriptionSection || currentPlanSection
1142
+ ? ['0', 'var(--stack-gap-normal)']
1143
+ : 0,
1144
+ }, children: topUpSection }), isStripeTestConfiguration ? (_jsx(Box, { sx: {
1145
+ gridColumn: ['1', '1 / -1'],
1146
+ borderTop: '1px solid',
1147
+ borderColor: 'border.muted',
1148
+ paddingTop: 'var(--stack-gap-normal)',
1149
+ }, children: _jsx(Label, { variant: "attention", children: "Stripe test configuration" }) })) : null] }), resumeConfirmationMessage && (_jsx(Flash, { variant: "success", sx: { mt: 3 }, children: resumeConfirmationMessage })), paymentMessage && !resumeConfirmationMessage && (_jsx(Flash, { variant: "success", sx: { mt: 3 }, children: paymentMessage })), topCards] })) : (_jsxs(Box, { children: [resumeConfirmationMessage && (_jsx(Flash, { variant: "success", sx: { mt: 3 }, children: resumeConfirmationMessage })), paymentMessage && !resumeConfirmationMessage && (_jsx(Flash, { variant: "success", sx: { mt: 3 }, children: paymentMessage })), topCards, _jsx(Flash, { variant: "default", children: "No products are available yet." })] }));
89
1150
  }
90
1151
  return view;
91
1152
  }