@wopr-network/platform-ui-core 1.27.8 → 1.27.9

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 (353) hide show
  1. package/next.config.ts +1 -2
  2. package/package.json +17 -17
  3. package/src/__tests__/account-switcher.test.tsx +21 -20
  4. package/src/__tests__/activity-page.test.tsx +2 -6
  5. package/src/__tests__/add-payment-method-dialog.test.tsx +9 -32
  6. package/src/__tests__/admin-api.test.ts +1 -6
  7. package/src/__tests__/admin-gpu-api.test.ts +1 -3
  8. package/src/__tests__/admin-marketplace-api.test.ts +1 -4
  9. package/src/__tests__/admin-middleware.test.ts +76 -83
  10. package/src/__tests__/affiliate-dashboard.test.tsx +3 -3
  11. package/src/__tests__/api-401-redirect.test.ts +46 -9
  12. package/src/__tests__/api-client.test.ts +3 -5
  13. package/src/__tests__/api-config.test.ts +22 -42
  14. package/src/__tests__/api-fleet-resources.test.ts +1 -2
  15. package/src/__tests__/api-fleet-trpc.test.ts +2 -8
  16. package/src/__tests__/api-null-guards.test.ts +3 -1
  17. package/src/__tests__/audit-log-table-pagination.test.tsx +2 -6
  18. package/src/__tests__/auth-password-reset.test.tsx +7 -21
  19. package/src/__tests__/auth-redirect.test.tsx +8 -2
  20. package/src/__tests__/auth.test.tsx +25 -23
  21. package/src/__tests__/auto-topup-card.test.tsx +4 -12
  22. package/src/__tests__/backups-tab.test.tsx +3 -4
  23. package/src/__tests__/billing-layout-nav-hidden.test.tsx +5 -37
  24. package/src/__tests__/billing-payment-org-invoices.test.tsx +2 -18
  25. package/src/__tests__/billing.test.tsx +8 -39
  26. package/src/__tests__/bot-settings/resources-tab.test.tsx +1 -3
  27. package/src/__tests__/bot-settings/storage-tab.test.tsx +1 -3
  28. package/src/__tests__/bot-settings/vps-upgrade-card.test.tsx +1 -3
  29. package/src/__tests__/bot-settings-restart.test.tsx +1 -3
  30. package/src/__tests__/bot-settings.test.tsx +2 -6
  31. package/src/__tests__/brand.test.ts +6 -26
  32. package/src/__tests__/buy-credits-panel.test.tsx +1 -3
  33. package/src/__tests__/buy-crypto-credits-panel.test.tsx +101 -119
  34. package/src/__tests__/capability-conflicts.test.ts +2 -8
  35. package/src/__tests__/capability-resolver.test.tsx +2 -12
  36. package/src/__tests__/channel-wizard.test.tsx +4 -17
  37. package/src/__tests__/chat/chat-panel.test.tsx +1 -4
  38. package/src/__tests__/chat-store.test.ts +5 -15
  39. package/src/__tests__/command-center.test.tsx +10 -12
  40. package/src/__tests__/compliance-retention-edit.test.tsx +3 -6
  41. package/src/__tests__/confirmation-tracker.test.tsx +3 -18
  42. package/src/__tests__/coupon-input.test.tsx +1 -3
  43. package/src/__tests__/create-instance.test.tsx +1 -3
  44. package/src/__tests__/credit-balance.test.tsx +4 -12
  45. package/src/__tests__/credits.test.tsx +32 -85
  46. package/src/__tests__/email-verification-banner.test.tsx +2 -6
  47. package/src/__tests__/error-boundaries.test.tsx +0 -1
  48. package/src/__tests__/fetch-pricing.test.ts +2 -1
  49. package/src/__tests__/field-oauth.test.tsx +2 -6
  50. package/src/__tests__/fixtures/mock-manifests-data.js +1 -3
  51. package/src/__tests__/fixtures/mock-manifests.ts +2 -4
  52. package/src/__tests__/fleet-health-timestamp.test.tsx +1 -8
  53. package/src/__tests__/fleet-health-update.test.tsx +1 -8
  54. package/src/__tests__/gpu-dashboard.test.tsx +2 -6
  55. package/src/__tests__/instance-detail.test.tsx +3 -9
  56. package/src/__tests__/instance-list.test.tsx +1 -5
  57. package/src/__tests__/layout-snapshots.test.tsx +64 -11
  58. package/src/__tests__/marketplace-admin.test.tsx +2 -6
  59. package/src/__tests__/marketplace.test.tsx +11 -35
  60. package/src/__tests__/merge-api-rates.test.ts +1 -6
  61. package/src/__tests__/middleware.test.ts +32 -219
  62. package/src/__tests__/next-config-headers.test.ts +1 -3
  63. package/src/__tests__/notifications.test.tsx +4 -11
  64. package/src/__tests__/oauth-buttons.test.tsx +36 -59
  65. package/src/__tests__/oauth-error-mapping.test.tsx +2 -6
  66. package/src/__tests__/observability.test.tsx +23 -36
  67. package/src/__tests__/onboarding-page.test.tsx +4 -6
  68. package/src/__tests__/org-billing-api.test.tsx +1 -6
  69. package/src/__tests__/plugin-install-flow.test.tsx +28 -58
  70. package/src/__tests__/plugin-registry.test.tsx +3 -11
  71. package/src/__tests__/plugin-tool-sync.test.ts +1 -3
  72. package/src/__tests__/plugins-catalog-error.test.tsx +2 -6
  73. package/src/__tests__/plugins-toggle-race.test.tsx +3 -5
  74. package/src/__tests__/portfolio-chart.test.tsx +2 -6
  75. package/src/__tests__/promotion-form.test.tsx +2 -6
  76. package/src/__tests__/promotions-list.test.tsx +1 -3
  77. package/src/__tests__/provider-key-api.test.ts +2 -1
  78. package/src/__tests__/resend-verification-button.test.tsx +8 -24
  79. package/src/__tests__/secrets-audit-pagination.test.tsx +1 -3
  80. package/src/__tests__/settings.test.tsx +11 -21
  81. package/src/__tests__/setup-checklist.test.tsx +3 -9
  82. package/src/__tests__/setup.ts +25 -6
  83. package/src/__tests__/snapshot-api.test.ts +2 -1
  84. package/src/__tests__/step-superpowers.test.tsx +1 -3
  85. package/src/__tests__/tenant-context.test.tsx +1 -6
  86. package/src/__tests__/tenant-keys-api.test.ts +3 -4
  87. package/src/__tests__/tenant-table-pagination.test.tsx +2 -6
  88. package/src/__tests__/terminal-log-cleanup.test.tsx +0 -1
  89. package/src/__tests__/transaction-history.test.tsx +190 -238
  90. package/src/__tests__/trpc-types.test.ts +2 -6
  91. package/src/__tests__/use-chat.test.ts +1 -3
  92. package/src/__tests__/use-plugin-setup-chat-stale-closure.test.ts +1 -4
  93. package/src/__tests__/use-sidecar-bridge.test.tsx +105 -0
  94. package/src/__tests__/use-webmcp.test.ts +1 -3
  95. package/src/__tests__/validate-elevenlabs-key.test.ts +2 -1
  96. package/src/__tests__/verify-page.test.tsx +4 -13
  97. package/src/__tests__/verify-redirect.test.tsx +2 -6
  98. package/src/app/(auth)/error.tsx +1 -7
  99. package/src/app/(auth)/forgot-password/page.tsx +4 -18
  100. package/src/app/(auth)/login/page.tsx +5 -22
  101. package/src/app/(auth)/reset-password/page.tsx +2 -12
  102. package/src/app/(auth)/signup/page.tsx +10 -44
  103. package/src/app/(auth)/verify/page.tsx +47 -0
  104. package/src/app/(dashboard)/billing/credits/page.tsx +14 -67
  105. package/src/app/(dashboard)/billing/error.tsx +2 -10
  106. package/src/app/(dashboard)/billing/layout.tsx +12 -62
  107. package/src/app/(dashboard)/billing/payment/page.tsx +17 -68
  108. package/src/app/(dashboard)/billing/plans/page.tsx +3 -9
  109. package/src/app/(dashboard)/billing/usage/hosted/page.tsx +8 -25
  110. package/src/app/(dashboard)/billing/usage/page.tsx +63 -103
  111. package/src/app/(dashboard)/changesets/[id]/changeset-detail-client.tsx +9 -27
  112. package/src/app/(dashboard)/changesets/[id]/error.tsx +2 -6
  113. package/src/app/(dashboard)/changesets/error.tsx +1 -7
  114. package/src/app/(dashboard)/chat/page.tsx +2 -6
  115. package/src/app/(dashboard)/dashboard/network/page.tsx +5 -19
  116. package/src/app/(dashboard)/error.tsx +1 -7
  117. package/src/app/(dashboard)/layout.tsx +15 -36
  118. package/src/app/(dashboard)/marketplace/[plugin]/page.tsx +14 -51
  119. package/src/app/(dashboard)/marketplace/error.tsx +1 -7
  120. package/src/app/(dashboard)/marketplace/page.tsx +6 -27
  121. package/src/app/(dashboard)/not-found.tsx +2 -5
  122. package/src/app/(dashboard)/onboarding/page.tsx +5 -22
  123. package/src/app/(dashboard)/settings/account/page.tsx +1 -6
  124. package/src/app/(dashboard)/settings/activity/page.tsx +8 -34
  125. package/src/app/(dashboard)/settings/api-keys/page.tsx +15 -60
  126. package/src/app/(dashboard)/settings/brain/page.tsx +9 -31
  127. package/src/app/(dashboard)/settings/error.tsx +2 -10
  128. package/src/app/(dashboard)/settings/notifications/page.tsx +2 -6
  129. package/src/app/(dashboard)/settings/org/page.tsx +13 -56
  130. package/src/app/(dashboard)/settings/page.tsx +1 -0
  131. package/src/app/(dashboard)/settings/profile/page.tsx +126 -73
  132. package/src/app/(dashboard)/settings/providers/page.tsx +21 -78
  133. package/src/app/(dashboard)/settings/secrets/page.tsx +13 -58
  134. package/src/app/(dashboard)/settings/security/page.tsx +31 -111
  135. package/src/app/admin/email-templates/email-templates-client.tsx +15 -58
  136. package/src/app/admin/error.tsx +1 -7
  137. package/src/app/admin/fleet-updates/error.tsx +1 -7
  138. package/src/app/admin/fleet-updates/fleet-updates-client.tsx +10 -50
  139. package/src/app/admin/layout.tsx +4 -0
  140. package/src/app/admin/payment-methods/page.tsx +9 -38
  141. package/src/app/admin/products/error.tsx +2 -7
  142. package/src/app/admin/products/page.tsx +1 -4
  143. package/src/app/admin/promotions/[id]/page.tsx +9 -38
  144. package/src/app/admin/promotions/page.tsx +9 -36
  145. package/src/app/admin/rate-overrides/page.tsx +9 -45
  146. package/src/app/auth/callback/[provider]/page.tsx +1 -8
  147. package/src/app/auth/verify/page.tsx +9 -36
  148. package/src/app/channels/error.tsx +2 -10
  149. package/src/app/channels/layout.tsx +9 -0
  150. package/src/app/channels/page.tsx +8 -20
  151. package/src/app/channels/setup/[plugin]/page.tsx +3 -5
  152. package/src/app/error.tsx +1 -7
  153. package/src/app/fleet/error.tsx +1 -7
  154. package/src/app/fleet/layout.tsx +5 -0
  155. package/src/app/fleet/settings/page.tsx +1 -3
  156. package/src/app/global-error.tsx +2 -10
  157. package/src/app/globals.css +1 -4
  158. package/src/app/instances/[id]/instance-detail-client.tsx +51 -125
  159. package/src/app/instances/error.tsx +2 -10
  160. package/src/app/instances/instance-list-client.tsx +20 -69
  161. package/src/app/instances/layout.tsx +9 -0
  162. package/src/app/instances/new/create-instance-client.tsx +10 -31
  163. package/src/app/layout.tsx +2 -10
  164. package/src/app/not-found.tsx +1 -3
  165. package/src/app/page.tsx +1 -2
  166. package/src/app/plugins/error.tsx +2 -10
  167. package/src/app/plugins/layout.tsx +5 -0
  168. package/src/app/plugins/page.tsx +16 -48
  169. package/src/app/pricing/error.tsx +1 -7
  170. package/src/app/privacy/page.tsx +93 -150
  171. package/src/app/status/error.tsx +1 -7
  172. package/src/app/terms/page.tsx +89 -144
  173. package/src/components/account-switcher.tsx +25 -52
  174. package/src/components/admin/accounting-dashboard.tsx +1 -3
  175. package/src/components/admin/admin-guard.tsx +1 -3
  176. package/src/components/admin/admin-nav.tsx +1 -3
  177. package/src/components/admin/affiliate-dashboard.tsx +25 -94
  178. package/src/components/admin/audit-log-table.tsx +13 -49
  179. package/src/components/admin/billing-health-dashboard.tsx +7 -25
  180. package/src/components/admin/bulk-actions-bar.test.tsx +1 -7
  181. package/src/components/admin/bulk-actions-bar.tsx +1 -3
  182. package/src/components/admin/bulk-export-dialog.test.tsx +1 -7
  183. package/src/components/admin/bulk-export-dialog.tsx +6 -32
  184. package/src/components/admin/bulk-grant-dialog.test.tsx +2 -6
  185. package/src/components/admin/bulk-grant-dialog.tsx +4 -15
  186. package/src/components/admin/bulk-preview-dialog.tsx +3 -12
  187. package/src/components/admin/bulk-reactivate-dialog.tsx +1 -7
  188. package/src/components/admin/bulk-select-all-banner.tsx +1 -6
  189. package/src/components/admin/bulk-suspend-dialog.tsx +5 -12
  190. package/src/components/admin/bulk-undo-toast.tsx +1 -2
  191. package/src/components/admin/compliance-dashboard.tsx +31 -101
  192. package/src/components/admin/gpu-dashboard.tsx +21 -70
  193. package/src/components/admin/grant-credits-dialog.tsx +4 -17
  194. package/src/components/admin/incident-dashboard.tsx +10 -25
  195. package/src/components/admin/inference-dashboard.tsx +14 -54
  196. package/src/components/admin/marketplace-admin.tsx +18 -60
  197. package/src/components/admin/migrations-dashboard.tsx +9 -42
  198. package/src/components/admin/onboarding-dashboard.tsx +14 -64
  199. package/src/components/admin/pool-config-dashboard.tsx +4 -10
  200. package/src/components/admin/products/fleet-form.tsx +2 -11
  201. package/src/components/admin/products/nav-editor.tsx +3 -10
  202. package/src/components/admin/promotions/promotion-form.tsx +9 -42
  203. package/src/components/admin/roles-dashboard.tsx +7 -34
  204. package/src/components/admin/suspend-dialog.tsx +4 -11
  205. package/src/components/admin/tenant-notes-panel.tsx +1 -3
  206. package/src/components/admin/tenant-row-actions.tsx +4 -20
  207. package/src/components/admin/tenant-table.tsx +12 -49
  208. package/src/components/auth/auth-redirect.tsx +11 -3
  209. package/src/components/auth/email-verification-result-banner.tsx +1 -3
  210. package/src/components/auth/resend-verification-button.tsx +2 -10
  211. package/src/components/auth/wopr-wordmark.tsx +1 -3
  212. package/src/components/billing/add-payment-method-dialog.tsx +1 -2
  213. package/src/components/billing/affiliate-dashboard.tsx +4 -16
  214. package/src/components/billing/amount-selector.tsx +1 -3
  215. package/src/components/billing/auto-topup-card.tsx +2 -11
  216. package/src/components/billing/buy-credits-panel.tsx +4 -14
  217. package/src/components/billing/byok-callout.tsx +6 -8
  218. package/src/components/billing/confirmation-tracker.tsx +4 -14
  219. package/src/components/billing/credit-balance-badge.tsx +22 -0
  220. package/src/components/billing/credit-balance.tsx +3 -9
  221. package/src/components/billing/crypto-checkout.tsx +5 -24
  222. package/src/components/billing/degraded-state-banner.tsx +1 -3
  223. package/src/components/billing/deposit-view.tsx +301 -41
  224. package/src/components/billing/dividend-banner.tsx +1 -3
  225. package/src/components/billing/dividend-eligibility.tsx +3 -12
  226. package/src/components/billing/dividend-pool-stats.tsx +6 -20
  227. package/src/components/billing/first-dividend-dialog.tsx +2 -2
  228. package/src/components/billing/org-billing-page.tsx +8 -31
  229. package/src/components/billing/payment-method-picker.tsx +2 -10
  230. package/src/components/billing/suspension-banner.tsx +2 -7
  231. package/src/components/billing/transaction-history.tsx +10 -58
  232. package/src/components/billing/unified-checkout.tsx +547 -0
  233. package/src/components/bot-settings/backups-tab.tsx +9 -33
  234. package/src/components/bot-settings/bot-settings-client.tsx +32 -134
  235. package/src/components/bot-settings/resources-tab.tsx +2 -9
  236. package/src/components/bot-settings/storage-tab.tsx +19 -48
  237. package/src/components/bot-settings/vps-info-panel.tsx +3 -11
  238. package/src/components/bot-settings/vps-upgrade-card.tsx +3 -4
  239. package/src/components/brand-hydrator.tsx +13 -0
  240. package/src/components/channel-wizard/field-interactive.tsx +1 -3
  241. package/src/components/channel-wizard/field-qr.tsx +10 -39
  242. package/src/components/channel-wizard/step-renderer.tsx +5 -28
  243. package/src/components/channel-wizard/wizard.tsx +6 -31
  244. package/src/components/chat/chat-message.tsx +1 -4
  245. package/src/components/chat/chat-panel.tsx +4 -18
  246. package/src/components/chat/chat-widget.tsx +3 -14
  247. package/src/components/dashboard/command-center.tsx +15 -61
  248. package/src/components/fleet/update-settings-card.tsx +7 -23
  249. package/src/components/instance-update-banner.tsx +130 -0
  250. package/src/components/instances/friends-tab.test.tsx +2 -9
  251. package/src/components/instances/friends-tab.tsx +18 -74
  252. package/src/components/instances/update-available-badge.tsx +2 -11
  253. package/src/components/landing/hero.tsx +3 -9
  254. package/src/components/landing/landing-page.tsx +1 -3
  255. package/src/components/landing/portfolio-chart.tsx +4 -9
  256. package/src/components/landing/story-sections.tsx +1 -3
  257. package/src/components/landing/terminal-sequence.tsx +4 -17
  258. package/src/components/marketplace/empty-state.tsx +2 -6
  259. package/src/components/marketplace/first-visit-hero.tsx +1 -3
  260. package/src/components/marketplace/install-wizard.tsx +20 -77
  261. package/src/components/marketplace/marketplace-tabs.tsx +1 -4
  262. package/src/components/marketplace/plugin-card.tsx +2 -9
  263. package/src/components/marketplace/superpower-content.tsx +1 -3
  264. package/src/components/marketplace/terminal-search.tsx +2 -8
  265. package/src/components/oauth-buttons.tsx +29 -14
  266. package/src/components/observability/fleet-health.tsx +5 -18
  267. package/src/components/observability/health-overview.tsx +7 -20
  268. package/src/components/observability/logs-viewer.tsx +8 -32
  269. package/src/components/observability/metrics-dashboard.tsx +2 -15
  270. package/src/components/onboarding/fallback-setup.tsx +6 -25
  271. package/src/components/onboarding/setup-checklist.tsx +18 -51
  272. package/src/components/onboarding/step-superpowers.tsx +1 -4
  273. package/src/components/plugin-setup/setup-chat-panel.tsx +6 -22
  274. package/src/components/pricing/dividend-calculator.tsx +6 -12
  275. package/src/components/pricing/dividend-stats.tsx +5 -17
  276. package/src/components/pricing/pricing-page.tsx +17 -36
  277. package/src/components/settings/create-org-wizard.tsx +2 -5
  278. package/src/components/sidebar.tsx +7 -42
  279. package/src/components/sidecar-frame.tsx +78 -0
  280. package/src/components/status/status-page.tsx +6 -28
  281. package/src/components/ui/alert-dialog.tsx +8 -25
  282. package/src/components/ui/badge.tsx +2 -8
  283. package/src/components/ui/banner.tsx +1 -6
  284. package/src/components/ui/card.tsx +5 -24
  285. package/src/components/ui/checkbox.tsx +1 -5
  286. package/src/components/ui/collapsible.tsx +3 -8
  287. package/src/components/ui/dialog.tsx +4 -10
  288. package/src/components/ui/dropdown-menu.tsx +9 -18
  289. package/src/components/ui/form.tsx +2 -16
  290. package/src/components/ui/popover.tsx +3 -23
  291. package/src/components/ui/progress.tsx +1 -5
  292. package/src/components/ui/radio-group.tsx +3 -15
  293. package/src/components/ui/select.tsx +4 -17
  294. package/src/components/ui/sheet.tsx +5 -19
  295. package/src/components/ui/skeleton.tsx +1 -7
  296. package/src/components/ui/table.tsx +5 -22
  297. package/src/components/ui/tabs.tsx +3 -13
  298. package/src/components/ui/tooltip.tsx +1 -1
  299. package/src/components/unified-sidebar.tsx +493 -0
  300. package/src/hooks/__tests__/use-fleet-sse.test.ts +1 -4
  301. package/src/hooks/__tests__/use-save-queue.test.ts +2 -8
  302. package/src/hooks/use-credit-balance.ts +27 -0
  303. package/src/hooks/use-my-org-role.ts +1 -3
  304. package/src/hooks/use-plugin-registry.ts +8 -14
  305. package/src/hooks/use-plugin-setup-chat.ts +2 -5
  306. package/src/hooks/use-sidecar-bridge.tsx +148 -0
  307. package/src/hooks/use-webmcp.ts +1 -4
  308. package/src/lib/__tests__/admin-api.test.ts +1 -3
  309. package/src/lib/__tests__/api-bot-crud.test.ts +8 -18
  310. package/src/lib/__tests__/api-fetch.test.ts +4 -16
  311. package/src/lib/__tests__/org-billing-api.test.ts +1 -3
  312. package/src/lib/__tests__/pricing-data.test.ts +0 -8
  313. package/src/lib/__tests__/settings-api.test.ts +1 -3
  314. package/src/lib/admin-affiliate-api.ts +2 -7
  315. package/src/lib/admin-api.ts +6 -26
  316. package/src/lib/admin-incident-api.ts +11 -19
  317. package/src/lib/admin-marketplace-api.ts +1 -5
  318. package/src/lib/api-config.test.ts +5 -50
  319. package/src/lib/api.ts +143 -122
  320. package/src/lib/auth-client.ts +1 -2
  321. package/src/lib/bot-settings-data.ts +11 -36
  322. package/src/lib/brand-config.ts +56 -115
  323. package/src/lib/brand.ts +2 -15
  324. package/src/lib/chat/use-chat.ts +2 -7
  325. package/src/lib/cost-comparison-data.test.ts +1 -3
  326. package/src/lib/cost-comparison-data.ts +1 -4
  327. package/src/lib/errors.ts +1 -4
  328. package/src/lib/fetch-utils.test.ts +26 -9
  329. package/src/lib/fetch-utils.ts +40 -11
  330. package/src/lib/logger.ts +2 -0
  331. package/src/lib/marketplace-data.ts +3 -11
  332. package/src/lib/oauth-errors.ts +2 -4
  333. package/src/lib/onboarding-data.ts +3 -11
  334. package/src/lib/org-api.ts +2 -10
  335. package/src/lib/org-billing-api.ts +5 -19
  336. package/src/lib/plugin/tool-definitions.ts +1 -2
  337. package/src/lib/require-auth.ts +57 -0
  338. package/src/lib/settings-api.ts +1 -4
  339. package/src/lib/sidecar-routes.ts +43 -0
  340. package/src/lib/trpc-server.ts +49 -0
  341. package/src/lib/trpc-types.ts +4 -6
  342. package/src/lib/trpc.tsx +12 -4
  343. package/src/lib/validate-redirect-url.ts +1 -4
  344. package/src/lib/webmcp/marketplace-onboarding-tools.ts +6 -16
  345. package/src/lib/webmcp/register.ts +1 -4
  346. package/src/lib/webmcp/tools.ts +2 -9
  347. package/src/proxy.ts +35 -212
  348. package/src/types/missing-deps.d.ts +2 -8
  349. package/tsconfig.json +1 -8
  350. package/biome.json +0 -52
  351. package/src/__tests__/__snapshots__/layout-snapshots.test.tsx.snap +0 -741
  352. package/src/__tests__/billing-byok-callout.test.tsx +0 -76
  353. package/src/lib/__tests__/__snapshots__/pricing-data.test.ts.snap +0 -112
