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
7 changes: 6 additions & 1 deletion src/app/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,12 @@
"cryptoRequiresSignIn": "Melde dich an, um mit Kryptowährung zu bezahlen.",
"resendEmail": "E-Mail erneut senden",
"emailResent": "Wir haben dir die E-Mail erneut gesendet",
"resendEmailError": "Wir konnten die E-Mail nicht senden. Bitte versuche es später erneut."
"resendEmailError": "Wir konnten die E-Mail nicht senden. Bitte versuche es später erneut.",
"checkEmail": {
"title": "Prüfe deine E-Mails",
"description": "Deine Zahlung war erfolgreich. Wir haben einen Link zum Abschließen der Kontoeinrichtung gesendet an:",
"spamHint": "Nicht gefunden? Sieh in deinem Spam-Ordner nach."
}
}
},
"urgentCheckout": {
Expand Down
7 changes: 6 additions & 1 deletion src/app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,12 @@
"cryptoRequiresSignIn": "Log in to pay with crypto.",
"resendEmail": "Resend email",
"emailResent": "We've sent you the email again",
"resendEmailError": "We couldn't send the email. Please try again later."
"resendEmailError": "We couldn't send the email. Please try again later.",
"checkEmail": {
"title": "Check your email",
"description": "Your payment was successful. We've sent a link to finish setting up your account to:",
"spamHint": "Can't find it? Check your spam folder."
}
}
},
"urgentCheckout": {
Expand Down
7 changes: 6 additions & 1 deletion src/app/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,12 @@
"cryptoRequiresSignIn": "Inicia sesión para pagar con criptomonedas.",
"resendEmail": "Reenviar correo",
"emailResent": "Te hemos vuelto a enviar el correo",
"resendEmailError": "No hemos podido enviar el correo. Inténtalo de nuevo más tarde."
"resendEmailError": "No hemos podido enviar el correo. Inténtalo de nuevo más tarde.",
"checkEmail": {
"title": "Revisa tu correo",
"description": "El pago se ha completado. Hemos enviado un enlace para terminar de configurar tu cuenta a:",
"spamHint": "¿No lo encuentras? Revisa la carpeta de spam."
}
}
},
"urgentCheckout": {
Expand Down
7 changes: 6 additions & 1 deletion src/app/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,12 @@
"cryptoRequiresSignIn": "Connectez-vous pour payer en cryptomonnaie.",
"resendEmail": "Renvoyer l'e-mail",
"emailResent": "Nous vous avons renvoyé l'e-mail",
"resendEmailError": "Nous n'avons pas pu envoyer l'e-mail. Veuillez réessayer plus tard."
"resendEmailError": "Nous n'avons pas pu envoyer l'e-mail. Veuillez réessayer plus tard.",
"checkEmail": {
"title": "Consultez vos e-mails",
"description": "Votre paiement a été effectué. Nous avons envoyé un lien pour finaliser la configuration de votre compte à :",
"spamHint": "Vous ne le trouvez pas ? Vérifiez votre dossier de spam."
}
}
},
"urgentCheckout": {
Expand Down
7 changes: 6 additions & 1 deletion src/app/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,12 @@
"cryptoRequiresSignIn": "Accedi per pagare con criptovalute.",
"resendEmail": "Invia di nuovo l'email",
"emailResent": "Ti abbiamo inviato di nuovo l'email",
"resendEmailError": "Non siamo riusciti a inviare l'email. Riprova più tardi."
"resendEmailError": "Non siamo riusciti a inviare l'email. Riprova più tardi.",
"checkEmail": {
"title": "Controlla la tua email",
"description": "Il pagamento è andato a buon fine. Abbiamo inviato un link per completare la configurazione del tuo account a:",
"spamHint": "Non la trovi? Controlla la cartella dello spam."
}
}
},
"urgentCheckout": {
Expand Down
31 changes: 30 additions & 1 deletion src/app/i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,12 @@
"cryptoRequiresSignIn": "Faça login para pagar com criptomoedas.",
"resendEmail": "Reenviar e-mail",
"emailResent": "Enviamos o e-mail novamente",
"resendEmailError": "Não foi possível enviar o e-mail. Tente novamente mais tarde."
"resendEmailError": "Não foi possível enviar o e-mail. Tente novamente mais tarde.",
"checkEmail": {
"title": "Verifique seu e-mail",
"description": "Seu pagamento foi concluído. Enviamos um link para concluir a configuração da sua conta para:",
"spamHint": "Não encontrou? Verifique sua pasta de spam."
}
}
},
"urgentCheckout": {
Expand Down Expand Up @@ -1582,6 +1587,30 @@
}
}
},
"accountSetup": {
"form": {
"title": "Configure sua conta",
"description": "Crie uma senha para concluir a configuração da sua conta Internxt.",
"confirmPassword": "Confirmar senha",
"passwordsDoNotMatch": "As senhas não coincidem",
"submit": "Criar conta",
"genericError": "Algo deu errado ao configurar sua conta. Tente novamente."
},
"invalidLink": {
"title": "Este link não é mais válido",
"description": "O link expirou ou já foi usado. Informe seu e-mail e enviaremos um novo.",
"emailLabel": "E-mail",
"resendButton": "Enviar um novo link",
"resendError": "Não foi possível enviar o e-mail. Tente novamente.",
"emailSentTitle": "Verifique seu e-mail",
"emailSentDescription": "Se {{email}} tiver uma conta aguardando configuração, você receberá um novo link em breve."
},
"accountAlreadyExists": {
"title": "Sua conta já está configurada",
"description": "Esta conta já existe. Faça login com seu e-mail e senha para acessá-la.",
"loginButton": "Ir para o login"
}
},
"blockedAccount": {
"title": "Confirme que é você",
"text1": "Houve tentativas de login malsucedidas demais.",
Expand Down
7 changes: 6 additions & 1 deletion src/app/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,12 @@
"cryptoRequiresSignIn": "Войдите, чтобы оплатить криптовалютой.",
"resendEmail": "Отправить письмо ещё раз",
"emailResent": "Мы отправили письмо ещё раз",
"resendEmailError": "Не удалось отправить письмо. Повторите попытку позже."
"resendEmailError": "Не удалось отправить письмо. Повторите попытку позже.",
"checkEmail": {
"title": "Проверьте почту",
"description": "Оплата прошла успешно. Мы отправили ссылку для завершения настройки аккаунта на адрес:",
"spamHint": "Не нашли письмо? Проверьте папку «Спам»."
}
}
},
"urgentCheckout": {
Expand Down
7 changes: 6 additions & 1 deletion src/app/i18n/locales/tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,12 @@
"cryptoRequiresSignIn": "請登入後使用加密貨幣付款。",
"resendEmail": "重新寄送郵件",
"emailResent": "我們已重新寄送郵件",
"resendEmailError": "無法寄送郵件,請稍後再試。"
"resendEmailError": "無法寄送郵件,請稍後再試。",
"checkEmail": {
"title": "請查看你的電子郵件",
"description": "付款成功。我們已將完成帳戶設定的連結寄送至:",
"spamHint": "找不到郵件?請檢查垃圾郵件資料夾。"
}
}
},
"urgentCheckout": {
Expand Down
7 changes: 6 additions & 1 deletion src/app/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,12 @@
"cryptoRequiresSignIn": "请登录后使用加密货币付款。",
"resendEmail": "重新发送邮件",
"emailResent": "我们已重新发送邮件",
"resendEmailError": "无法发送邮件,请稍后重试。"
"resendEmailError": "无法发送邮件,请稍后重试。",
"checkEmail": {
"title": "请查看你的电子邮件",
"description": "付款成功。我们已将完成账户设置的链接发送至:",
"spamHint": "找不到邮件?请检查垃圾邮件文件夹。"
}
}
},
"urgentCheckout": {
Expand Down
2 changes: 1 addition & 1 deletion src/app/routes/paths.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
"layout": "empty",
"path": "/checkout/success",
"exact": false,
"auth": true
"auth": false
},
{
"id": "checkout-cancel",
Expand Down
2 changes: 1 addition & 1 deletion src/views/AccountSetup/AccountSetupView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ vi.mock('services/encrypted-storage.service', () => ({
default: { setToken: vi.fn(), getToken: vi.fn(), getUser: vi.fn(), clear: vi.fn() },
}));
vi.mock('services/local-storage.service', () => ({
default: { get: vi.fn(), set: vi.fn(), clear: vi.fn() },
default: { get: vi.fn(), set: vi.fn(), clear: vi.fn(), clearExcept: vi.fn() },
}));
vi.mock('app/store/slices/user', () => ({
initializeUserThunk: vi.fn(),
Expand Down
12 changes: 11 additions & 1 deletion src/views/AccountSetup/services/completeAccountSetup.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { CompleteAccountSetupPayload } from '@internxt/sdk/dist/auth/types';
import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';
import { LocalStorageItem } from 'app/core/types';
import { decryptText, decryptTextWithKey, passToHash } from 'app/crypto/services/utils';
import { planThunks } from 'app/store/slices/plan';
import { userThunks } from 'app/store/slices/user';
import { validateMnemonic } from 'bip39';
import { Buffer } from 'node:buffer';
import encryptedStorageService from 'services/encrypted-storage.service';
import envService from 'services/env.service';
import localStorageService from 'services/local-storage.service';
import { getCompleteAccountSetupResponse } from 'testUtils/fixtures/accountSetup.fixtures';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { completeAccountSetup } from './completeAccountSetup';
Expand All @@ -23,7 +25,7 @@ vi.mock('services/encrypted-storage.service', () => ({
default: { setToken: vi.fn(), getToken: vi.fn(), getUser: vi.fn(), clear: vi.fn() },
}));
vi.mock('services/local-storage.service', () => ({
default: { get: vi.fn(), set: vi.fn(), clear: vi.fn(), clearExcept: vi.fn() },
default: { get: vi.fn(), set: vi.fn(), clear: vi.fn(), clearExcept: vi.fn(), removeItem: vi.fn() },
}));
vi.mock('app/store/slices/user', () => ({
initializeUserThunk: vi.fn(),
Expand Down Expand Up @@ -101,5 +103,13 @@ describe('Completing the setup of a paid account', () => {
);
expect(encryptedStorageService.setToken).not.toHaveBeenCalled();
expect(userThunks.setUserThunk).not.toHaveBeenCalled();
expect(localStorageService.clearExcept).not.toHaveBeenCalled();
});

test('When the account is set up, then the checkout "check your email" screen is no longer shown on a later visit', async () => {
await completeAccountSetup({ setupToken: SETUP_TOKEN, password: PASSWORD, dispatch: vi.fn() });

const [keysToPreserve] = vi.mocked(localStorageService.clearExcept).mock.calls[0];
expect(keysToPreserve).not.toContain(LocalStorageItem.CheckoutAccountSetupEmail);
});
});
4 changes: 3 additions & 1 deletion src/views/AccountSetup/services/completeAccountSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const completeAccountSetup = async ({ setupToken, password, dispatch }: C
const { user, token, newToken } = await authClient.completeAccountSetup({ token: setupToken, ...credentials });
const mnemonic = decryptTextWithKey(user.mnemonic, password);

return startNewAccountSession({
const session = await startNewAccountSession({
registeredUser: {
xUser: { ...user, mnemonic } as unknown as UserSettings,
xToken: token,
Expand All @@ -31,4 +31,6 @@ export const completeAccountSetup = async ({ setupToken, password, dispatch }: C
redeemCodeObject: false,
dispatch,
});

return session;
};
49 changes: 46 additions & 3 deletions src/views/Checkout/views/CheckoutSuccessView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import CheckoutSuccessView from './CheckoutSuccessView';
import { AppView } from 'app/core/types';
import { AppView, LocalStorageItem } from 'app/core/types';
import { PURCHASE_LOCAL_STORAGE_ITEMS } from 'services/storage-keys';

const mocks = vi.hoisted(() => ({
localStorageRemoveItem: vi.fn(),
localStorageGet: vi.fn(),
getSessionToken: vi.fn(),
navigationPush: vi.fn(),
retrievePaymentIntent: vi.fn(),
retrieveSetupIntent: vi.fn(),
Expand All @@ -19,7 +21,13 @@ const mocks = vi.hoisted(() => ({

vi.mock('app/store/hooks', () => ({ useAppDispatch: () => vi.fn() }));
vi.mock('services/navigation.service', () => ({ default: { push: mocks.navigationPush } }));
vi.mock('services/local-storage.service', () => ({ default: { removeItem: mocks.localStorageRemoveItem } }));
vi.mock('services/local-storage.service', () => ({
default: { removeItem: mocks.localStorageRemoveItem, get: mocks.localStorageGet },
}));
vi.mock('services/encrypted-storage.service', () => ({ default: { getToken: mocks.getSessionToken } }));
vi.mock('../components/ResendAccountSetupEmailButton', () => ({
ResendAccountSetupEmailButton: () => <button>checkout.accountSetup.resendEmail</button>,
}));
vi.mock('app/analytics/meta.service', () => ({ default: { trackPurchase: mocks.metaTrackPurchase } }));
vi.mock('app/analytics/ga.service', () => ({ default: { trackPurchase: mocks.gaTrackPurchase } }));
vi.mock('app/analytics/impact.service', () => ({ trackPaymentConversion: mocks.trackPaymentConversion }));
Expand Down Expand Up @@ -94,9 +102,44 @@ const expectCheckoutFinished = () => {
describe('Checkout success view', () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.getSessionToken.mockReturnValue('session_token');
mocks.localStorageGet.mockReturnValue(null);
landOn(SUCCESS_PATH);
});

describe('When a buyer without an account finishes paying', () => {
beforeEach(() => {
mocks.getSessionToken.mockReturnValue(undefined);
mocks.localStorageGet.mockImplementation((key: string) =>
key === LocalStorageItem.CheckoutAccountSetupEmail ? 'new.buyer@internxt.com' : null,
);
});

test('then they are asked to check the email the setup link was sent to, and they stay on the page', async () => {
render(<CheckoutSuccessView />);

expect(await screen.findByText('checkout.accountSetup.checkEmail.title')).toBeInTheDocument();
expect(screen.getByText('new.buyer@internxt.com')).toBeInTheDocument();
expect(screen.getByText('checkout.accountSetup.checkEmail.spamHint')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'checkout.accountSetup.resendEmail' })).toBeInTheDocument();
await waitFor(() => expectPurchaseTracked());
PURCHASE_LOCAL_STORAGE_ITEMS.forEach((item) => expect(mocks.localStorageRemoveItem).toHaveBeenCalledWith(item));
expect(mocks.userStoragePolling).not.toHaveBeenCalled();
expect(mocks.navigationPush).not.toHaveBeenCalled();
});
});

describe('When a logged-in user finishes paying after a previous purchase without an account', () => {
test('then they are taken to Drive instead of being asked to check their email', async () => {
mocks.localStorageGet.mockReturnValue('old.buyer@internxt.com');

await renderAndWaitForRedirect();

expect(screen.queryByText('checkout.accountSetup.checkEmail.title')).not.toBeInTheDocument();
expectCheckoutFinished();
});
});

describe('When the customer paid without Stripe redirecting back (crypto, lifetime with a 100% off coupon)', () => {
test('then the purchase is tracked without asking Stripe and the checkout is finished', async () => {
await renderAndWaitForRedirect();
Expand Down
41 changes: 38 additions & 3 deletions src/views/Checkout/views/CheckoutSuccessView.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,45 @@
import useEffectAsync from 'hooks/useEffectAsync';
import navigationService from 'services/navigation.service';
import { AppView } from 'app/core/types';
import { AppView, LocalStorageItem } from 'app/core/types';
import { useAppDispatch } from 'app/store/hooks';
import { useCallback, useRef } from 'react';
import { useCallback, useRef, useState } from 'react';
import { CheckCircle } from '@phosphor-icons/react';

Check warning on line 6 in src/views/Checkout/views/CheckoutSuccessView.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'CheckCircle' is deprecated.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-web&issues=AaEReFpPV0XCE97sY1yB&open=AaEReFpPV0XCE97sY1yB&pullRequest=2189
import localStorageService from 'services/local-storage.service';
import encryptedStorageService from 'services/encrypted-storage.service';
import { useTranslationContext } from 'app/i18n/provider/TranslationProvider';
import { trackPaymentConversion } from 'app/analytics/impact.service';
import gaService from 'app/analytics/ga.service';
import { PURCHASE_LOCAL_STORAGE_ITEMS } from 'services/storage-keys';
import metaService from 'app/analytics/meta.service';
import { userStoragePolling } from 'utils/userStoragePolling.utils';
import { useTranslationContext } from 'app/i18n/provider/TranslationProvider';
import notificationsService, { ToastType } from 'app/notifications/services/notifications.service';
import { paymentService } from '../services';
import envService from 'services/env.service';
import { ResendAccountSetupEmailButton } from '../components/ResendAccountSetupEmailButton';

export function removePaymentsStorage() {
PURCHASE_LOCAL_STORAGE_ITEMS.forEach((item) => localStorageService.removeItem(item));
}

const AccountSetupEmailSentCard = ({ email }: { email: string }) => {
const { translate } = useTranslationContext();

return (
<div className="flex h-full w-full items-center justify-center bg-gray-1 px-5">
<div className="flex w-full max-w-md flex-col items-center gap-6 rounded-2xl border border-gray-10 bg-surface p-8 text-center">
<CheckCircle size={80} weight="thin" className="text-primary" />

Check warning on line 30 in src/views/Checkout/views/CheckoutSuccessView.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'CheckCircle' is deprecated.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-web&issues=AaEReFpPV0XCE97sY1yC&open=AaEReFpPV0XCE97sY1yC&pullRequest=2189
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-medium text-gray-100">{translate('checkout.accountSetup.checkEmail.title')}</h1>
<p className="text-gray-80">{translate('checkout.accountSetup.checkEmail.description')}</p>
<p className="break-all font-medium text-gray-100">{email}</p>
</div>
<ResendAccountSetupEmailButton email={email} />
<p className="text-sm text-gray-50">{translate('checkout.accountSetup.checkEmail.spamHint')}</p>
</div>
</div>
);
};

const hasStripeReportedPaymentFailed = async (): Promise<boolean> => {
const params = new URLSearchParams(globalThis.location.search);
const setupIntentSecret = params.get('setup_intent_client_secret');
Expand Down Expand Up @@ -56,7 +78,12 @@
const dispatch = useAppDispatch();
const { translate } = useTranslationContext();
const hasTrackedRef = useRef(false);
const hasSession = Boolean(encryptedStorageService.getToken());
const [accountSetupEmail] = useState(() =>
hasSession ? null : localStorageService.get(LocalStorageItem.CheckoutAccountSetupEmail),
);

// `startNewAccountSession` already clears the storage, so no need to clear the CheckoutAccountSetupEmail variable here
const onCheckoutSuccess = useCallback(async () => {
if (hasTrackedRef.current) {
return;
Expand All @@ -76,13 +103,21 @@
console.error('Analytics error:', err);
}

if (accountSetupEmail) {
return;
}
Comment on lines +106 to +108

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case the CheckoutAccountSetupEmail is never removed. Is this the desired behaviour?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, as we use this variable to display either the "Setup email has been sent" screen or redirect the user to the app (like the current flow). This variable should be removed after the account setup. Maybe this component is not the place to remove the variable and we can move the removal to another place.


userStoragePolling();

navigationService.push(AppView.Drive);
}, [dispatch, translate]);

useEffectAsync(onCheckoutSuccess, []);

if (accountSetupEmail) {
return <AccountSetupEmailSentCard email={accountSetupEmail} />;
}

return <div></div>;
};

Expand Down
Loading