Merge pull request #56 from yusufipk/fix/upload-upgrade-link

fix(upload): give a full trial account the upgrade link where it reads the refusal
This commit is contained in:
Yusuf İpek
2026-08-18 12:05:03 +03:00
committed by GitHub
2 changed files with 34 additions and 5 deletions
+22 -3
View File
@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { CheckCircle2, Loader2, UploadCloud, XCircle } from 'lucide-react'; import { CheckCircle2, Loader2, UploadCloud, XCircle } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -22,6 +23,7 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn'; import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
import { isTrialStorageError, toastApiError } from '@/lib/client/api-error';
import { import {
cleanupPendingProjectUpload, cleanupPendingProjectUpload,
getDefaultTitleFromFile, getDefaultTitleFromFile,
@@ -46,6 +48,8 @@ type QueueItem = {
status: QueueItemStatus; status: QueueItemStatus;
progress: number; progress: number;
error?: string; error?: string;
/** The failure was the trial ceiling, so the row shows the way out. */
errorIsTrialLimit?: boolean;
}; };
interface VideoDragDropUploaderProps { interface VideoDragDropUploaderProps {
@@ -306,12 +310,17 @@ export function VideoDragDropUploader({
failCount += 1; failCount += 1;
const message = error instanceof Error ? error.message : 'Failed to upload video'; const message = error instanceof Error ? error.message : 'Failed to upload video';
const isTrialLimit = isTrialStorageError(error);
setQueue((prev) => setQueue((prev) =>
prev.map((entry) => 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({
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="truncate font-medium">{item.file.name}</p> <p className="truncate font-medium">{item.file.name}</p>
{item.error ? ( {item.error ? (
<p className="truncate text-xs text-destructive">{item.error}</p> <p className="text-xs text-destructive">
<span className="align-middle">{item.error}</span>
{item.errorIsTrialLimit && (
<Link
href="/settings"
className="ml-1 align-middle font-medium underline underline-offset-2"
>
Upgrade
</Link>
)}
</p>
) : ( ) : (
<p className="truncate text-xs text-muted-foreground"> <p className="truncate text-xs text-muted-foreground">
{getDefaultTitleFromFile(item.file)} {getDefaultTitleFromFile(item.file)}
+12 -2
View File
@@ -42,6 +42,14 @@ export function apiRequestError(
return new ApiRequestError(payload?.error || fallback, payload?.code); 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 { function codeOf(source: unknown): string | null {
if (source instanceof ApiRequestError) return source.code; if (source instanceof ApiRequestError) return source.code;
if (source && typeof source === 'object' && 'code' in source) { if (source && typeof source === 'object' && 'code' in source) {
@@ -82,13 +90,15 @@ export function toastApiError(
const message = messageOf(source, fallback); const message = messageOf(source, fallback);
const text = options.prefix ? `${options.prefix}: ${message}` : message; const text = options.prefix ? `${options.prefix}: ${message}` : message;
if (codeOf(source) === API_ERROR_CODES.TRIAL_STORAGE_LIMIT_EXCEEDED) { if (isTrialStorageError(source)) {
toast.error(text, { toast.error(text, {
// Longer than a plain error: this one is asking for a decision rather than // 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. // just reporting, and it disappears under the cursor at the usual timing.
duration: 12000, duration: 12000,
action: { 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: () => { onClick: () => {
window.location.href = '/settings'; window.location.href = '/settings';
}, },