From 93bb3c54f9f68285e8a6e96c3da9ae11b95376bf Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Tue, 29 Sep 2026 11:12:03 +0200 Subject: [PATCH 1/3] feat(checkout): support checkout without a Drive session Add the passwordless checkout groundwork: an env flag, a checkout SDK client that carries no session token, an account-setup service, and the createCustomerWithoutAccount/withoutSession option in the checkout service and payment hook. No user-facing behaviour changes yet. --- .env.example | 1 + src/app/core/factory/sdk/index.ts | 5 + src/services/account-setup.service.ts | 19 ++ src/services/env.service.ts | 1 + .../Checkout/hooks/useUserPayment.test.ts | 179 ++++++++++++++++++ src/views/Checkout/hooks/useUserPayment.ts | 110 ++++++----- .../services/checkout.service.test.ts | 78 ++++++++ .../Checkout/services/checkout.service.ts | 50 +++-- src/views/Checkout/types/index.ts | 3 + 9 files changed, 375 insertions(+), 71 deletions(-) create mode 100644 src/services/account-setup.service.ts diff --git a/.env.example b/.env.example index 3c0b7ccec1..62cf38a75d 100644 --- a/.env.example +++ b/.env.example @@ -29,3 +29,4 @@ REACT_APP_META_PIXEL= REACT_APP_TURNSTILE_SITE_KEY=turnstile_site_key REACT_APP_TURNSTILE_ENABLED=false +REACT_APP_PASSWORDLESS_CHECKOUT_ENABLED=false diff --git a/src/app/core/factory/sdk/index.ts b/src/app/core/factory/sdk/index.ts index 27f7c8973a..3fa43225ea 100644 --- a/src/app/core/factory/sdk/index.ts +++ b/src/app/core/factory/sdk/index.ts @@ -131,6 +131,11 @@ export class SdkFactory { return Checkout.client(envService.getVariable('payments'), appDetails, apiSecurity); } + public createCheckoutClientWithoutSession(): Checkout { + const appDetails = SdkFactory.getAppDetails(); + return Checkout.client(envService.getVariable('payments'), appDetails, { token: '' }); + } + public createBackupsClient(): Backups { const apiUrl = this.getApiUrl(); const appDetails = SdkFactory.getAppDetails(); diff --git a/src/services/account-setup.service.ts b/src/services/account-setup.service.ts new file mode 100644 index 0000000000..1a63b89492 --- /dev/null +++ b/src/services/account-setup.service.ts @@ -0,0 +1,19 @@ +import { isAccountSetupPendingError } from '@internxt/sdk'; +import { SdkFactory } from 'app/core/factory/sdk'; +import { generateCaptchaToken } from 'utils'; + +/** + * Asks Drive to send again the email to finish the setup of an account paid from the checkout. + * Drive always answers the same way, so the caller can't tell whether the email had a pending setup. + */ +export const resendAccountSetupEmail = async (email: string): Promise => { + const captchaToken = await generateCaptchaToken(); + const authClient = SdkFactory.getNewApiInstance().createAuthClient({ captchaToken }); + await authClient.resendAccountSetupEmail(email.toLowerCase()); +}; + +/** + * True when the error comes from an email whose account was paid but its setup is not finished yet. + * Pass the raw error: `errorService.castError` does not keep the response `code`. + */ +export const isAccountSetupPending = (error: unknown): boolean => isAccountSetupPendingError(error); diff --git a/src/services/env.service.ts b/src/services/env.service.ts index 0dfb3acc32..06d342ccbd 100644 --- a/src/services/env.service.ts +++ b/src/services/env.service.ts @@ -39,6 +39,7 @@ const variableList = { recaptchaV3: 'REACT_APP_RECAPTCHA_V3', turnstileSiteKey: 'REACT_APP_TURNSTILE_SITE_KEY', turnstileEnabled: 'REACT_APP_TURNSTILE_ENABLED', + passwordlessCheckoutEnabled: 'REACT_APP_PASSWORDLESS_CHECKOUT_ENABLED', shareLinksDomain: 'REACT_APP_SHARE_LINKS_DOMAIN', proxy: 'REACT_APP_PROXY', dontUseProxy: 'REACT_APP_DONT_USE_PROXY', diff --git a/src/views/Checkout/hooks/useUserPayment.test.ts b/src/views/Checkout/hooks/useUserPayment.test.ts index 305865ad37..c96b2ef36f 100644 --- a/src/views/Checkout/hooks/useUserPayment.test.ts +++ b/src/views/Checkout/hooks/useUserPayment.test.ts @@ -511,6 +511,185 @@ describe('Custom hook to handle payments', () => { expect(createPaymentIntentSpy).toHaveBeenCalled(); }); + test('When the buyer has no account yet, then the purchase is recorded as their first one without looking up their invoices', async () => { + const payment = useUserPayment(); + vi.spyOn(checkoutService, 'createSubscription').mockResolvedValue({ + clientSecret: 'client_secret', + type: 'payment', + paymentIntentId: 'pi_123', + subscriptionId: 'sub_123', + }); + vi.spyOn(paymentService, 'getInvoices').mockRejectedValue(new Error('Unauthorized')); + const localStorageSetSpy = vi.spyOn(localStorageService, 'set').mockImplementation(() => {}); + + await payment.handleUserPayment({ + customerId: 'customer_id', + priceId: 'price_id', + token: 'payments_token', + translate: vi.fn(), + currency: 'eur', + confirmationTokenId: 'ctoken_123', + selectedPlan: { price: { interval: 'year', type: UserType.Individual } } as any, + captchaToken: 'captcha_token', + userAddress: '1.1.1.1', + confirmPayment: vi.fn().mockResolvedValue({ error: undefined }), + confirmSetupIntent: vi.fn().mockResolvedValue({ error: undefined }), + gclidStored: null, + isFirstPurchase: true, + }); + + expect(paymentService.getInvoices).not.toHaveBeenCalled(); + expect(localStorageSetSpy).toHaveBeenCalledWith('isFirstPurchase', 'true'); + }); + + test('When a buyer without an account purchases a subscription, then it is created through the client without a Drive session', async () => { + const payment = useUserPayment(); + const createSubscriptionSpy = vi.spyOn(checkoutService, 'createSubscription').mockResolvedValue({ + clientSecret: 'client_secret', + type: 'payment', + paymentIntentId: 'pi_123', + subscriptionId: 'sub_123', + }); + vi.spyOn(localStorageService, 'set').mockImplementation(() => {}); + + await payment.handleUserPayment({ + customerId: 'customer_id', + priceId: 'price_id', + token: 'payments_token', + translate: vi.fn(), + currency: 'eur', + confirmationTokenId: 'ctoken_123', + selectedPlan: { price: { interval: 'year', type: UserType.Individual } } as any, + captchaToken: 'captcha_token', + userAddress: '1.1.1.1', + confirmPayment: vi.fn().mockResolvedValue({ error: undefined }), + confirmSetupIntent: vi.fn().mockResolvedValue({ error: undefined }), + gclidStored: null, + isFirstPurchase: true, + isPasswordlessSignUp: true, + }); + + expect(createSubscriptionSpy).toHaveBeenCalledWith( + { + customerId: 'customer_id', + priceId: 'price_id', + token: 'payments_token', + currency: 'eur', + captchaToken: 'captcha_token', + promoCodeId: undefined, + }, + { withoutSession: true }, + ); + }); + + test('When a buyer without an account purchases a lifetime plan, then the payment intent is created through the client without a Drive session', async () => { + const payment = useUserPayment(); + const createPaymentIntentSpy = vi.spyOn(checkoutService, 'createPaymentIntent').mockResolvedValue({ + clientSecret: 'client_secret', + invoiceStatus: 'open', + type: 'fiat', + id: 'pi_123', + }); + vi.spyOn(localStorageService, 'set').mockImplementation(() => {}); + + await payment.handleUserPayment({ + customerId: 'customer_id', + priceId: 'price_id', + token: 'payments_token', + translate: vi.fn(), + currency: 'eur', + confirmationTokenId: 'ctoken_123', + selectedPlan: { price: { interval: 'lifetime', type: UserType.Individual } } as any, + captchaToken: 'captcha_token', + userAddress: '1.1.1.1', + confirmPayment: vi.fn().mockResolvedValue({ error: undefined }), + confirmSetupIntent: vi.fn().mockResolvedValue({ error: undefined }), + gclidStored: null, + isFirstPurchase: true, + isPasswordlessSignUp: true, + }); + + expect(createPaymentIntentSpy).toHaveBeenCalledWith( + { + customerId: 'customer_id', + priceId: 'price_id', + token: 'payments_token', + currency: 'eur', + userAddress: '1.1.1.1', + captchaToken: 'captcha_token', + promoCodeId: undefined, + }, + { withoutSession: true }, + ); + }); + + test('When a logged-in buyer purchases a subscription, then it is created through the regular client', async () => { + const payment = useUserPayment(); + const createSubscriptionSpy = vi.spyOn(checkoutService, 'createSubscription').mockResolvedValue({ + clientSecret: 'client_secret', + type: 'payment', + paymentIntentId: 'pi_123', + subscriptionId: 'sub_123', + }); + vi.spyOn(localStorageService, 'set').mockImplementation(() => {}); + vi.spyOn(paymentService, 'getInvoices').mockResolvedValue([]); + + await payment.handleUserPayment({ + customerId: 'customer_id', + priceId: 'price_id', + token: 'user_token', + translate: vi.fn(), + currency: 'eur', + confirmationTokenId: 'ctoken_123', + selectedPlan: { price: { interval: 'year', type: UserType.Individual } } as any, + captchaToken: 'captcha_token', + userAddress: '1.1.1.1', + confirmPayment: vi.fn().mockResolvedValue({ error: undefined }), + confirmSetupIntent: vi.fn().mockResolvedValue({ error: undefined }), + gclidStored: null, + }); + + expect(createSubscriptionSpy).toHaveBeenCalledWith({ + customerId: 'customer_id', + priceId: 'price_id', + token: 'user_token', + currency: 'eur', + captchaToken: 'captcha_token', + promoCodeId: undefined, + }); + }); + + test('When the buyer cancels or fails the payment confirmation, then the error is raised and they are not sent to the success page', async () => { + const payment = useUserPayment(); + vi.spyOn(checkoutService, 'createSubscription').mockResolvedValue({ + clientSecret: 'client_secret', + type: 'payment', + paymentIntentId: 'pi_123', + subscriptionId: 'sub_123', + }); + vi.spyOn(localStorageService, 'set').mockImplementation(() => {}); + const navigationServiceSpy = vi.spyOn(navigationService, 'push').mockImplementation(() => {}); + + const paymentAttempt = payment.handleUserPayment({ + customerId: 'customer_id', + priceId: 'price_id', + token: 'payments_token', + translate: vi.fn(), + currency: 'eur', + confirmationTokenId: 'ctoken_123', + selectedPlan: { price: { interval: 'year', type: UserType.Individual } } as any, + captchaToken: 'captcha_token', + userAddress: '1.1.1.1', + confirmPayment: vi.fn().mockResolvedValue({ error: { message: 'Your card was declined' } }), + confirmSetupIntent: vi.fn(), + gclidStored: null, + isFirstPurchase: true, + }); + + await expect(paymentAttempt).rejects.toThrow('Your card was declined'); + expect(navigationServiceSpy).not.toHaveBeenCalledWith(AppView.CheckoutSuccess); + }); + test('When the plan is neither a subscription nor a lifetime, then the user is redirected to the Drive page directly', async () => { const payment = useUserPayment(); diff --git a/src/views/Checkout/hooks/useUserPayment.ts b/src/views/Checkout/hooks/useUserPayment.ts index 782da2459b..3b432d8ee7 100644 --- a/src/views/Checkout/hooks/useUserPayment.ts +++ b/src/views/Checkout/hooks/useUserPayment.ts @@ -34,27 +34,21 @@ export const checkIsFirstPurchase = async (): Promise => { }; export const useUserPayment = () => { - const getSubscriptionPaymentIntent = async ({ - customerId, - priceId, - token, - currency, - captchaToken, - promoCodeId, - }: CreateSubscriptionPayload) => { + const getSubscriptionPaymentIntent = async ( + { customerId, priceId, token, currency, captchaToken, promoCodeId }: CreateSubscriptionPayload, + isPasswordlessSignUp?: boolean, + ) => { const { type: paymentType, clientSecret: client_secret, subscriptionId, paymentIntentId, - } = await checkoutService.createSubscription({ - customerId, - priceId, - token, - currency, - captchaToken, - promoCodeId, - }); + } = isPasswordlessSignUp + ? await checkoutService.createSubscription( + { customerId, priceId, token, currency, captchaToken, promoCodeId }, + { withoutSession: true }, + ) + : await checkoutService.createSubscription({ customerId, priceId, token, currency, captchaToken, promoCodeId }); return { type: paymentType, @@ -64,24 +58,24 @@ export const useUserPayment = () => { }; }; - const getLifetimePaymentIntent = async ({ - customerId, - priceId, - currency, - token, - userAddress, - captchaToken, - promoCodeId, - }: CreatePaymentIntentPayload) => { - const paymentIntentResponse = await checkoutService.createPaymentIntent({ - customerId, - priceId, - currency, - userAddress, - token, - captchaToken, - promoCodeId: promoCodeId, - }); + const getLifetimePaymentIntent = async ( + { customerId, priceId, currency, token, userAddress, captchaToken, promoCodeId }: CreatePaymentIntentPayload, + isPasswordlessSignUp?: boolean, + ) => { + const paymentIntentResponse = isPasswordlessSignUp + ? await checkoutService.createPaymentIntent( + { customerId, priceId, currency, userAddress, token, captchaToken, promoCodeId }, + { withoutSession: true }, + ) + : await checkoutService.createPaymentIntent({ + customerId, + priceId, + currency, + userAddress, + token, + captchaToken, + promoCodeId, + }); if (paymentIntentResponse.type === PaymentType['CRYPTO']) { return { @@ -139,15 +133,19 @@ export const useUserPayment = () => { confirmPayment, confirmSetupIntent, isFirstPurchase, + isPasswordlessSignUp, }: ProcessPurchasePayload) => { - const subscription = await getSubscriptionPaymentIntent({ - customerId, - priceId, - token, - captchaToken, - promoCodeId: couponCodeData?.codeId, - currency, - }); + const subscription = await getSubscriptionPaymentIntent( + { + customerId, + priceId, + token, + captchaToken, + promoCodeId: couponCodeData?.codeId, + currency, + }, + isPasswordlessSignUp, + ); savePaymentDataInLocalStorage({ subscriptionId: subscription.subscriptionId, @@ -190,6 +188,7 @@ export const useUserPayment = () => { confirmPayment, openCryptoPaymentDialog, isFirstPurchase, + isPasswordlessSignUp, }: ProcessPurchasePayload) => { const { id: paymentIntentId, @@ -198,15 +197,18 @@ export const useUserPayment = () => { type, clientSecret, payload, - } = await getLifetimePaymentIntent({ - customerId, - priceId, - token, - captchaToken, - promoCodeId: couponCodeData?.codeId, - userAddress, - currency, - }); + } = await getLifetimePaymentIntent( + { + customerId, + priceId, + token, + captchaToken, + promoCodeId: couponCodeData?.codeId, + userAddress, + currency, + }, + isPasswordlessSignUp, + ); savePaymentDataInLocalStorage({ subscriptionId: undefined, @@ -262,9 +264,11 @@ export const useUserPayment = () => { confirmPayment, openCryptoPaymentDialog, confirmSetupIntent, + isFirstPurchase: isKnownFirstPurchase, + isPasswordlessSignUp, }: UseUserPaymentPayload) => { const planInterval = selectedPlan.price.interval; - const isFirstPurchase = await checkIsFirstPurchase(); + const isFirstPurchase = isKnownFirstPurchase ?? (await checkIsFirstPurchase()); if (gclidStored) { await sendConversionToAPI({ @@ -295,6 +299,7 @@ export const useUserPayment = () => { confirmPayment, confirmSetupIntent, isFirstPurchase, + isPasswordlessSignUp, }); break; @@ -314,6 +319,7 @@ export const useUserPayment = () => { openCryptoPaymentDialog, confirmSetupIntent, isFirstPurchase, + isPasswordlessSignUp, }); break; diff --git a/src/views/Checkout/services/checkout.service.test.ts b/src/views/Checkout/services/checkout.service.test.ts index 3812df8556..4287daec33 100644 --- a/src/views/Checkout/services/checkout.service.test.ts +++ b/src/views/Checkout/services/checkout.service.test.ts @@ -7,6 +7,7 @@ import { GetPriceByIdPayload, } from '@internxt/sdk/dist/payments/types'; import userService from 'services/user.service'; +import { SdkFactory } from 'app/core/factory/sdk'; vi.mock('app/drive/services/size.service', () => ({ bytesToString: (bytes: number) => `${bytes} B`, @@ -80,6 +81,19 @@ vi.mock('app/core/factory/sdk', () => ({ }), verifyCryptoPayment: vi.fn().mockResolvedValue(true), }), + createCheckoutClientWithoutSession: vi.fn().mockReturnValue({ + createSubscription: vi.fn().mockResolvedValue({ + type: 'payment', + clientSecret: 'no_session_client_secret', + subscriptionId: 'sub_no_session', + paymentIntentId: 'py_no_session', + }), + createPaymentIntent: vi.fn().mockResolvedValue({ + clientSecret: 'no_session_client_secret', + id: 'py_no_session_id', + invoiceStatus: 'paid', + }), + }), }), }, })); @@ -244,6 +258,36 @@ describe('Checkout Service tests', () => { paymentIntentId: 'py_123', }); }); + + it('When a buyer without an account creates a subscription, then it is created through the client without a Drive session', async () => { + const createSubPayload: CreateSubscriptionPayload = { + customerId: 'cus_123', + priceId: 'price_123', + token: 'payments_token', + captchaToken: 'captcha_token', + }; + + const createSubResponse = await checkoutService.createSubscription(createSubPayload, { withoutSession: true }); + + const sessionClient = await SdkFactory.getNewApiInstance().createCheckoutClient(); + const sessionlessClient = SdkFactory.getNewApiInstance().createCheckoutClientWithoutSession(); + + expect(sessionlessClient.createSubscription).toHaveBeenCalledWith({ + customerId: createSubPayload.customerId, + priceId: createSubPayload.priceId, + token: createSubPayload.token, + currency: undefined, + captchaToken: createSubPayload.captchaToken, + promoCodeId: undefined, + }); + expect(sessionClient.createSubscription).not.toHaveBeenCalled(); + expect(createSubResponse).toStrictEqual({ + type: 'payment', + clientSecret: 'no_session_client_secret', + subscriptionId: 'sub_no_session', + paymentIntentId: 'py_no_session', + }); + }); }); describe('Create a payment intent', () => { @@ -285,6 +329,40 @@ describe('Checkout Service tests', () => { invoiceStatus: 'paid', }); }); + + it('When a buyer without an account creates a payment intent, then it is created through the client without a Drive session', async () => { + const createInvoicePayload: CreatePaymentIntentPayload = { + customerId: 'cus_123', + priceId: 'price_123', + token: 'payments_token', + currency: 'eur', + captchaToken: 'captcha_token', + userAddress: '1.1.1.1', + }; + + const createInvoiceResponse = await checkoutService.createPaymentIntent(createInvoicePayload, { + withoutSession: true, + }); + + const sessionClient = await SdkFactory.getNewApiInstance().createCheckoutClient(); + const sessionlessClient = SdkFactory.getNewApiInstance().createCheckoutClientWithoutSession(); + + expect(sessionlessClient.createPaymentIntent).toHaveBeenCalledWith({ + customerId: createInvoicePayload.customerId, + priceId: createInvoicePayload.priceId, + token: createInvoicePayload.token, + currency: createInvoicePayload.currency, + captchaToken: createInvoicePayload.captchaToken, + userAddress: createInvoicePayload.userAddress, + promoCodeId: undefined, + }); + expect(sessionClient.createPaymentIntent).not.toHaveBeenCalled(); + expect(createInvoiceResponse).toStrictEqual({ + clientSecret: 'no_session_client_secret', + id: 'py_no_session_id', + invoiceStatus: 'paid', + }); + }); }); describe('Fetch promotion code by name', () => { diff --git a/src/views/Checkout/services/checkout.service.ts b/src/views/Checkout/services/checkout.service.ts index b1f751343a..35cc6b7183 100644 --- a/src/views/Checkout/services/checkout.service.ts +++ b/src/views/Checkout/services/checkout.service.ts @@ -1,8 +1,11 @@ import { CouponCodeData, CreatedSubscriptionData } from '@internxt/sdk/dist/drive/payments/types/types'; +import { Checkout } from '@internxt/sdk/dist/payments'; import axios from 'axios'; import { SdkFactory } from 'app/core/factory/sdk'; import { CreateCustomerPayload, + CreateCustomerWithoutAccountPayload, + CreatedCustomer, CreatePaymentIntentPayload, CreateSubscriptionPayload, GetPriceByIdPayload, @@ -114,6 +117,13 @@ const createCustomer = async ({ } as CreateCustomerPayload); }; +const createCustomerWithoutAccount = async (payload: CreateCustomerWithoutAccountPayload): Promise => { + const checkoutClient = SdkFactory.getNewApiInstance().createCheckoutClientWithoutSession(); + return checkoutClient.createCustomerWithoutAccount(payload); +}; + +const isPasswordlessCheckoutEnabled = (): boolean => envService.getVariable('passwordlessCheckoutEnabled') === 'true'; + const getPriceById = async ({ priceId, promoCodeName, @@ -133,15 +143,20 @@ const getPriceById = async ({ }); }; -const createSubscription = async ({ - customerId, - priceId, - token, - currency, - captchaToken, - promoCodeId, -}: CreateSubscriptionPayload): Promise => { - const checkoutClient = await SdkFactory.getNewApiInstance().createCheckoutClient(); +export interface CheckoutClientOptions { + withoutSession?: boolean; +} + +const getCheckoutClient = async (options?: CheckoutClientOptions): Promise => + options?.withoutSession + ? SdkFactory.getNewApiInstance().createCheckoutClientWithoutSession() + : SdkFactory.getNewApiInstance().createCheckoutClient(); + +const createSubscription = async ( + { customerId, priceId, token, currency, captchaToken, promoCodeId }: CreateSubscriptionPayload, + options?: CheckoutClientOptions, +): Promise => { + const checkoutClient = await getCheckoutClient(options); return checkoutClient.createSubscription({ customerId, priceId, @@ -152,16 +167,11 @@ const createSubscription = async ({ }); }; -export const createPaymentIntent = async ({ - customerId, - priceId, - token, - currency, - captchaToken, - userAddress, - promoCodeId, -}: CreatePaymentIntentPayload): Promise => { - const checkoutClient = await SdkFactory.getNewApiInstance().createCheckoutClient(); +export const createPaymentIntent = async ( + { customerId, priceId, token, currency, captchaToken, userAddress, promoCodeId }: CreatePaymentIntentPayload, + options?: CheckoutClientOptions, +): Promise => { + const checkoutClient = await getCheckoutClient(options); return checkoutClient.createPaymentIntent({ customerId, priceId, @@ -293,6 +303,8 @@ const trackIncompleteCheckout = async (selectedPlan: PriceWithTax | undefined, p const checkoutService = { fetchPromotionCodeByName, createCustomer, + createCustomerWithoutAccount, + isPasswordlessCheckoutEnabled, createPaymentIntent, getPriceById, createSubscription, diff --git a/src/views/Checkout/types/index.ts b/src/views/Checkout/types/index.ts index acd61ed05c..4d3e576872 100644 --- a/src/views/Checkout/types/index.ts +++ b/src/views/Checkout/types/index.ts @@ -110,6 +110,7 @@ export interface ProcessPurchasePayload { currentSelectedPlan: PriceWithTax; couponCodeData?: CouponCodeData; isFirstPurchase?: boolean; + isPasswordlessSignUp?: boolean; } export interface UseUserPaymentPayload { @@ -127,6 +128,8 @@ export interface UseUserPaymentPayload { captchaToken: string; translate: Translate; couponCodeData?: CouponCodeData; + isFirstPurchase?: boolean; + isPasswordlessSignUp?: boolean; } export enum PlanInterval { From ea032facbc1258e22ec8f3db98e7ca8fe6f927d8 Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Tue, 6 Oct 2026 13:30:58 +0200 Subject: [PATCH 2/3] chore: update SDK verison --- package.json | 2 +- src/views/Drive/DriveView.tsx | 19 ++++++++++++++++--- yarn.lock | 8 ++++---- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index c18cbb8ba3..26027b493c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@iconscout/react-unicons": "^2.2.5", "@internxt/css-config": "1.1.1", "@internxt/lib": "=1.5.1", - "@internxt/sdk": "1.21.1", + "@internxt/sdk": "1.22.1", "@internxt/ui": "=0.1.21", "@marsidev/react-turnstile": "^1.6.1", "@phosphor-icons/react": "^2.1.10", diff --git a/src/views/Drive/DriveView.tsx b/src/views/Drive/DriveView.tsx index a91a99a13d..38d438dec1 100644 --- a/src/views/Drive/DriveView.tsx +++ b/src/views/Drive/DriveView.tsx @@ -26,7 +26,20 @@ import DriveExplorer from 'views/Drive/components/DriveExplorer/DriveExplorer'; import encryptedStorageService from 'services/encrypted-storage.service'; import { Loader } from '@internxt/ui'; -type FileMetaWithThumbnails = FileMeta & Partial>; +type DriveThumbnail = DriveFileData['thumbnails'][number]; +type FileMetaThumbnail = NonNullable[number]; + +const toDriveThumbnail = (thumbnail: FileMetaThumbnail): DriveThumbnail => ({ + id: thumbnail.id, + file_id: thumbnail.fileId, + type: thumbnail.type, + max_width: thumbnail.maxWidth, + max_height: thumbnail.maxHeight, + size: thumbnail.size, + bucket_id: thumbnail.bucketId, + bucket_file: thumbnail.bucketFile, + encrypt_version: thumbnail.encryptVersion, +}); export interface DriveViewProps { namePath: FolderPath[]; @@ -157,7 +170,7 @@ const DriveView = (props: DriveViewProps) => { const showFile = async (fileUUID: string, workspacesToken?: string) => { try { - const fileMeta: FileMetaWithThumbnails = await fileService.getFile(fileUUID, workspacesToken); + const fileMeta = await fileService.getFile(fileUUID, workspacesToken); const listedFile = folderLevels[fileMeta.folderUuid]?.find((item) => item.uuid === fileMeta.uuid); dispatch(uiActions.setIsFileViewerOpen(true)); /* @@ -186,7 +199,7 @@ const DriveView = (props: DriveViewProps) => { status: fileMeta.status, uuid: fileMeta.uuid, updatedAt: fileMeta.updatedAt, - thumbnails: listedFile?.thumbnails ?? fileMeta.thumbnails ?? [], + thumbnails: listedFile?.thumbnails ?? fileMeta.thumbnails?.map(toDriveThumbnail) ?? [], currentThumbnail: listedFile?.currentThumbnail ?? null, }), ); diff --git a/yarn.lock b/yarn.lock index dc6d34e556..54a07ed4d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1939,10 +1939,10 @@ resolved "https://registry.yarnpkg.com/@internxt/prettier-config/-/prettier-config-2.0.1.tgz#a25e7a16f62a990aed2bd1804e04140cc8c33e77" integrity sha512-sCnOMX7GR7BYDJ5OH1RiV/JaOBLE3uhL6QN6tYhMKi7QXK/cnM/77qmDlIEBlhTNIFEMLFljSkc0vZEvf6m2xA== -"@internxt/sdk@1.21.1": - version "1.21.1" - resolved "https://registry.yarnpkg.com/@internxt/sdk/-/sdk-1.21.1.tgz#4f6ee22a7815e05f2e6d4b8f528b117d738ed37f" - integrity sha512-Pw5bq4j/hmgT5ANDr88S7mUbSsNSPdwzr1m+a92pVa6I2GYYA2+erWI4eKHZpXadylP4o5Yst0pn9W2nbKRQ6Q== +"@internxt/sdk@1.22.1": + version "1.22.1" + resolved "https://registry.yarnpkg.com/@internxt/sdk/-/sdk-1.22.1.tgz#8e197658f33f293d62db44e02eebafe2a8c9f7ce" + integrity sha512-O5J/3BwSSPFQefSzb4LbQGVIuzxb8sR7B0tbb3XmjXV90EcWXpqIpixF35mLijab/+0ffa2MbnNyPB4n6I9F/Q== dependencies: axios "^1.16.0" From eac761d5c1c9ec722b527f50fda3e4b739780777 Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Tue, 6 Oct 2026 13:33:28 +0200 Subject: [PATCH 3/3] feat: missing pt keys --- src/app/i18n/locales/pt.json | 66 +++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/app/i18n/locales/pt.json b/src/app/i18n/locales/pt.json index 9f7f950477..6723cf8594 100644 --- a/src/app/i18n/locales/pt.json +++ b/src/app/i18n/locales/pt.json @@ -291,7 +291,70 @@ "businessPlan": "O plano empresarial não está disponível", "postalCodeRequired": "Informe seu CEP para continuar", "countryRequired": "Não foi possível determinar seu país de cobrança. Tente novamente mais tarde.", - "missingPaymentDetails": "Faltam dados de pagamento para confirmar a transação" + "missingPaymentDetails": "Faltam dados de pagamento para confirmar a transação", + "paymentFailed": "Falha no pagamento" + } + }, + "urgentCheckout": { + "header": { + "tagline": "Sua nuvem privada", + "europeanBased": "Com sede na Europa", + "gdprCompliant": "Em conformidade com o RGPD", + "trustedByMillions": "Milhões confiam em nós" + }, + "hero": { + "offerPill": "{{percent}}% OFF — Oferta exclusiva por tempo limitado", + "offerPillNoDiscount": "Oferta exclusiva por tempo limitado", + "title": "Mantenha seus arquivos, fotos e vídeos", + "titleHighlight": "100% privados.", + "subtitle": "Tenha {{storage}} de armazenamento em nuvem privada por apenas {{price}} no primeiro mês.", + "imageAlt": "Internxt Drive em um notebook e em um celular", + "points": { + "privacy": { + "title": "Só você pode acessar seus arquivos", + "description": "A criptografia de conhecimento zero faz com que nem mesmo a Internxt possa acessar seu conteúdo." + }, + "storage": { + "title": "Sua nuvem privada, protegida", + "description": "Guarde suas fotos, vídeos, documentos e arquivos pessoais com privacidade integrada." + }, + "offer": { + "title": "{{percent}}% OFF — por tempo limitado", + "titleNoDiscount": "Oferta exclusiva — por tempo limitado", + "description": "Tenha {{storage}} por apenas {{price}}. Esta oferta exclusiva não vai durar." + } + }, + "badges": { + "basedInEu": "Com sede na UE", + "openSource": "Código aberto", + "audited": "Auditado pela Securitum", + "privacyPriority": "Sua privacidade. Nossa prioridade." + } + }, + "countdown": { + "title": "Sua oferta exclusiva de {{percent}}% OFF expira em:", + "titleNoDiscount": "Sua oferta exclusiva expira em:", + "footer": "Tenha {{storage}} por {{price}} antes que esta oferta acabe." + }, + "summary": { + "discountApplied": "{{percent}}% OFF aplicado", + "estimatedTax": "Impostos estimados" + }, + "form": { + "emailLabel": "Seu endereço de e-mail", + "emailPlaceholder": "voce@email.com", + "createPasswordLabel": "Crie uma senha", + "passwordLabel": "Sua senha", + "passwordPlaceholder": "Senha", + "togglePasswordVisibility": "Mostrar ou ocultar a senha", + "incompleteDetails": "Informe seu e-mail e sua senha para continuar.", + "encryptedTitle": "Seus arquivos são criptografados e privados.", + "encryptedDescription": "A Internxt não pode acessar seu conteúdo." + }, + "cta": { + "button": "Tenha {{storage}} por {{price}}", + "renewalNoteMonthly": "Primeiro mês por {{price}}, depois renova por {{renewalPrice}}/mês. Cancele quando quiser.", + "renewalNoteYearly": "Primeiro ano por {{price}}, depois renova por {{renewalPrice}}/ano. Cancele quando quiser." } }, "general": { @@ -1102,6 +1165,7 @@ }, "storageIsFullDescription": "Você não pode enviar, sincronizar nem fazer backup de arquivos.", "noFilePreview": "Não há pré-visualização disponível para este arquivo", + "filePreviewTooLarge": "Arquivo grande demais para visualizar (mais de {{limit}})", "errorMovingToTrash": "Ocorreu um erro ao mover os itens para a lixeira.", "errorDeletingFromTrash": "Ocorreu um erro ao excluir os itens da lixeira.", "connectionLostError": "Conexão com a internet perdida",