@@ -0,0 +1,547 @@
1
+ "use client";
2
+
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+ import { AnimatePresence, motion } from "framer-motion";
5
+ import { CircleDollarSign, CreditCard } from "lucide-react";
6
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
7
+ import { useCallback, useEffect, useMemo, useState } from "react";
8
+ import { toast } from "sonner";
9
+ import { Button } from "@/components/ui/button";
10
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
11
+ import { Input } from "@/components/ui/input";
12
+ import {
13
+ type CheckoutResult,
14
+ type CreditOption,
15
+ createCheckout,
16
+ createCreditCheckout,
17
+ getChargeStatus,
18
+ getCreditOptions,
19
+ getSupportedPaymentMethods,
20
+ type SupportedPaymentMethod,
21
+ } from "@/lib/api";
22
+ import { cn } from "@/lib/utils";
23
+ import { isAllowedRedirectUrl } from "@/lib/validate-redirect-url";
24
+ import { ConfirmationTracker } from "./confirmation-tracker";
25
+ import { DepositView } from "./deposit-view";
26
+
27
+ const PRESETS = [10, 25, 50, 100];
28
+ const MIN_AMOUNT = 10;
29
+
30
+ type Step = "amount" | "method" | "chain" | "deposit" | "confirming";
31
+ type PaymentStatus = "waiting" | "partial" | "confirming" | "credited" | "expired" | "failed";
32
+
33
+ const slide = {
34
+ initial: { opacity: 0, x: -20 },
35
+ animate: { opacity: 1, x: 0 },
36
+ exit: { opacity: 0, x: 20 },
37
+ };
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // LocalStorage helpers — persist pending crypto charges so users can resume
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function storePendingCharge(result: CheckoutResult) {
44
+ try {
45
+ localStorage.setItem(`pending_charge_${result.referenceId}`, JSON.stringify(result));
46
+ } catch {
47
+ /* quota exceeded — non-critical */
48
+ }
49
+ }
50
+
51
+ function loadPendingCharge(referenceId: string): CheckoutResult | null {
52
+ try {
53
+ const raw = localStorage.getItem(`pending_charge_${referenceId}`);
54
+ return raw ? (JSON.parse(raw) as CheckoutResult) : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function clearPendingCharge(referenceId: string) {
61
+ try {
62
+ localStorage.removeItem(`pending_charge_${referenceId}`);
63
+ } catch {
64
+ /* ignore */
65
+ }
66
+ }
67
+
68
+ // Global dedup — prevents double toast/invalidation from desktop+mobile dual mount
69
+ const notifiedCharges = new Set<string>();
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // UnifiedCheckout — Amount → Method (Card + Coins) → Chain → Deposit → Confirm
73
+ // ---------------------------------------------------------------------------
74
+
75
+ export function UnifiedCheckout() {
76
+ const searchParams = useSearchParams();
77
+ const pathname = usePathname();
78
+ const router = useRouter();
79
+ const queryClient = useQueryClient();
80
+
81
+ // Wizard state
82
+ const [step, setStep] = useState<Step>("amount");
83
+ const [amountUsd, setAmountUsd] = useState(0);
84
+ const [selectedCoin, setSelectedCoin] = useState<string | null>(null);
85
+ const [checkout, setCheckout] = useState<CheckoutResult | null>(null);
86
+ const [status, setStatus] = useState<PaymentStatus>("waiting");
87
+ const [confirmations, setConfirmations] = useState(0);
88
+ const [confirmationsRequired, setConfirmationsRequired] = useState(0);
89
+ const [loading, setLoading] = useState(false);
90
+ const [error, setError] = useState<string | null>(null);
91
+ const [expectedAmount, setExpectedAmount] = useState<string | null>(null);
92
+ const [receivedAmount, setReceivedAmount] = useState<string | null>(null);
93
+ const [chargeToken, setChargeToken] = useState<string | null>(null);
94
+ const [chargeDecimals, setChargeDecimals] = useState(18);
95
+
96
+ // Amount input
97
+ const [selected, setSelected] = useState<number | null>(null);
98
+ const [custom, setCustom] = useState("");
99
+
100
+ // Data sources
101
+ const [creditTiers, setCreditTiers] = useState<CreditOption[]>([]);
102
+ const [cryptoMethods, setCryptoMethods] = useState<SupportedPaymentMethod[]>([]);
103
+ const [dataReady, setDataReady] = useState(false);
104
+
105
+ // ── Group crypto methods by token ──────────────────────────────────────
106
+ const coinGroups = useMemo(() => {
107
+ const groups = new Map<string, SupportedPaymentMethod[]>();
108
+ for (const m of cryptoMethods) {
109
+ const existing = groups.get(m.token) ?? [];
110
+ existing.push(m);
111
+ groups.set(m.token, existing);
112
+ }
113
+ return groups;
114
+ }, [cryptoMethods]);
115
+
116
+ // Chains for the currently selected coin
117
+ const chainsForCoin = useMemo(
118
+ () => (selectedCoin ? (coinGroups.get(selectedCoin) ?? []) : []),
119
+ [selectedCoin, coinGroups],
120
+ );
121
+
122
+ // ── Load Stripe tiers + crypto methods concurrently ────────────────────
123
+ useEffect(() => {
124
+ let mounted = true;
125
+ Promise.allSettled([getCreditOptions(), getSupportedPaymentMethods()]).then(([tiersResult, methodsResult]) => {
126
+ if (!mounted) return;
127
+ if (tiersResult.status === "fulfilled") setCreditTiers(tiersResult.value);
128
+ if (methodsResult.status === "fulfilled") setCryptoMethods(methodsResult.value);
129
+ setDataReady(true);
130
+ });
131
+ return () => {
132
+ mounted = false;
133
+ };
134
+ }, []);
135
+
136
+ // ── Resume pending crypto charge from URL (?charge=ref_xxx) ────────────
137
+ const chargeRef = searchParams.get("charge");
138
+ useEffect(() => {
139
+ if (!chargeRef) return;
140
+ const stored = loadPendingCharge(chargeRef);
141
+ if (!stored) {
142
+ router.replace(pathname);
143
+ return;
144
+ }
145
+ setCheckout(stored);
146
+ setAmountUsd(stored.amountUsd);
147
+ getChargeStatus(chargeRef)
148
+ .then((res) => {
149
+ setConfirmations(res.confirmations);
150
+ setConfirmationsRequired(res.confirmationsRequired);
151
+ if (res.credited) {
152
+ setStatus("credited");
153
+ setStep("confirming");
154
+ clearPendingCharge(chargeRef);
155
+ if (!notifiedCharges.has(chargeRef)) {
156
+ notifiedCharges.add(chargeRef);
157
+ queryClient.invalidateQueries({ queryKey: [["billing"]] });
158
+ toast.success("Payment confirmed — credits added to your account");
159
+ }
160
+ } else if (res.status === "expired" || res.status === "failed") {
161
+ setStatus(res.status as PaymentStatus);
162
+ clearPendingCharge(chargeRef);
163
+ setStep("amount");
164
+ router.replace(pathname);
165
+ } else if (res.amountReceivedCents > 0 && res.amountReceivedCents >= res.amountExpectedCents) {
166
+ setStatus("confirming");
167
+ setStep("confirming");
168
+ } else if (res.amountReceivedCents > 0) {
169
+ setStatus("partial");
170
+ setStep("deposit");
171
+ } else {
172
+ setStatus("waiting");
173
+ setStep("deposit");
174
+ }
175
+ })
176
+ .catch(() => {
177
+ clearPendingCharge(chargeRef);
178
+ router.replace(pathname);
179
+ });
180
+ }, [chargeRef, pathname, router, queryClient.invalidateQueries]);
181
+
182
+ // ── Poll charge status every 5s while on deposit/confirming ────────────
183
+ useEffect(() => {
184
+ if (!checkout?.referenceId) return;
185
+ const interval = setInterval(async () => {
186
+ try {
187
+ const res = await getChargeStatus(checkout.referenceId);
188
+ setConfirmations(res.confirmations);
189
+ setConfirmationsRequired(res.confirmationsRequired);
190
+ setExpectedAmount(res.expectedAmount ?? null);
191
+ setReceivedAmount(res.receivedAmount ?? null);
192
+ if (res.token) setChargeToken(res.token);
193
+ if (res.decimals != null) setChargeDecimals(res.decimals);
194
+ if (res.credited) {
195
+ setStatus("credited");
196
+ setStep("confirming");
197
+ clearPendingCharge(checkout.referenceId);
198
+ clearInterval(interval);
199
+ if (!notifiedCharges.has(checkout.referenceId)) {
200
+ notifiedCharges.add(checkout.referenceId);
201
+ queryClient.invalidateQueries({ queryKey: [["billing"]] });
202
+ toast.success("Payment confirmed — credits added to your account");
203
+ }
204
+ } else if (res.status === "expired" || res.status === "failed") {
205
+ setStatus(res.status as PaymentStatus);
206
+ clearPendingCharge(checkout.referenceId);
207
+ clearInterval(interval);
208
+ } else if (res.amountReceivedCents > 0 && res.amountReceivedCents >= res.amountExpectedCents) {
209
+ setStatus("confirming");
210
+ setStep("confirming");
211
+ } else if (res.amountReceivedCents > 0) {
212
+ setStatus("partial");
213
+ }
214
+ } catch {
215
+ /* ignore poll errors */
216
+ }
217
+ }, 5000);
218
+ return () => clearInterval(interval);
219
+ }, [checkout?.referenceId, queryClient.invalidateQueries]);
220
+
221
+ // ── Derived state ──────────────────────────────────────────────────────
222
+ const activeAmount = custom ? Number(custom) : selected;
223
+ const isValidAmount = activeAmount != null && activeAmount >= MIN_AMOUNT && Number.isFinite(activeAmount);
224
+ const hasMatchingTier = creditTiers.some((t) => t.amountCents === amountUsd * 100);
225
+
226
+ // ── Handlers ───────────────────────────────────────────────────────────
227
+
228
+ const handleContinueToMethod = useCallback(() => {
229
+ if (isValidAmount && activeAmount != null) {
230
+ setAmountUsd(activeAmount);
231
+ setError(null);
232
+ setStep("method");
233
+ }
234
+ }, [isValidAmount, activeAmount]);
235
+
236
+ const handleCardCheckout = useCallback(async () => {
237
+ const tier = creditTiers.find((t) => t.amountCents === amountUsd * 100);
238
+ if (!tier) return;
239
+ setLoading(true);
240
+ setError(null);
241
+ try {
242
+ const { checkoutUrl } = await createCreditCheckout(tier.priceId);
243
+ if (isAllowedRedirectUrl(checkoutUrl)) {
244
+ window.location.href = checkoutUrl;
245
+ } else {
246
+ setError("Unexpected checkout URL.");
247
+ setLoading(false);
248
+ }
249
+ } catch {
250
+ setError("Card checkout failed. Please try again.");
251
+ setLoading(false);
252
+ }
253
+ }, [amountUsd, creditTiers]);
254
+
255
+ const handleCryptoCheckout = useCallback(
256
+ async (method: SupportedPaymentMethod) => {
257
+ setLoading(true);
258
+ setError(null);
259
+ try {
260
+ const result = await createCheckout(method.id, amountUsd);
261
+ setCheckout(result);
262
+ setStatus("waiting");
263
+ setStep("deposit");
264
+ storePendingCharge(result);
265
+ router.replace(`${pathname}?charge=${result.referenceId}`);
266
+ } catch {
267
+ setError("Crypto checkout failed. Please try again.");
268
+ } finally {
269
+ setLoading(false);
270
+ }
271
+ },
272
+ [amountUsd, pathname, router],
273
+ );
274
+
275
+ const handleCoinSelect = useCallback(
276
+ (token: string) => {
277
+ const methods = coinGroups.get(token);
278
+ if (!methods || methods.length === 0) return;
279
+ setError(null);
280
+ if (methods.length === 1) {
281
+ handleCryptoCheckout(methods[0]);
282
+ } else {
283
+ setSelectedCoin(token);
284
+ setStep("chain");
285
+ }
286
+ },
287
+ [coinGroups, handleCryptoCheckout],
288
+ );
289
+
290
+ const handleReset = useCallback(() => {
291
+ if (checkout?.referenceId) clearPendingCharge(checkout.referenceId);
292
+ setStep("amount");
293
+ setCheckout(null);
294
+ setStatus("waiting");
295
+ setAmountUsd(0);
296
+ setConfirmations(0);
297
+ setConfirmationsRequired(0);
298
+ setSelected(null);
299
+ setSelectedCoin(null);
300
+ setCustom("");
301
+ setError(null);
302
+ router.replace(pathname);
303
+ }, [checkout?.referenceId, pathname, router]);
304
+
305
+ // ── Render ─────────────────────────────────────────────────────────────
306
+
307
+ if (!dataReady) return null;
308
+ if (creditTiers.length === 0 && cryptoMethods.length === 0) return null;
309
+
310
+ return (
311
+ <motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
312
+ <Card>
313
+ <CardHeader>
314
+ <CardTitle className="flex items-center gap-2">
315
+ <CircleDollarSign className="h-4 w-4 text-primary" />
316
+ Buy Credits
317
+ </CardTitle>
318
+ </CardHeader>
319
+ <CardContent>
320
+ <AnimatePresence mode="wait">
321
+ {/* ── Step 1: Amount ─────────────────────────────────────── */}
322
+ {step === "amount" && (
323
+ <motion.div key="amount" {...slide}>
324
+ <div className="space-y-4">
325
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
326
+ {PRESETS.map((amt) => (
327
+ <button
328
+ key={amt}
329
+ type="button"
330
+ onClick={() => {
331
+ setSelected(amt);
332
+ setCustom("");
333
+ }}
334
+ className={cn(
335
+ "rounded-md border p-3 text-lg font-bold transition-colors hover:bg-accent",
336
+ selected === amt && !custom
337
+ ? "border-primary bg-primary/5 ring-1 ring-primary"
338
+ : "border-border",
339
+ )}
340
+ >
341
+ ${amt}
342
+ </button>
343
+ ))}
344
+ </div>
345
+ <Input
346
+ type="number"
347
+ min={MIN_AMOUNT}
348
+ placeholder="Custom amount ($10 minimum)..."
349
+ value={custom}
350
+ onChange={(e) => {
351
+ setCustom(e.target.value);
352
+ setSelected(null);
353
+ }}
354
+ />
355
+ <Button onClick={handleContinueToMethod} disabled={!isValidAmount} className="w-full">
356
+ Continue to payment
357
+ </Button>
358
+ </div>
359
+ </motion.div>
360
+ )}
361
+
362
+ {/* ── Step 2: Method — Card + Coins ─────────────────────── */}
363
+ {step === "method" && (
364
+ <motion.div key="method" {...slide}>
365
+ <div className="space-y-4">
366
+ <button
367
+ type="button"
368
+ onClick={() => setStep("amount")}
369
+ className="text-sm text-muted-foreground hover:text-foreground"
370
+ >
371
+ &larr; Back
372
+ </button>
373
+
374
+ <p className="text-center text-sm font-medium">${amountUsd.toFixed(0)}</p>
375
+
376
+ {/* Card (Stripe) */}
377
+ {hasMatchingTier && (
378
+ <>
379
+ <button
380
+ type="button"
381
+ onClick={handleCardCheckout}
382
+ disabled={loading}
383
+ className="flex w-full items-center gap-3 rounded-lg border border-border p-4 text-left transition-colors hover:bg-accent hover:border-primary"
384
+ >
385
+ <div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
386
+ <CreditCard className="h-5 w-5 text-primary" />
387
+ </div>
388
+ <div>
389
+ <div className="text-sm font-medium">Pay with Card</div>
390
+ <div className="text-xs text-muted-foreground">Visa, Mastercard, AMEX &mdash; instant</div>
391
+ </div>
392
+ </button>
393
+
394
+ {coinGroups.size > 0 && (
395
+ <div className="relative">
396
+ <div className="absolute inset-0 flex items-center">
397
+ <span className="w-full border-t" />
398
+ </div>
399
+ <div className="relative flex justify-center text-xs uppercase">
400
+ <span className="bg-card px-2 text-muted-foreground">or pay with crypto</span>
401
+ </div>
402
+ </div>
403
+ )}
404
+ </>
405
+ )}
406
+
407
+ {/* Coin grid — one button per unique token */}
408
+ {coinGroups.size > 0 && (
409
+ <div className="grid grid-cols-3 gap-2 sm:grid-cols-4">
410
+ {Array.from(coinGroups.entries()).map(([token, methods]) => {
411
+ const first = methods[0];
412
+ return (
413
+ <button
414
+ key={token}
415
+ type="button"
416
+ disabled={loading}
417
+ onClick={() => handleCoinSelect(token)}
418
+ className="flex flex-col items-center gap-1.5 rounded-lg border border-border p-3 transition-colors hover:bg-accent hover:border-primary"
419
+ >
420
+ {first.iconUrl && (
421
+ // biome-ignore lint/performance/noImgElement: external dynamic URLs
422
+ <img
423
+ src={first.iconUrl}
424
+ alt={token}
425
+ className="h-8 w-8 rounded-full"
426
+ loading="lazy"
427
+ onError={(e) => {
428
+ e.currentTarget.style.display = "none";
429
+ }}
430
+ />
431
+ )}
432
+ <span className="text-xs font-medium">{token}</span>
433
+ {methods.length > 1 && (
434
+ <span className="text-[10px] text-muted-foreground">{methods.length} chains</span>
435
+ )}
436
+ </button>
437
+ );
438
+ })}
439
+ </div>
440
+ )}
441
+
442
+ {loading && (
443
+ <p className="mt-2 text-center text-xs text-muted-foreground animate-pulse">Creating checkout...</p>
444
+ )}
445
+ {error && <p className="mt-2 text-center text-sm text-destructive">{error}</p>}
446
+ </div>
447
+ </motion.div>
448
+ )}
449
+
450
+ {/* ── Step 3: Chain picker (multi-chain tokens) ─────────── */}
451
+ {step === "chain" && selectedCoin && (
452
+ <motion.div key="chain" {...slide}>
453
+ <div className="space-y-4">
454
+ <button
455
+ type="button"
456
+ onClick={() => {
457
+ setSelectedCoin(null);
458
+ setStep("method");
459
+ }}
460
+ className="text-sm text-muted-foreground hover:text-foreground"
461
+ >
462
+ &larr; Back
463
+ </button>
464
+
465
+ <p className="text-center text-sm font-medium">
466
+ ${amountUsd.toFixed(0)} in {selectedCoin}
467
+ </p>
468
+ <p className="text-center text-xs text-muted-foreground">Choose a network</p>
469
+
470
+ <div className="space-y-2">
471
+ {chainsForCoin.map((m) => (
472
+ <button
473
+ key={m.id}
474
+ type="button"
475
+ disabled={loading}
476
+ onClick={() => handleCryptoCheckout(m)}
477
+ className="flex w-full items-center justify-between rounded-lg border border-border p-3 text-left transition-colors hover:bg-accent hover:border-primary"
478
+ >
479
+ <div className="flex items-center gap-3">
480
+ {m.iconUrl && (
481
+ // biome-ignore lint/performance/noImgElement: external dynamic URLs
482
+ <img
483
+ src={m.iconUrl}
484
+ alt={m.chain}
485
+ className="h-7 w-7 rounded-full"
486
+ loading="lazy"
487
+ onError={(e) => {
488
+ e.currentTarget.style.display = "none";
489
+ }}
490
+ />
491
+ )}
492
+ <div>
493
+ <div className="text-sm font-medium">{m.chain}</div>
494
+ <div className="text-xs text-muted-foreground">
495
+ {m.type === "erc20" ? "ERC-20" : "Native"}
496
+ </div>
497
+ </div>
498
+ </div>
499
+ </button>
500
+ ))}
501
+ </div>
502
+
503
+ {loading && (
504
+ <p className="mt-2 text-center text-xs text-muted-foreground animate-pulse">Creating checkout...</p>
505
+ )}
506
+ {error && <p className="mt-2 text-center text-sm text-destructive">{error}</p>}
507
+ </div>
508
+ </motion.div>
509
+ )}
510
+
511
+ {/* ── Step 4: Deposit (crypto only) ──────────────────────── */}
512
+ {step === "deposit" && checkout && (
513
+ <motion.div key="deposit" {...slide}>
514
+ <DepositView
515
+ checkout={checkout}
516
+ status={status}
517
+ onBack={() => setStep("method")}
518
+ expectedAmount={expectedAmount}
519
+ receivedAmount={receivedAmount}
520
+ token={chargeToken ?? undefined}
521
+ decimals={chargeDecimals}
522
+ />
523
+ </motion.div>
524
+ )}
525
+
526
+ {/* ── Step 5: Confirmation (crypto only) ─────────────────── */}
527
+ {step === "confirming" && checkout && (
528
+ <motion.div key="confirming" {...slide}>
529
+ <ConfirmationTracker
530
+ confirmations={confirmations}
531
+ confirmationsRequired={confirmationsRequired}
532
+ displayAmount={checkout.displayAmount}
533
+ credited={status === "credited"}
534
+ />
535
+ {status === "credited" && (
536
+ <button type="button" onClick={handleReset} className="mt-4 text-sm text-primary hover:underline">
537
+ Done &mdash; buy more credits
538
+ </button>
539
+ )}
540
+ </motion.div>
541
+ )}
542
+ </AnimatePresence>
543
+ </CardContent>
544
+ </Card>
545
+ </motion.div>
546
+ );
547
+ }
@@ -15,13 +15,7 @@ import {
15
15
  DialogTitle,
16
16
  } from "@/components/ui/dialog";
17
17
  import { Input } from "@/components/ui/input";
18
- import {
19
- createSnapshot,
20
- deleteSnapshot,
21
- listSnapshots,
22
- restoreSnapshot,
23
- type Snapshot,
24
- } from "@/lib/api";
18
+ import { createSnapshot, deleteSnapshot, listSnapshots, restoreSnapshot, type Snapshot } from "@/lib/api";
25
19
  import { toUserMessage } from "@/lib/errors";
26
20
 
27
21
  function formatDate(iso: string): string {
@@ -55,13 +49,7 @@ function typeBadge(snap: Snapshot): {
55
49
  }
56
50
  }
57
51
 
58
- export function BackupsTab({
59
- botId,
60
- onRestore,
61
- }: {
62
- botId: string;
63
- onRestore?: () => void | Promise<void>;
64
- }) {
52
+ export function BackupsTab({ botId, onRestore }: { botId: string; onRestore?: () => void | Promise<void> }) {
65
53
  const [snapshots, setSnapshots] = useState<Snapshot[]>([]);
66
54
  const [loading, setLoading] = useState(true);
67
55
  const [error, setError] = useState<string | null>(null);
@@ -173,11 +161,7 @@ export function BackupsTab({
173
161
  }
174
162
 
175
163
  if (loading) {
176
- return (
177
- <div className="flex h-40 items-center justify-center text-muted-foreground">
178
- Loading backups...
179
- </div>
180
- );
164
+ return <div className="flex h-40 items-center justify-center text-muted-foreground">Loading backups...</div>;
181
165
  }
182
166
 
183
167
  return (
@@ -196,9 +180,7 @@ export function BackupsTab({
196
180
  </div>
197
181
 
198
182
  {error && (
199
- <div className="rounded-sm border border-red-500/25 bg-red-500/10 px-4 py-3 text-sm text-red-400">
200
- {error}
201
- </div>
183
+ <div className="rounded-sm border border-red-500/25 bg-red-500/10 px-4 py-3 text-sm text-red-400">{error}</div>
202
184
  )}
203
185
 
204
186
  {error ? (
@@ -224,10 +206,7 @@ export function BackupsTab({
224
206
  {snapshots.map((snap) => {
225
207
  const badge = typeBadge(snap);
226
208
  return (
227
- <Card
228
- key={snap.id}
229
- className="transition-colors hover:border-primary/50 hover:bg-accent/30"
230
- >
209
+ <Card key={snap.id} className="transition-colors hover:border-primary/50 hover:bg-accent/30">
231
210
  <CardContent className="flex items-center justify-between p-4">
232
211
  <div className="space-y-1">
233
212
  <div className="flex items-center gap-2">
@@ -241,9 +220,7 @@ export function BackupsTab({
241
220
  </span>
242
221
  <span>{snap.sizeMb} MB</span>
243
222
  {snap.expiresAt && (
244
- <span>
245
- Expires {formatDate(new Date(snap.expiresAt * 1000).toISOString())}
246
- </span>
223
+ <span>Expires {formatDate(new Date(snap.expiresAt * 1000).toISOString())}</span>
247
224
  )}
248
225
  </div>
249
226
  </div>
@@ -278,8 +255,7 @@ export function BackupsTab({
278
255
  <DialogHeader>
279
256
  <DialogTitle>Create Backup</DialogTitle>
280
257
  <DialogDescription>
281
- Save a checkpoint of your bot's current state. You can restore it later if something
282
- breaks.
258
+ Save a checkpoint of your bot's current state. You can restore it later if something breaks.
283
259
  </DialogDescription>
284
260
  </DialogHeader>
285
261
  <Input
@@ -319,8 +295,8 @@ export function BackupsTab({
319
295
  <DialogTitle>Are you sure?</DialogTitle>
320
296
  <DialogDescription>
321
297
  This will overwrite your bot's current state with the backup from{" "}
322
- <strong>{restoreTarget ? formatDate(restoreTarget.createdAt) : ""}</strong>. A
323
- pre-restore backup will be created automatically.
298
+ <strong>{restoreTarget ? formatDate(restoreTarget.createdAt) : ""}</strong>. A pre-restore backup will be
299
+ created automatically.
324
300
  </DialogDescription>
325
301
  </DialogHeader>
326
302
  <DialogFooter>