Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/apps/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useTheme } from './hooks/useTheme';
import i18next from 'i18next';
import { isLanguage } from '../shared/Locale/Language';
import { UsageProvider } from './context/UsageContext/usage-provider';
import { MaxFileSizeRejectionModal } from './pages/MaxFileSizeRejectionModal';

function LocationWrapper({ children }: { children: JSX.Element }) {
const { pathname } = useLocation();
Expand All @@ -28,8 +29,13 @@ function LoggedInWrapper({ children }: { children: JSX.Element }) {
const navigate = useNavigate();
const { pathname } = useLocation();
const intendedRoute = useRef<null | string>(null);
const isStandaloneModal = pathname === '/max-file-size-rejection-modal';

function onUserLoggedInChanged(isLoggedIn: boolean) {
if (isStandaloneModal) {
return;
}

if (!isLoggedIn) {
intendedRoute.current = pathname;
navigate('/login');
Expand All @@ -41,7 +47,7 @@ function LoggedInWrapper({ children }: { children: JSX.Element }) {
useEffect(() => {
window.electron.onUserLoggedInChanged(onUserLoggedInChanged);
window.electron.isUserLoggedIn().then(onUserLoggedInChanged);
}, []);
}, [isStandaloneModal]);

return children;
}
Expand Down Expand Up @@ -84,6 +90,7 @@ export default function App() {
<Route path="/login" element={<Login />} />
<Route path="/process-issues" element={<IssuesPage />} />
<Route path="/onboarding" element={<Onboarding />} />
<Route path="/max-file-size-rejection-modal" element={<MaxFileSizeRejectionModal />} />
<Route path="/settings" element={<Settings />} />
<Route path="/" element={<Widget />} />
</Routes>
Expand Down
19 changes: 18 additions & 1 deletion src/apps/renderer/localize/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,8 @@
}
},
"common": {
"cancel": "Cancel"
"cancel": "Cancel",
"close": "Close"
},
"backupErrors": {
"noInternet": {
Expand All @@ -411,5 +412,21 @@
"networkConnectionLost": {
"title": "Network connection lost",
"message": "Your network connection has been lost. Please check your internet connection and try again."
},
"maxFileSizeRejectionModal": {
"single": {
"title": "This file is too large for your current plan",
"description": "Your plan allows uploading files up to {{limit}}. Upgrade your plan to upload this file.",
"description_no_suggested_plan": "Your plan allows uploading files up to {{limit}}",
"description_unknown_limit": "This file exceeds the upload size allowed for your current plan. Upgrade your plan to upload larger files."
},
"multiple": {
"title": "Some files are too large for your current plan",
"description": "Your plan allows uploading files up to {{limit}}. Upgrade your plan to upload larger files.",
"description_no_suggested_plan": "Your plan allows uploading files up to {{limit}}",
"description_unknown_limit": "Some files exceed the upload size allowed for your current plan. Upgrade your plan to upload larger files."
},
"ctaUpgrade": "Upgrade plan",
"plan": "{{planName}} -> up to {{planMaxFileSize}}"
}
}
19 changes: 18 additions & 1 deletion src/apps/renderer/localize/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,8 @@
}
},
"common": {
"cancel": "Cancelar"
"cancel": "Cancelar",
"close": "Cerrar"
},
"backupErrors": {
"noInternet": {
Expand All @@ -411,5 +412,21 @@
"networkConnectionLost": {
"title": "Conexión de red perdida",
"message": "Se ha perdido la conexión de red. Por favor, verifica tu conexión a internet e inténtalo de nuevo."
},
"maxFileSizeRejectionModal": {
"single": {
"title": "Este archivo es demasiado grande para su plan actual",
"description": "Su plan permite subir archivos de hasta {{limit}}. Actualice su plan para cargar este archivo.",
"description_no_suggested_plan": "Su plan permite subir archivos de hasta {{limit}}",
"description_unknown_limit": "Este archivo supera el tamano de subida permitido para su plan actual. Actualice su plan para subir archivos mas grandes."
},
"multiple": {
"title": "Algunos archivos son demasiado grandes para su plan actual",
"description": "Su plan permite subir archivos de hasta {{limit}}. Actualice su plan para cargar archivos más grandes.",
"description_no_suggested_plan": "Su plan permite subir archivos de hasta {{limit}}",
"description_unknown_limit": "Algunos archivos superan el tamano de subida permitido para su plan actual. Actualice su plan para subir archivos mas grandes."
},
"ctaUpgrade": "Mejorar plan",
"plan": "{{planName}} -> hasta {{planMaxFileSize}}"
}
}
19 changes: 18 additions & 1 deletion src/apps/renderer/localize/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,8 @@
}
},
"common": {
"cancel": "Annuler"
"cancel": "Annuler",
"close": "Fermer"
},
"backupErrors": {
"noInternet": {
Expand All @@ -411,5 +412,21 @@
"networkConnectionLost": {
"title": "Connexion réseau perdue",
"message": "Votre connexion réseau a été perdue. Veuillez vérifier votre connexion Internet et réessayer."
},
"maxFileSizeRejectionModal": {
"single": {
"title": "Ce fichier est trop volumineux pour votre plan actuel",
"description": "Votre plan permet de télécharger des fichiers jusqu'à {{limit}}. Mettez à jour votre plan pour télécharger ce fichier.",
"description_no_suggested_plan": "Votre plan permet de télécharger des fichiers jusqu'à {{limit}}",
"description_unknown_limit": "Ce fichier depasse la taille de telechargement autorisee pour votre plan actuel. Mettez a jour votre plan pour telecharger des fichiers plus volumineux."
},
"multiple": {
"title": "Certains fichiers sont trop volumineux pour votre plan actuel",
"description": "Votre plan permet de télécharger des fichiers jusqu'à {{limit}}. Mettez à jour votre plan pour télécharger des fichiers plus volumineux.",
"description_no_suggested_plan": "Votre plan permet de télécharger des fichiers jusqu'à {{limit}}",
"description_unknown_limit": "Certains fichiers depassent la taille de telechargement autorisee pour votre plan actuel. Mettez a jour votre plan pour telecharger des fichiers plus volumineux."
},
"ctaUpgrade": "Mettre à jour le plan",
"plan": "{{planName}} -> jusqu'à {{planMaxFileSize}}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const PLANS_URL = 'https://drive.internxt.com/?preferences=open&section=account&subsection=plans';
const GB = 1024 ** 3;

export const upgradePlans = [
{ name: 'Essential', maxFileSize: 10 * GB },
{ name: 'Premium', maxFileSize: 50 * GB },
{ name: 'Ultimate', maxFileSize: 100 * GB },
] as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { formatBytes } from './format-bytes';

describe('formatBytes', () => {
it('should format whole GB values without decimals', () => {
expect(formatBytes(5 * 1024 ** 3)).toBe('5GB');
});

it('should format fractional GB values with one decimal', () => {
expect(formatBytes(1.5 * 1024 ** 3)).toBe('1.5GB');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function formatBytes(bytes: number) {
const gb = bytes / 1024 ** 3;

if (Number.isInteger(gb)) {
return `${gb}GB`;
}

return `${gb.toFixed(1)}GB`;
}
59 changes: 59 additions & 0 deletions src/apps/renderer/pages/MaxFileSizeRejectionModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Button from '../../components/Button';
import { useTranslationContext } from '../../context/LocalContext';
import { PLANS_URL } from './constants';
import { formatBytes } from './format-bytes';
import { getDescriptionTranslationKey, getModalPropsFromUrlParams, getSuggestedUpgradePlan } from './service';
import { UpgradePlanList } from './upgrade-plan-list';

async function onUpgradePlan() {
await globalThis.window.electron.openUrl(PLANS_URL);
window.close();
}

export function MaxFileSizeRejectionModal() {
const { translate } = useTranslationContext();

const modal = getModalPropsFromUrlParams();
if (!modal) return null;

const suggestedPlan = modal.showUpgradeCta ? getSuggestedUpgradePlan(modal.fileSize) : undefined;

return (
<main className="flex h-screen w-screen items-center justify-center rounded-[20px] bg-transparent p-5 text-highlight">
<section className="w-full max-w-[486px] bg-surface p-5 shadow-xl dark:bg-gray-1">
<h1 className="mb-4 text-xl font-medium leading-6">
{modal.variant === 'single'
? translate('maxFileSizeRejectionModal.single.title')
: translate('maxFileSizeRejectionModal.multiple.title')}
</h1>

<div className="text-base leading-5 text-gray-80 dark:text-gray-80">
<p>
{translate(
getDescriptionTranslationKey({
variant: modal.variant,
showUpgradeCta: modal.showUpgradeCta,
hasKnownLimit: Boolean(modal.maxFileSize),
}),
{
limit: modal.maxFileSize ? formatBytes(modal.maxFileSize) : '',
},
)}
</p>
{modal.showUpgradeCta && <UpgradePlanList suggestedPlan={suggestedPlan} />}
</div>

<div className="mt-5 flex justify-end gap-2">
<Button variant="secondary" onClick={() => window.close()}>
{translate('common.close')}
</Button>
{modal.showUpgradeCta && (
<Button variant="primary" onClick={onUpgradePlan}>
{translate('maxFileSizeRejectionModal.ctaUpgrade')}
</Button>
)}
</div>
</section>
</main>
);
}
92 changes: 92 additions & 0 deletions src/apps/renderer/pages/MaxFileSizeRejectionModal/service.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { getDescriptionTranslationKey, getModalPropsFromUrlParams, getSuggestedUpgradePlan } from './service';

describe('MaxFileSizeRejectionModal service', () => {
describe('getModalPropsFromUrlParams', () => {
it('should return undefined if modal query param is missing', () => {
globalThis.window.history.replaceState({}, '', '/max-file-size-rejection-modal');

const res = getModalPropsFromUrlParams();

expect(res).toBeUndefined();
});

it('should parse modal props from query params', () => {
const modal = { variant: 'multiple', showUpgradeCta: true, fileSize: 20 };
const query = new URLSearchParams({ modal: JSON.stringify(modal) }).toString();
globalThis.window.history.replaceState({}, '', `/max-file-size-rejection-modal?${query}`);

const res = getModalPropsFromUrlParams();

expect(res).toStrictEqual(modal);
});

it('should return undefined if modal query param is not valid JSON', () => {
const query = new URLSearchParams({ modal: '{invalid' }).toString();
globalThis.window.history.replaceState({}, '', `/max-file-size-rejection-modal?${query}`);

const res = getModalPropsFromUrlParams();

expect(res).toBeUndefined();
});

it('should return undefined if modal url is incorrect', () => {
globalThis.window.history.replaceState({}, '', '/max-file-size-rejection');

const res = getModalPropsFromUrlParams();

expect(res).toBeUndefined();
});
});

describe('getSuggestedUpgradePlan', () => {
it('should return smallest plan that supports file size', () => {
const res = getSuggestedUpgradePlan(20 * 1024 ** 3);

expect(res?.name).toBe('Premium');
});

it('should return undefined if no plan supports file size', () => {
const res = getSuggestedUpgradePlan(120 * 1024 ** 3);

expect(res).toBeUndefined();
});
});

describe('getDescriptionTranslationKey', () => {
it('should return single description key when upgrade cta is shown and limit is known', () => {
const res = getDescriptionTranslationKey({ variant: 'single', showUpgradeCta: true, hasKnownLimit: true });

expect(res).toBe('maxFileSizeRejectionModal.single.description');
});

it('should return single no-suggested-plan description key when upgrade cta is hidden and limit is known', () => {
const res = getDescriptionTranslationKey({ variant: 'single', showUpgradeCta: false, hasKnownLimit: true });

expect(res).toBe('maxFileSizeRejectionModal.single.description_no_suggested_plan');
});

it('should return multiple description key when upgrade cta is shown and limit is known', () => {
const res = getDescriptionTranslationKey({ variant: 'multiple', showUpgradeCta: true, hasKnownLimit: true });

expect(res).toBe('maxFileSizeRejectionModal.multiple.description');
});

it('should return multiple no-suggested-plan description key when upgrade cta is hidden and limit is known', () => {
const res = getDescriptionTranslationKey({ variant: 'multiple', showUpgradeCta: false, hasKnownLimit: true });

expect(res).toBe('maxFileSizeRejectionModal.multiple.description_no_suggested_plan');
});

it('should return single unknown-limit description key when limit is unknown', () => {
const res = getDescriptionTranslationKey({ variant: 'single', showUpgradeCta: true, hasKnownLimit: false });

expect(res).toBe('maxFileSizeRejectionModal.single.description_unknown_limit');
});

it('should return multiple unknown-limit description key when limit is unknown', () => {
const res = getDescriptionTranslationKey({ variant: 'multiple', showUpgradeCta: true, hasKnownLimit: false });

expect(res).toBe('maxFileSizeRejectionModal.multiple.description_unknown_limit');
});
});
});
35 changes: 35 additions & 0 deletions src/apps/renderer/pages/MaxFileSizeRejectionModal/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { MaxFileSizeRejectionModalProps } from '../../../../backend/features/user/file-size-limit';
import { upgradePlans } from './constants';

export function getModalPropsFromUrlParams(): MaxFileSizeRejectionModalProps | undefined {
const rawModal = new URLSearchParams(globalThis.window.location.search).get('modal');
if (!rawModal) return;

try {
return JSON.parse(rawModal) as MaxFileSizeRejectionModalProps;
} catch {
return;
}
}

export function getSuggestedUpgradePlan(fileSize?: number) {
if (!fileSize) return;
return upgradePlans.find((plan) => fileSize <= plan.maxFileSize);
}

export function getDescriptionTranslationKey({
variant,
showUpgradeCta,
hasKnownLimit,
}: {
variant: 'single' | 'multiple';
showUpgradeCta: boolean;
hasKnownLimit: boolean;
}) {
if (!hasKnownLimit) return `maxFileSizeRejectionModal.${variant}.description_unknown_limit` as const;
if (variant === 'single' && showUpgradeCta) return 'maxFileSizeRejectionModal.single.description';
if (variant === 'single') return 'maxFileSizeRejectionModal.single.description_no_suggested_plan';
if (showUpgradeCta) return 'maxFileSizeRejectionModal.multiple.description';

return 'maxFileSizeRejectionModal.multiple.description_no_suggested_plan';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useTranslationContext } from '../../context/LocalContext';
import { upgradePlans } from './constants';
import { formatBytes } from './format-bytes';
import { getSuggestedUpgradePlan } from './service';

export function UpgradePlanList({
suggestedPlan,
}: {
readonly suggestedPlan: ReturnType<typeof getSuggestedUpgradePlan>;
}) {
const { translate } = useTranslationContext();

return (
<ul className="mt-1 list-disc pl-6">
{upgradePlans.map((plan) => {
const planText = translate('maxFileSizeRejectionModal.plan', {
planName: plan.name,
planMaxFileSize: formatBytes(plan.maxFileSize),
});

return <li key={plan.name}>{plan.name === suggestedPlan?.name ? <strong>{planText}</strong> : planText}</li>;
})}
</ul>
);
}
9 changes: 6 additions & 3 deletions src/backend/features/user/file-size-limit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export { ABSOLUTE_UPLOAD_FILE_SIZE_LIMIT } from './constants';
export { calculateProjectedWriteSize } from './calculate-projected-write-size';
export { preserveRejectedFileSizeTooBig } from './rejected-file-size-too-big/preserve-rejected-file-size-too-big';
export { resolveUserFileSizeLimit } from './resolve-user-file-size-limit';
export { showMaxFileSizeRejectionModal } from './show-max-file-size-rejection-modal';
export type { MaxFileSizeRejectionModalPayload } from './show-max-file-size-rejection-modal';
export type { UploadFileSizeValidation } from './validate-upload-file-size';
export { validateUploadFileSize } from './validate-upload-file-size';
export type MaxFileSizeRejectionModalProps = {
variant: 'single' | 'multiple';
showUpgradeCta: boolean;
maxFileSize?: number;
fileSize?: number;
};
Loading
Loading