From 8b33b924df21a630bb8fc3e7448ba206b592d823 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 22 Oct 2026 15:11:44 -0300 Subject: [PATCH] fix: Improves errors on file attachments for expired subs --- .../src/Domain/Strings/Messages.spec.ts | 30 ++++++++++++++++ .../services/src/Domain/Strings/Messages.ts | 35 +++++++++++++++++++ packages/snjs/lib/Services/Api/ApiService.ts | 4 ++- .../Components/FilePreview/FilePreview.tsx | 8 ++++- .../FilePreview/FilePreviewError.tsx | 8 +++-- .../Controllers/FilesController.ts | 6 +++- 6 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 packages/services/src/Domain/Strings/Messages.spec.ts diff --git a/packages/services/src/Domain/Strings/Messages.spec.ts b/packages/services/src/Domain/Strings/Messages.spec.ts new file mode 100644 index 00000000000..1493b29156a --- /dev/null +++ b/packages/services/src/Domain/Strings/Messages.spec.ts @@ -0,0 +1,30 @@ +import { ValetTokenOperation } from '@standardnotes/responses' +import { messageForCreateValetTokenFailure } from './Messages' + +describe('messageForCreateValetTokenFailure', () => { + it('returns a clear message for expired subscription on download', () => { + const message = messageForCreateValetTokenFailure('expired-subscription', ValetTokenOperation.Read) + expect(message).toContain('subscription has expired') + expect(message).toContain('downloaded') + expect(message).not.toContain('expired-subscription') + expect(message).not.toContain('Account settings') + }) + + it('returns a clear message for expired subscription on upload', () => { + const message = messageForCreateValetTokenFailure('expired-subscription', ValetTokenOperation.Write) + expect(message).toContain('subscription has expired') + expect(message).toContain('uploaded') + }) + + it('returns a clear message for missing subscription', () => { + const message = messageForCreateValetTokenFailure('no-subscription', ValetTokenOperation.Read) + expect(message).toContain('active subscription') + expect(message).not.toContain('Account settings') + expect(message).not.toContain('no-subscription') + }) + + it('falls back to generic file token error for unknown reasons', () => { + const message = messageForCreateValetTokenFailure('something-else') + expect(message).toContain('Failed to create file token') + }) +}) diff --git a/packages/services/src/Domain/Strings/Messages.ts b/packages/services/src/Domain/Strings/Messages.ts index 8a802f42299..8ab48d2c567 100644 --- a/packages/services/src/Domain/Strings/Messages.ts +++ b/packages/services/src/Domain/Strings/Messages.ts @@ -1,4 +1,5 @@ import { ProtocolVersion } from '@standardnotes/models' +import { CreateValetTokenResponseData, ValetTokenOperation } from '@standardnotes/responses' import { c } from 'ttag' import { jtString, AppName, ListedName, HelpSecurityUrl, SupportEmail } from '@standardnotes/features' @@ -52,6 +53,40 @@ export const API_MESSAGE_FAILED_LISTED_REGISTRATION = jtString( export const API_MESSAGE_FAILED_CREATE_FILE_TOKEN = c('B7.FilesSubscriptionHelp.Files.Error') .t`Failed to create file token.` +type CreateValetTokenFailureReason = Extract['reason'] + +export function messageForCreateValetTokenFailure( + reason: CreateValetTokenFailureReason | string, + operation?: ValetTokenOperation, +): string { + switch (reason) { + case 'expired-subscription': + if (operation === ValetTokenOperation.Write) { + return c('B7.FilesSubscriptionHelp.Files.Error') + .t`Files cannot be uploaded because your subscription has expired. Renew your subscription to use encrypted file storage again.` + } + if (operation === ValetTokenOperation.Read) { + return c('B7.FilesSubscriptionHelp.Files.Error') + .t`This file cannot be downloaded because your subscription has expired. Renew your subscription to access your files again.` + } + return c('B7.FilesSubscriptionHelp.Files.Error') + .t`Encrypted files are unavailable because your subscription has expired. Renew your subscription to download and upload files again.` + case 'no-subscription': + if (operation === ValetTokenOperation.Write) { + return c('B7.FilesSubscriptionHelp.Files.Error') + .t`Files cannot be uploaded without an active subscription. Upgrade your plan to use encrypted file storage.` + } + if (operation === ValetTokenOperation.Read) { + return c('B7.FilesSubscriptionHelp.Files.Error') + .t`This file cannot be downloaded without an active subscription. Upgrade your plan to access encrypted files.` + } + return c('B7.FilesSubscriptionHelp.Files.Error') + .t`Encrypted file storage requires an active subscription. Upgrade your plan to download and upload files.` + default: + return API_MESSAGE_FAILED_CREATE_FILE_TOKEN + } +} + export const API_MESSAGE_FAILED_SUBSCRIPTION_INFO = c('B7.FilesSubscriptionHelp.Subscription.Error') .t`Failed to get subscription's information.` diff --git a/packages/snjs/lib/Services/Api/ApiService.ts b/packages/snjs/lib/Services/Api/ApiService.ts index b750cdf0b7e..35a93135a24 100644 --- a/packages/snjs/lib/Services/Api/ApiService.ts +++ b/packages/snjs/lib/Services/Api/ApiService.ts @@ -13,6 +13,7 @@ import { API_MESSAGE_CHANGE_CREDENTIALS_IN_PROGRESS, API_MESSAGE_FAILED_ACCESS_PURCHASE, API_MESSAGE_FAILED_CREATE_FILE_TOKEN, + messageForCreateValetTokenFailure, API_MESSAGE_FAILED_GET_SETTINGS, API_MESSAGE_FAILED_LISTED_REGISTRATION, API_MESSAGE_FAILED_OFFLINE_ACTIVATION, @@ -748,7 +749,8 @@ export class LegacyApiService } if (!response.data?.success) { - return new ClientDisplayableError(response.data?.reason as string, undefined, response.data?.reason as string) + const reason = response.data.reason + return new ClientDisplayableError(messageForCreateValetTokenFailure(reason, operation), undefined, reason) } return response.data?.valetToken diff --git a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx index cadec99ca73..72d2f5ee9e0 100644 --- a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx @@ -40,6 +40,7 @@ const FilePreview = ({ const [isDownloading, setIsDownloading] = useState(true) const [downloadProgress, setDownloadProgress] = useState() const [downloadedBytes, setDownloadedBytes] = useState() + const [downloadErrorMessage, setDownloadErrorMessage] = useState() useEffect(() => { setIsAuthorized(application.isAuthorizedToRenderItem(file)) @@ -75,6 +76,7 @@ const FilePreview = ({ try { const chunks: Uint8Array[] = [] setDownloadProgress(undefined) + setDownloadErrorMessage(undefined) const error = await application.files.downloadFile(file, async (decryptedChunk, progress) => { chunks.push(decryptedChunk) if (progress) { @@ -82,7 +84,9 @@ const FilePreview = ({ } }) - if (!error) { + if (error) { + setDownloadErrorMessage(error.text) + } else { const finalDecryptedBytes = concatenateUint8Arrays(chunks) setDownloadedBytes(finalDecryptedBytes) } @@ -159,8 +163,10 @@ const FilePreview = ({ filesController={application.filesController} tryAgainCallback={() => { setDownloadedBytes(undefined) + setDownloadErrorMessage(undefined) }} isFilePreviewable={isFilePreviewable} + errorDetail={downloadErrorMessage} /> ) } diff --git a/packages/web/src/javascripts/Components/FilePreview/FilePreviewError.tsx b/packages/web/src/javascripts/Components/FilePreview/FilePreviewError.tsx index cc42cfd88d9..ddb7b64cb57 100644 --- a/packages/web/src/javascripts/Components/FilePreview/FilePreviewError.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/FilePreviewError.tsx @@ -10,9 +10,10 @@ type Props = { filesController: FilesController isFilePreviewable: boolean tryAgainCallback: () => void + errorDetail?: string } -const FilePreviewError = ({ file, filesController, isFilePreviewable, tryAgainCallback }: Props) => { +const FilePreviewError = ({ file, filesController, isFilePreviewable, tryAgainCallback, errorDetail }: Props) => { return (
@@ -21,8 +22,9 @@ const FilePreviewError = ({ file, filesController, isFilePreviewable, tryAgainCa {isFilePreviewable ? ( <>
- {c('B7.FilesSubscriptionHelp.Files.Error') - .t`There was an error loading the file. Try again, or download the file and open it using another application.`} + {errorDetail ?? + c('B7.FilesSubscriptionHelp.Files.Error') + .t`There was an error loading the file. Try again, or download the file and open it using another application.`}