From f9f08a021dcd8f7177a2bed79b644d1d81f859ea Mon Sep 17 00:00:00 2001 From: yusufipk Date: Tue, 18 Aug 2026 11:57:17 +0300 Subject: [PATCH] fix(upload): give a full trial account the upgrade link where it reads the refusal The video uploader threw the API error away and toasted the bare message, so the one caller most likely to hit the trial storage ceiling was the one that lost the way out of it. Route it through toastApiError, which keeps the error code and attaches the action, and repeat the link in the queue row so it survives the toast timing out. The button now says Upgrade rather than See plans, matching the verb the message itself uses. --- components/video-drag-drop-uploader.tsx | 25 ++++++++++++++++++++++--- lib/client/api-error.ts | 14 ++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/components/video-drag-drop-uploader.tsx b/components/video-drag-drop-uploader.tsx index a337551..adb8498 100644 --- a/components/video-drag-drop-uploader.tsx +++ b/components/video-drag-drop-uploader.tsx @@ -1,6 +1,7 @@ 'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { CheckCircle2, Loader2, UploadCloud, XCircle } from 'lucide-react'; import { toast } from 'sonner'; @@ -22,6 +23,7 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn'; +import { isTrialStorageError, toastApiError } from '@/lib/client/api-error'; import { cleanupPendingProjectUpload, getDefaultTitleFromFile, @@ -46,6 +48,8 @@ type QueueItem = { status: QueueItemStatus; progress: number; error?: string; + /** The failure was the trial ceiling, so the row shows the way out. */ + errorIsTrialLimit?: boolean; }; interface VideoDragDropUploaderProps { @@ -306,12 +310,17 @@ export function VideoDragDropUploader({ failCount += 1; const message = error instanceof Error ? error.message : 'Failed to upload video'; + const isTrialLimit = isTrialStorageError(error); setQueue((prev) => prev.map((entry) => - entry.id === item.id ? { ...entry, status: 'error', error: message } : entry + entry.id === item.id + ? { ...entry, status: 'error', error: message, errorIsTrialLimit: isTrialLimit } + : entry ) ); - toast.error(`${item.file.name}: ${message}`); + // Keeps the error code alive to the toast: a trial account that has run + // out of room is shown the plan rather than just told the upload failed. + toastApiError(error, 'Failed to upload video', { prefix: item.file.name }); } } @@ -500,7 +509,17 @@ export function VideoDragDropUploader({

{item.file.name}

{item.error ? ( -

{item.error}

+

+ {item.error} + {item.errorIsTrialLimit && ( + + Upgrade + + )} +

) : (

{getDefaultTitleFromFile(item.file)} diff --git a/lib/client/api-error.ts b/lib/client/api-error.ts index e659015..f8856c1 100644 --- a/lib/client/api-error.ts +++ b/lib/client/api-error.ts @@ -42,6 +42,14 @@ export function apiRequestError( return new ApiRequestError(payload?.error || fallback, payload?.code); } +/** + * Whether this failure is the trial ceiling, for callers that draw the way out + * themselves rather than handing it to `toastApiError`. + */ +export function isTrialStorageError(source: unknown): boolean { + return codeOf(source) === API_ERROR_CODES.TRIAL_STORAGE_LIMIT_EXCEEDED; +} + function codeOf(source: unknown): string | null { if (source instanceof ApiRequestError) return source.code; if (source && typeof source === 'object' && 'code' in source) { @@ -82,13 +90,15 @@ export function toastApiError( const message = messageOf(source, fallback); const text = options.prefix ? `${options.prefix}: ${message}` : message; - if (codeOf(source) === API_ERROR_CODES.TRIAL_STORAGE_LIMIT_EXCEEDED) { + if (isTrialStorageError(source)) { toast.error(text, { // Longer than a plain error: this one is asking for a decision rather than // just reporting, and it disappears under the cursor at the usual timing. duration: 12000, action: { - label: 'See plans', + // The message ends in "Upgrade to get 200 GB", so the button is the verb + // that sentence just used rather than a second name for the same thing. + label: 'Upgrade', onClick: () => { window.location.href = '/settings'; },