Merge pull request #17779 from mozilla/FXA-9561

feat(settings): Redirect to relying party after password reset
This commit is contained in:
Valerie Pomerleau 2024-10-09 14:27:14 -07:00 коммит произвёл GitHub
Родитель bcfabbed7b 21aa85d7ba
Коммит ae82b7f823
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
31 изменённых файлов: 549 добавлений и 239 удалений

Просмотреть файл

@ -70,9 +70,13 @@ export class ResetPasswordPage extends BaseLayout {
return this.page.getByRole('button', { name: 'Create new password' });
}
get passwordResetConfirmationHeading() {
return this.page.getByRole('heading', {
name: 'Your password has been reset',
get passwordResetSuccessMessage() {
return this.page.getByText('Your password has been reset');
}
get passwordResetConfirmationContinueButton() {
return this.page.getByRole('button', {
name: /^Continue to/,
});
}

Просмотреть файл

@ -31,22 +31,13 @@ test.describe('severity-1 #smoke', () => {
await resetPassword.fillOutNewPasswordForm(newPassword);
await expect(page).toHaveURL(/reset_password_verified/);
await expect(resetPassword.passwordResetSuccessMessage).toBeVisible();
await expect(
resetPassword.passwordResetConfirmationHeading
resetPassword.passwordResetConfirmationContinueButton
).toBeVisible();
// TODO in FXA-9561 - Verify that the service name is displayed in the "Continue to ${serviceName}" button
// This functionality is not yet implemented in the reset password flow
// TODO in FXA-9561 - Remove this temporary test of sign in with new password
// user should be signed in and able to navigate to the relying party
await relier.goto();
await relier.clickEmailFirst();
// a successful password reset means that the user is signed in
await expect(signin.cachedSigninHeading).toBeVisible();
await signin.signInButton.click();
await resetPassword.passwordResetConfirmationContinueButton.click();
await expect(page).toHaveURL(target.relierUrl);
expect(await relier.isLoggedIn()).toBe(true);
@ -61,6 +52,10 @@ test.describe('severity-1 #smoke', () => {
pages: { resetPassword, signin },
testAccountTracker,
}) => {
test.fixme(
true,
'FXA-10518 - 123Done PKCE button is not working, and this test does not validate the PKCE flow for sign-in after reset password'
);
const credentials = await testAccountTracker.signUp();
const newPassword = testAccountTracker.generatePassword();
@ -89,12 +84,7 @@ test.describe('severity-1 #smoke', () => {
await resetPassword.fillOutNewPasswordForm(newPassword);
await expect(page).toHaveURL(/reset_password_verified/);
await expect(
resetPassword.passwordResetConfirmationHeading
).toBeVisible();
// TODO in FXA-9561 - Verify that the service name is displayed in the "Continue to ${serviceName}" button
// This functionality is not yet implemented in the reset password flow
await expect(resetPassword.passwordResetSuccessMessage).toBeVisible();
// update password for cleanup function
credentials.password = newPassword;
@ -144,21 +134,22 @@ test.describe('severity-1 #smoke', () => {
await resetPassword.fillOutNewPasswordForm(newPassword);
await expect(page).toHaveURL(/reset_password_verified/);
await expect(
resetPassword.passwordResetConfirmationHeading
).toBeVisible();
await expect(page).toHaveURL(/signin/);
await expect(resetPassword.passwordResetSuccessMessage).toBeVisible();
await expect(signin.passwordFormHeading).toBeVisible();
await signin.fillOutPasswordForm(newPassword);
await expect(page).toHaveURL(/signin_totp_code/);
totpCode = await getCode(secret);
await signinTotpCode.fillOutCodeForm(totpCode);
await expect(page).toHaveURL(target.relierUrl);
expect(await relier.isLoggedIn()).toBe(true);
// Goes to settings and disables totp on user's account (required for cleanup)
await signin.goto();
await signin.fillOutEmailFirstForm(credentials.email);
await signin.fillOutPasswordForm(newPassword);
await expect(page).toHaveURL(/signin_totp_code/);
totpCode = await getCode(secret);
await expect(page).toHaveURL(/signin_totp_code/);
await signinTotpCode.fillOutCodeForm(totpCode);
await expect(signin.cachedSigninHeading).toBeVisible();
await signin.signInButton.click();
await expect(settings.settingsHeading).toBeVisible();
await settings.disconnectTotp();

Просмотреть файл

@ -44,12 +44,19 @@ test.describe('severity-1 #smoke', () => {
await resetPassword.fillOutNewPasswordForm(newPassword);
await expect(page).toHaveURL(/reset_password_with_recovery_key_verified/);
await expect(
resetPassword.passwordResetConfirmationHeading
).toBeVisible();
await expect(resetPassword.passwordResetSuccessMessage).toBeVisible();
await expect(resetPassword.generateRecoveryKeyButton).toBeVisible();
// TODO in FXA-9561 - Verify that the service name is displayed in the "Continue to ${serviceName}" button
// This functionality is not yet implemented in the reset password flow
// User is shown a prompt to generate a new recovery key
// and is signed in after the password reset so can click through to create a new key.
await resetPassword.generateRecoveryKeyButton.click();
await expect(recoveryKey.accountRecoveryKeyHeading).toBeVisible();
await recoveryKey.createRecoveryKey(newPassword, 'hint');
// Currently redirects to settings page after creating a new recovery key
// In FXA-7904, we will redirect back to the relier after a new key is autogenerated.
await expect(settings.settingsHeading).toBeVisible();
await expect(settings.recoveryKey.status).toHaveText('Enabled');
// update password for cleanup function
credentials.password = newPassword;

Просмотреть файл

@ -32,12 +32,16 @@ test.describe('severity-1 #smoke', () => {
await resetPassword.fillOutNewPasswordForm(newPassword);
await expect(page).toHaveURL(/reset_password_verified/);
await expect(resetPassword.passwordResetSuccessMessage).toBeVisible();
await expect(
resetPassword.passwordResetConfirmationHeading
resetPassword.passwordResetConfirmationContinueButton
).toBeVisible();
// TODO in FXA-9561 - Verify that the service name is displayed in the "Continue to ${serviceName}" button
// This functionality is not yet implemented in the reset password flow
await resetPassword.passwordResetConfirmationContinueButton.click();
await expect(page).toHaveURL(target.relierUrl);
expect(await relier.isLoggedIn()).toBe(true);
// update password for cleanup function
credentials.password = newPassword;

Просмотреть файл

@ -11,13 +11,7 @@ test.describe('severity-1 #smoke', () => {
test.describe('oauth reset password Sync mobile react', () => {
test('reset password through Sync mobile', async ({
target,
syncBrowserPages: {
page,
connectAnotherDevice,
resetPassword,
signin,
settings,
},
syncBrowserPages: { page, resetPassword, signin, settings },
testAccountTracker,
}) => {
const credentials = await testAccountTracker.signUp();

Просмотреть файл

@ -187,13 +187,8 @@ test.describe('severity-1 #smoke', () => {
// Create and submit new password
await resetPassword.fillOutNewPasswordForm(newPassword);
// Wait for new page to navigate
await expect(page).toHaveURL(/reset_password_verified/);
await page.goto(target.contentServerUrl);
await signin.fillOutEmailFirstForm(credentials.email);
await expect(page).toHaveURL(/signin/);
await expect(resetPassword.passwordResetSuccessMessage).toBeVisible();
await signin.fillOutPasswordForm(newPassword);
totpCode = await getCode(secret);
@ -258,13 +253,8 @@ test.describe('severity-1 #smoke', () => {
// Create and submit new password
await resetPassword.fillOutNewPasswordForm(newPassword);
// Wait for new page to navigate
await expect(page).toHaveURL(/reset_password_verified/);
await page.goto(target.contentServerUrl);
await signin.fillOutEmailFirstForm(credentials.email);
await expect(page).toHaveURL(/signin/);
await expect(resetPassword.passwordResetSuccessMessage).toBeVisible();
await signin.fillOutPasswordForm(newPassword);
const totpCode = await getCode(secret);
@ -395,10 +385,7 @@ test.describe('severity-1 #smoke', () => {
// Create recovery key
await settings.recoveryKey.createButton.click();
const key = await recoveryKey.createRecoveryKey(
credentials.password,
'hint'
);
await recoveryKey.createRecoveryKey(credentials.password, 'hint');
// Verify status as 'enabled'
await expect(settings.settingsHeading).toBeVisible();
@ -430,11 +417,8 @@ test.describe('severity-1 #smoke', () => {
// Create and submit new password
await resetPassword.fillOutNewPasswordForm(newPassword);
// Wait for new page to navigate
await expect(page).toHaveURL(/reset_password_verified/);
await page.goto(target.contentServerUrl);
await signin.fillOutEmailFirstForm(credentials.email);
await expect(page).toHaveURL(/signin/);
await expect(resetPassword.passwordResetSuccessMessage).toBeVisible();
await signin.fillOutPasswordForm(newPassword);
// Prompted for 2FA on new login

Просмотреть файл

@ -63,36 +63,18 @@ test.describe('severity-1 #smoke', () => {
await resetPassword.fillOutNewPasswordForm(newPassword);
// After using a recovery key to reset password, expect to be prompted to create a new one
await expect(page).toHaveURL(/reset_password_with_recovery_key_verified/);
await expect(resetPassword.passwordResetSuccessMessage).toBeVisible();
await expect(resetPassword.generateRecoveryKeyButton).toBeVisible();
// User is shown a prompt to generate a new recovery key
// and is signed in after the password reset so can click through to create a new key.
await resetPassword.generateRecoveryKeyButton.click();
await expect(recoveryKey.accountRecoveryKeyHeading).toBeVisible();
await recoveryKey.createRecoveryKey(newPassword, 'hint');
// TODO in FXA-7904 - Verify that a new recovery key is generated without needing to sign in again
// not currently implemented
await page.waitForURL(/settings\/account_recovery/);
// Attempt to sign in with new password
const { sessionToken } = await target.authClient.signIn(
credentials.email,
newPassword
);
const newAccountData = await target.authClient.sessionReauth(
sessionToken,
credentials.email,
newPassword,
{
keys: true,
reason: 'recovery_key',
}
);
expect(newAccountData.keyFetchToken).toBeDefined();
expect(newAccountData.unwrapBKey).toBeDefined();
const newEncryptionKeys = await target.authClient.accountKeys(
newAccountData.keyFetchToken as string,
newAccountData.unwrapBKey as string
);
expect(originalEncryptionKeys).toEqual(newEncryptionKeys);
await expect(settings.settingsHeading).toBeVisible();
await expect(settings.recoveryKey.status).toHaveText('Enabled');
// update password for cleanup function
credentials.password = newPassword;

Просмотреть файл

@ -415,9 +415,16 @@ Router = Router.extend({
);
},
'reset_password_confirmed(/)': createViewHandler(ReadyView, {
type: VerificationReasons.PASSWORD_RESET,
}),
'reset_password_confirmed(/)': function () {
this.createReactOrBackboneViewHandler(
'reset_password_verified',
ReadyView,
null,
{
type: VerificationReasons.PASSWORD_RESET,
}
);
},
'reset_password_with_recovery_key_verified(/)': function () {
this.createReactOrBackboneViewHandler(

Просмотреть файл

@ -57,8 +57,8 @@ import ResetPasswordContainer from '../../pages/ResetPassword/ResetPassword/cont
import ConfirmResetPasswordContainer from '../../pages/ResetPassword/ConfirmResetPassword/container';
import CompleteResetPasswordContainer from '../../pages/ResetPassword/CompleteResetPassword/container';
import AccountRecoveryConfirmKeyContainer from '../../pages/ResetPassword/AccountRecoveryConfirmKey/container';
import ResetPasswordConfirmed from '../../pages/ResetPassword/ResetPasswordConfirmed';
import ConfirmTotpResetPasswordContainer from '../../pages/ResetPassword/ConfirmTotpResetPassword/container';
import ResetPasswordConfirmedContainer from '../../pages/ResetPassword/ResetPasswordConfirmed/container';
import ResetPasswordWithRecoveryKeyVerified from '../../pages/ResetPassword/ResetPasswordWithRecoveryKeyVerified';
import CompleteSigninContainer from '../../pages/Signin/CompleteSignin/container';
import SigninContainer from '../../pages/Signin/container';
@ -344,9 +344,9 @@ const AuthAndAccountSetupRoutes = ({
path="/reset_password_with_recovery_key_verified/*"
{...{ integration, isSignedIn }}
/>
<ResetPasswordConfirmed
<ResetPasswordConfirmedContainer
path="/reset_password_verified/*"
{...{ isSignedIn, serviceName }}
{...{ integration, serviceName }}
/>
{/* Signin */}

Просмотреть файл

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React, { useState } from 'react';
import { CircleCheckOutlineImage, RecoveryKeyImage } from '../images';
import { CheckmarkCircleOutlineIcon, RecoveryKeyImage } from '../images';
import { FtlMsg } from 'fxa-react/lib/utils';
import Banner, { BannerType } from '../Banner';
import { CreateRecoveryKeyHandler } from '../../pages/InlineRecoveryKeySetup/interfaces';
@ -33,7 +33,7 @@ export const InlineRecoveryKeySetupCreate = ({
<>
<Banner type={BannerType.success} additionalClassNames="mt-0">
<p className="flex justify-center text-base">
<CircleCheckOutlineImage className="me-3" />
<CheckmarkCircleOutlineIcon className="me-3" />
<span>
<FtlMsg id="inline-recovery-key-setup-signed-in-firefox">
Youre signed in to Firefox

Просмотреть файл

@ -1,6 +1,5 @@
## Ready component
reset-password-complete-header = Your password has been reset
ready-complete-set-up-instruction = Complete setup by entering your new password on your other { -brand-firefox } devices.
manage-your-account-button = Manage your account
# This is a string that tells the user they can use whatever service prompted them to reset their password or to verify their email

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

Ширина:  |  Высота:  |  Размер: 8.2 KiB

Просмотреть файл

@ -0,0 +1,139 @@
<svg width="251" height="147" viewBox="0 0 251 147" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.animate-type-first-repeat {
animation: asterisk1Animation 5s infinite ease-in-out;
}
.animate-type-second-repeat {
animation: asterisk2Animation 5s infinite ease-in-out;
}
.animate-type-third-repeat {
animation: asterisk3Animation 5s infinite ease-in-out;
}
@keyframes asterisk1Animation {
0%, 10%, 100% { opacity: 0 }
20%, 80% { opacity: 1 }
}
@keyframes asterisk2Animation {
0%, 20%, 100% { opacity: 0; }
30%, 80% { opacity: 1; }
}
@keyframes asterisk3Animation {
0%, 30%, 100% { opacity: 0; }
40%, 80% { opacity: 1; }
}
.animate-pulse-first {
animation: twinkle 2.5s infinite ease-in-out alternate;
}
.animate-pulse-second {
animation: twinkle 3s infinite ease-in-out alternate;
}
.animate-pulse-third {
animation: twinkle 3.5s infinite ease-in-out alternate;
}
@keyframes twinkle {
0%, 100% {
transform: scale(0.9);
opacity: 0.2;
}
50% {
transform: scale(1.1);
opacity: 1;
}
}
.animate-draw-stroke-repeat {
animation: drawCheckmark 5s ease-in forwards infinite;
}
@keyframes drawCheckmark {
0% {
stroke-dasharray: 100;
stroke-dashoffset: 100;
opacity:0;
}
10% {
opacity:1;
}
50% {
stroke-dasharray: 100;
stroke-dashoffset: 100;
}
60% {
stroke-dashoffset: 0;
}
80% {
opacity:1;
}
100% {
opacity:0;
stroke-dashoffset: 0;
}
}
</style>
<!-- Large cloud -->
<g filter="url(#filter0_d_5194_14463)">
<path d="M147.886 64.891C147.692 64.891 147.5 64.9011 147.307 64.9056C147.307 64.9007 147.308 64.8959 147.308 64.891C147.308 47.8304 133.443 34 116.341 34C102.452 34 90.7 43.1205 86.7729 55.6845C83.8083 54.1271 80.4339 53.2413 76.8505 53.2413C65.0711 53.2413 55.5221 62.7669 55.5221 74.5175C55.5221 76.4317 55.7796 78.2852 56.2548 80.0498C54.1152 79.1017 51.7524 78.5641 49.2603 78.5641C39.7277 78.5641 32 86.2729 32 95.782C32 105.291 39.7277 113 49.2603 113H147.886C161.204 113 172 102.23 172 88.9455C172 75.6604 161.204 64.891 147.886 64.891Z" fill="white"/>
</g>
<!-- Small cloud -->
<g filter="url(#filter1_d_5194_14463)">
<path d="M129.602 75.8526C129.752 75.8526 129.9 75.8603 130.049 75.8638C130.049 75.8601 130.048 75.8563 130.048 75.8526C130.048 62.6792 140.744 52 153.937 52C164.651 52 173.717 59.0424 176.747 68.7438C179.034 67.5412 181.637 66.8572 184.401 66.8572C193.488 66.8572 200.854 74.2124 200.854 83.2856C200.854 84.7637 200.656 86.1949 200.289 87.5574C201.94 86.8253 203.762 86.4102 205.685 86.4102C213.039 86.4102 219 92.3626 219 99.7051C219 107.048 213.039 113 205.685 113H129.602C119.328 113 111 104.684 111 94.4262C111 84.1682 119.328 75.8526 129.602 75.8526Z" fill="white"/>
</g>
<!-- Green oval -->
<rect x="47" y="98" width="157.143" height="40.8571" rx="20.4286" fill="#1CC5A0"/>
<!-- Asterisks -->
<path class="animate-type-first-repeat" d="M88.8676 120.382C89.0542 120.569 89.2408 120.755 89.2408 120.942C89.4275 121.128 89.4275 121.315 89.2408 121.688L87.3748 124.861C87.1882 125.047 87.0016 125.234 86.815 125.234C86.6283 125.234 86.4417 125.234 86.2551 125.047L82.3364 122.062L82.8962 127.1C82.8962 127.287 82.8962 127.66 82.7096 127.66C82.523 127.846 82.3364 128.033 82.1498 128.033H78.4176C78.231 128.033 78.0444 127.846 77.8578 127.66C77.6712 127.473 77.6712 127.287 77.6712 127.1L78.231 121.875L74.1257 124.861C73.9391 125.047 73.7525 125.047 73.3792 125.047C73.1926 125.047 73.006 124.861 72.8194 124.674L71.14 121.502C70.9533 121.315 70.9533 121.128 71.14 120.755C71.14 120.569 71.3266 120.382 71.5132 120.195L76.1783 118.329L71.5132 116.277C71.3266 116.277 71.14 116.09 71.14 115.717C70.9533 115.53 70.9533 115.157 71.14 114.97L72.8194 111.985C73.006 111.798 73.1926 111.612 73.3792 111.612C73.5658 111.612 73.9391 111.612 74.1257 111.798L78.231 114.97L77.6712 110.119C77.6712 109.932 77.6712 109.559 77.8578 109.372C78.0444 109.186 78.231 108.999 78.4176 108.999H81.9632C82.1498 108.999 82.523 109.186 82.523 109.372C82.7096 109.559 82.7096 109.745 82.7096 110.119L81.9632 115.157L86.0685 112.171C86.2551 111.985 86.4417 111.985 86.815 111.985C87.0016 111.985 87.1882 112.171 87.3748 112.358L89.0542 115.53C89.2408 115.717 89.2408 115.903 89.0542 116.277C89.0542 116.463 88.8676 116.65 88.681 116.837L84.0158 118.889L88.8676 120.382Z" fill="white"/>
<path class="animate-type-second-repeat" d="M115.555 120.569C115.741 120.569 115.928 120.755 116.115 121.128C116.301 121.315 116.301 121.502 116.115 121.875L114.435 124.861C114.249 125.047 114.062 125.234 113.875 125.234C113.689 125.234 113.316 125.234 113.129 125.047L109.21 122.062L109.77 127.1C109.77 127.287 109.77 127.66 109.583 127.66C109.397 127.846 109.024 127.846 108.837 127.846H105.291C105.105 127.846 104.732 127.66 104.732 127.473C104.545 127.287 104.545 127.1 104.545 126.913L105.105 121.688L100.999 124.861C100.813 125.047 100.626 125.047 100.253 125.047C100.066 125.047 99.88 124.861 99.693 124.674L98.014 121.502C97.827 121.315 97.827 121.128 98.014 120.755C98.014 120.569 98.2 120.382 98.387 120.195L103.052 118.329L98.387 116.277C98.2 116.277 98.014 116.09 98.014 115.717C97.827 115.53 97.827 115.157 98.014 114.97L99.88 111.985C100.066 111.798 100.253 111.612 100.44 111.612C100.626 111.612 100.813 111.798 100.999 111.985L105.105 114.97L104.545 110.119C104.545 109.932 104.545 109.559 104.732 109.372C104.918 109.186 105.105 108.999 105.291 108.999H108.837C109.024 108.999 109.397 109.186 109.397 109.372C109.583 109.559 109.583 109.745 109.583 110.119L108.837 114.97L113.129 111.985C113.316 111.798 113.502 111.798 113.689 111.798C113.875 111.798 114.062 111.985 114.249 112.171L115.928 115.344C116.115 115.53 116.115 115.717 115.928 116.09C115.928 116.277 115.741 116.463 115.555 116.65L110.89 118.703L115.555 120.569Z" fill="white"/>
<path class="animate-type-third-repeat" d="M142.053 120.569C142.24 120.755 142.426 120.942 142.426 121.128C142.613 121.315 142.613 121.502 142.426 121.875L140.56 125.047C140.374 125.234 140.187 125.42 140 125.42C139.814 125.42 139.441 125.42 139.254 125.234L135.522 122.248L136.082 127.287C136.082 127.473 136.082 127.846 135.895 128.033C135.708 128.22 135.522 128.22 135.335 128.22H131.603C131.416 128.22 131.23 128.033 131.043 127.846C130.857 127.66 130.857 127.473 130.857 127.287L131.416 122.062L127.311 125.047C127.124 125.234 126.938 125.234 126.565 125.234C126.378 125.234 126.191 125.047 126.005 124.861L124.325 121.688C124.139 121.502 124.139 121.315 124.325 120.942C124.325 120.755 124.512 120.569 124.699 120.382L129.177 118.516L124.512 116.463C124.325 116.463 124.139 116.277 124.139 115.903C123.952 115.717 123.952 115.344 124.139 115.157L126.005 112.171C126.191 111.985 126.378 111.798 126.565 111.798C126.751 111.798 127.124 111.798 127.311 111.985L131.416 114.97L130.857 109.932C130.857 109.745 130.857 109.372 131.043 109.186C131.23 108.999 131.416 108.999 131.603 108.999H135.149C135.335 108.999 135.522 109.186 135.708 109.372C135.895 109.559 135.895 109.745 135.895 109.932L135.149 115.157L139.254 112.171C139.441 111.985 139.627 111.985 140 111.985C140.187 111.985 140.374 112.171 140.56 112.358L142.24 115.53C142.426 115.717 142.426 115.903 142.24 116.277C142.24 116.463 142.053 116.65 141.866 116.837L137.201 118.889L142.053 120.569Z" fill="white"/>
<!-- Checkmark -->
<path class="animate-draw-stroke-repeat" d="M153.143 116.547 L163.143 126.547 L178.387 109.427" stroke="#FFFFFF" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Small star -->
<g class="animate-pulse-first" style="transform-origin: 163px 35px">
<path fill-rule="evenodd" clip-rule="evenodd" d="M156.422 28.0178C156.42 27.9051 156.458 27.7953 156.528 27.7071C156.598 27.6189 156.697 27.5577 156.807 27.534C157.762 27.3627 160.386 26.7462 161.281 25.8598C162.176 24.9735 162.784 22.3531 162.96 21.3854C162.981 21.2755 163.04 21.1766 163.127 21.1061C163.214 21.0356 163.323 20.9981 163.435 21.0001V28.0178H156.422Z" fill="#DABDFF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M163.4 21.0031C163.514 21.0035 163.624 21.0441 163.711 21.1177C163.798 21.1913 163.856 21.2933 163.876 21.4056C164.034 22.3518 164.625 24.9337 165.511 25.8885C166.397 26.8434 169.039 27.4 170.054 27.5756C170.166 27.5966 170.266 27.6562 170.338 27.7439C170.41 27.8315 170.449 27.9417 170.448 28.0551H163.4V21.0031Z" fill="#00B3F4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M170.448 28.0192C170.451 28.1347 170.411 28.2473 170.336 28.3351C170.261 28.4229 170.156 28.4797 170.041 28.4945C169.086 28.6529 166.41 29.2395 165.511 30.1301C164.612 31.0207 164.034 33.6583 163.875 34.6602C163.862 34.7772 163.806 34.8852 163.719 34.964C163.631 35.0429 163.518 35.0871 163.4 35.0884V28.0192H170.448Z" fill="#007BED"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M163.435 28.0192V35.0413C163.322 35.0369 163.214 34.9949 163.128 34.922C163.042 34.849 162.983 34.7493 162.96 34.6388C162.784 33.6754 162.168 31.0122 161.281 30.1173C160.395 29.2224 157.77 28.6486 156.82 28.4902C156.708 28.4719 156.607 28.4144 156.534 28.3281C156.461 28.2418 156.421 28.1323 156.422 28.0192H163.435Z" fill="#00B3F4"/>
</g>
<!-- Medium star -->
<g class="animate-pulse-second" style="transform-origin: 174px 63px">
<path fill-rule="evenodd" clip-rule="evenodd" d="M158.148 63.3406C158.144 63.1683 158.201 63.0002 158.31 62.8662C158.418 62.7321 158.57 62.6408 158.739 62.6084C160.182 62.3472 164.168 61.4052 165.53 60.065C166.892 58.7249 167.812 54.7386 168.065 53.2742C168.095 53.105 168.186 52.9523 168.319 52.8439C168.453 52.7355 168.621 52.6785 168.793 52.6833V63.3406H158.148Z" fill="#DABDFF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M168.746 52.683C168.918 52.6837 169.086 52.7456 169.217 52.8577C169.349 52.9697 169.437 53.1248 169.465 53.2953C169.709 54.7297 170.604 58.6518 171.953 60.1033C173.301 61.5548 177.309 62.3983 178.846 62.6723C179.015 62.7054 179.168 62.7968 179.276 62.9306C179.385 63.0644 179.444 63.2321 179.441 63.4045H168.737L168.746 52.683Z" fill="#00B3F4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M179.441 63.3393C179.447 63.5136 179.388 63.6841 179.275 63.8166C179.161 63.9491 179.002 64.034 178.829 64.0543C177.373 64.3027 173.314 65.1933 171.952 66.542C170.591 67.8908 169.709 71.8942 169.464 73.4185C169.44 73.5922 169.354 73.7513 169.222 73.8668C169.09 73.9823 168.921 74.0466 168.745 74.0479V63.3436L179.441 63.3393Z" fill="#007BED"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M168.801 63.3393V73.9965C168.628 73.9914 168.462 73.9281 168.33 73.817C168.198 73.7058 168.107 73.5534 168.073 73.3843C167.811 71.9242 166.869 67.8779 165.529 66.5335C164.189 65.189 160.199 64.3027 158.756 64.0586C158.587 64.0313 158.433 63.9448 158.321 63.8147C158.21 63.6845 158.148 63.5191 158.148 63.3479L168.801 63.3393Z" fill="#00B3F4"/>
</g>
<!-- Big star -->
<g class="animate-pulse-third" style="transform-origin: 141px 47.5px">
<path fill-rule="evenodd" clip-rule="evenodd" d="M122.998 47.5261C122.993 47.2294 123.092 46.9404 123.278 46.709C123.464 46.4776 123.724 46.3183 124.015 46.2588C126.507 45.7859 133.387 44.1877 135.737 41.8754C138.087 39.5632 139.676 32.6831 140.125 30.1533C140.18 29.8613 140.338 29.5986 140.569 29.4122C140.8 29.2259 141.091 29.1282 141.388 29.1367V47.5261H122.998Z" fill="#DABDFF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M141.289 29.1362C141.587 29.1359 141.874 29.2421 142.101 29.4354C142.327 29.6288 142.477 29.8966 142.523 30.1906C142.949 32.6684 144.49 39.435 146.821 41.9411C149.152 44.4473 156.047 45.8989 158.714 46.3481C159.007 46.4039 159.272 46.5616 159.46 46.7935C159.649 47.0254 159.75 47.3165 159.744 47.6154H141.303L141.289 29.1362Z" fill="#00B3F4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M159.744 47.5268C159.752 47.8278 159.649 48.1212 159.453 48.3501C159.257 48.579 158.984 48.7271 158.685 48.7657C156.179 49.1912 149.171 50.7233 146.821 53.0545C144.471 55.3857 142.948 62.2894 142.523 64.9232C142.482 65.222 142.334 65.496 142.108 65.6949C141.881 65.8938 141.59 66.0043 141.289 66.006V47.5268H159.744Z" fill="#007BED"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M141.388 47.5268V65.9162C141.091 65.9073 140.805 65.7983 140.578 65.6069C140.35 65.4155 140.194 65.1529 140.135 64.8617C139.685 62.3414 138.059 55.362 135.747 53.0403C133.434 50.7186 126.549 49.1912 124.053 48.7846C123.762 48.7347 123.498 48.5843 123.307 48.3595C123.116 48.1348 123.01 47.8501 123.008 47.5552L141.388 47.5268Z" fill="#00B3F4"/>
</g>
<defs>
<filter id="filter0_d_5194_14463" x="0" y="0" width="204" height="143" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-2"/>
<feGaussianBlur stdDeviation="16"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.564706 0 0 0 0 0.929412 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5194_14463"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5194_14463" result="shape"/>
</filter>
<filter id="filter1_d_5194_14463" x="79" y="18" width="172" height="125" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-2"/>
<feGaussianBlur stdDeviation="16"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.564706 0 0 0 0 0.929412 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5194_14463"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5194_14463" result="shape"/>
</filter>
</defs>
</svg>

После

Ширина:  |  Высота:  |  Размер: 14 KiB

Просмотреть файл

@ -0,0 +1,10 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5101_495)">
<path d="M12 0.5C10.4241 0.5 8.86371 0.810389 7.4078 1.41345C5.95189 2.0165 4.62902 2.90042 3.51472 4.01472C2.40042 5.12902 1.5165 6.45189 0.913446 7.9078C0.310389 9.36371 0 10.9241 0 12.5C0 14.0759 0.310389 15.6363 0.913446 17.0922C1.5165 18.5481 2.40042 19.871 3.51472 20.9853C4.62902 22.0996 5.95189 22.9835 7.4078 23.5866C8.86371 24.1896 10.4241 24.5 12 24.5C15.1826 24.5 18.2348 23.2357 20.4853 20.9853C22.7357 18.7348 24 15.6826 24 12.5C24 9.3174 22.7357 6.26516 20.4853 4.01472C18.2348 1.76428 15.1826 0.5 12 0.5ZM10.12 17.3L5.8928 13.0712C5.70545 12.8834 5.60037 12.6289 5.60067 12.3637C5.60097 12.0984 5.70663 11.8441 5.8944 11.6568C6.08217 11.4695 6.33668 11.3644 6.60193 11.3647C6.86718 11.365 7.12145 11.4706 7.3088 11.6584L10.6224 14.9736L16.6928 8.9016C16.8822 8.7218 17.1343 8.62306 17.3954 8.62645C17.6565 8.62984 17.906 8.73507 18.0907 8.91973C18.2753 9.10438 18.3806 9.35386 18.384 9.61498C18.3873 9.8761 18.2886 10.1282 18.1088 10.3176L11.1248 17.3H10.12Z" fill="#1CC5A0"/>
</g>
<defs>
<clipPath id="clip0_5101_495">
<rect width="24" height="24" fill="white" transform="translate(0 0.5)"/>
</clipPath>
</defs>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 408 B

После

Ширина:  |  Высота:  |  Размер: 408 B

Просмотреть файл

@ -15,6 +15,9 @@ import {
RecoveryCodesImage,
RecoveryKeyImage,
TwoFactorAuthImage,
CheckmarkCircleOutlineIcon,
CheckmarkCircleFullIcon,
PasswordSuccessImage,
} from '.';
import { withLocalization } from 'fxa-react/lib/storybooks';
@ -30,6 +33,9 @@ export default {
RecoveryCodesImage,
RecoveryKeyImage,
TwoFactorAuthImage,
CheckmarkCircleOutlineIcon,
CheckmarkCircleFullIcon,
PasswordSuccessImage,
},
decorators: [withLocalization],
} as Meta;
@ -51,3 +57,9 @@ export const Mail = () => <MailImage />;
export const RecoveryCodes = () => <RecoveryCodesImage />;
export const TwoFactorAuth = () => <TwoFactorAuthImage />;
export const CircleCheckOutline = () => <CheckmarkCircleOutlineIcon />;
export const CircleCheckFull = () => <CheckmarkCircleFullIcon />;
export const PasswordSuccess = () => <PasswordSuccessImage />;

Просмотреть файл

@ -13,7 +13,9 @@ import { ReactComponent as Key } from './graphic_recovery_key.min.svg';
import { ReactComponent as Password } from './graphic_password.min.svg';
import { ReactComponent as Lightbulb } from './graphic_recovery_key_hint.min.svg';
import { ReactComponent as EmailCode } from './graphic_email_code.svg';
import { ReactComponent as CircleCheckOutline } from './icon-circle-check-outline.svg';
import { ReactComponent as CircleCheckOutline } from './icon_checkmark_circle_outline.svg';
import { ReactComponent as CircleCheckFull } from './icon_checkmark_circle_full.svg';
import { ReactComponent as PasswordSuccess } from './graphic_password_success.min.svg';
import { FtlMsg } from 'fxa-react/lib/utils';
@ -155,10 +157,23 @@ export const EmailCodeImage = ({ className, ariaHidden }: ImageProps) => (
/>
);
export const CircleCheckOutlineImage = ({ className }: ImageProps) => (
export const PasswordSuccessImage = ({ className, ariaHidden }: ImageProps) => (
<PreparedImage
ariaLabel="Illustration to represent a successful password change."
ariaLabelFtlId="password-success-image-aria-label"
Image={PasswordSuccess}
{...{ className, ariaHidden }}
/>
);
export const CheckmarkCircleOutlineIcon = ({ className }: ImageProps) => (
<PreparedImage
Image={CircleCheckOutline}
ariaHidden={true}
{...{ className }}
/>
);
export const CheckmarkCircleFullIcon = ({ className }: ImageProps) => (
<PreparedImage Image={CircleCheckFull} ariaHidden={true} {...{ className }} />
);

Просмотреть файл

@ -9,6 +9,8 @@ export function useNavigateWithQuery() {
return (to: string, options?: NavigateOptions<{}>) => {
const location = window.location;
console.log('location', location);
debugger;
let path = to;
if (to.includes('?')) {

Просмотреть файл

@ -8,7 +8,7 @@ import InlineRecoveryKeySetupCreate from '../../components/InlineRecoveryKeySetu
import RecoveryKeySetupDownload from '../../components/RecoveryKeySetupDownload';
import AppLayout from '../../components/AppLayout';
import {
CircleCheckOutlineImage,
CheckmarkCircleOutlineIcon,
RecoveryKeyImage,
} from '../../components/images';
import { FtlMsg, hardNavigate } from 'fxa-react/lib/utils';
@ -63,7 +63,7 @@ export const InlineRecoveryKeySetup = ({
<>
<Banner type={BannerType.success} additionalClassNames="mt-0">
<p className="flex justify-center text-base">
<CircleCheckOutlineImage className="me-3" />
<CheckmarkCircleOutlineIcon className="me-3" />
<span>
<FtlMsg id="inline-recovery-key-setup-recovery-created">
Account recovery key created

Просмотреть файл

@ -7,11 +7,12 @@ import { RouteComponentProps, useLocation } from '@reach/router';
import { useValidatedQueryParams } from '../../../lib/hooks/useValidate';
import {
Integration,
isWebIntegration,
isOAuthIntegration,
useAccount,
useAlertBar,
useConfig,
useFtlMsgResolver,
useSensitiveDataClient,
} from '../../../models';
import { KeyStretchExperiment } from '../../../models/experiments';
@ -42,13 +43,14 @@ const CompleteResetPasswordContainer = ({
const alertBar = useAlertBar();
const config = useConfig();
const ftlMsgResolver = useFtlMsgResolver();
const navigate = useNavigateWithQuery();
const navigateWithQuery = useNavigateWithQuery();
const location = useLocation();
const sensitiveDataClient = useSensitiveDataClient();
const [errorMessage, setErrorMessage] = useState('');
if (!location.state) {
navigate('/reset_password', { replace: true });
navigateWithQuery('/reset_password', { replace: true });
return;
}
@ -73,29 +75,53 @@ const CompleteResetPasswordContainer = ({
const isResetWithoutRecoveryKey = !!(code && token);
const localizedSuccessMessage = ftlMsgResolver.getMsg(
'reset-password-complete-header',
'Your password has been reset'
);
const handleNavigationWithRecoveryKey = () => {
navigate('/reset_password_with_recovery_key_verified');
navigateWithQuery('/reset_password_with_recovery_key_verified');
};
const handleNavigationWithoutRecoveryKey = async (
accountResetData: AccountResetData
) => {
if (
accountResetData.verified &&
(isWebIntegration(integration) || integration.isSync())
) {
alertBar.success(
ftlMsgResolver.getMsg(
'reset-password-complete-header',
'Your password has been reset'
)
);
return navigate(SETTINGS_PATH, { replace: true });
if (accountResetData.verified) {
// For verified users with OAuth integration, navigate to confirmation page then to the relying party
if (isOAuthIntegration(integration) && !integration.isSync()) {
const sensitiveData = { ...accountResetData, email };
sensitiveDataClient.setData('accountResetData', sensitiveData);
return navigateWithQuery('/reset_password_verified', {
replace: true,
});
}
// For web integration and sync navigate to settings
// Sync users will see an account recovery key promotion banner in settings
// if they don't have one configured
alertBar.success(localizedSuccessMessage);
return navigateWithQuery(SETTINGS_PATH, { replace: true });
}
navigate('/reset_password_verified', {
replace: true,
});
// if the session is not verified (e.g., 2FA verification is required), navigate to the sign-in page
if (location.search && location.search.includes('email')) {
return navigateWithQuery('/signin', {
replace: true,
state: {
bannerSuccessMessage: localizedSuccessMessage,
},
});
} else {
// if user started directly from the reset password page without passing GO (index),
// the email needs to be added to query params to avoid redirecting to the index page
return navigateWithQuery(`/signin?email=${encodeURIComponent(email)}`, {
replace: true,
state: {
bannerSuccessMessage: localizedSuccessMessage,
},
});
}
};
const resetPasswordWithRecoveryKey = async (
@ -217,7 +243,7 @@ const CompleteResetPasswordContainer = ({
// handle the case where we don't have all data required
if (!(hasConfirmedRecoveryKey || isResetWithoutRecoveryKey)) {
navigate('/reset_password', { replace: true });
navigateWithQuery('/reset_password', { replace: true });
}
return (

Просмотреть файл

@ -0,0 +1,108 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { RouteComponentProps, useLocation } from '@reach/router';
import { useNavigateWithQuery } from '../../../lib/hooks/useNavigateWithQuery';
import ResetPasswordConfirmed from '.';
import { MozServices } from '../../../lib/types';
import {
Integration,
isOAuthIntegration,
useAuthClient,
useSensitiveDataClient,
} from '../../../models';
import { useFinishOAuthFlowHandler } from '../../../lib/oauth/hooks';
import OAuthDataError from '../../../components/OAuthDataError';
import { hardNavigate } from 'fxa-react/lib/utils';
import { AuthUiErrors } from '../../../lib/auth-errors/auth-errors';
import { AuthError } from '../../../lib/oauth';
import { useState } from 'react';
import GleanMetrics from '../../../lib/glean';
const ResetPasswordConfirmedContainer = ({
integration,
serviceName,
}: {
integration: Integration;
serviceName: MozServices;
} & RouteComponentProps) => {
const authClient = useAuthClient();
const { finishOAuthFlowHandler, oAuthDataError } = useFinishOAuthFlowHandler(
authClient,
integration
);
const location = useLocation();
const navigateWithQuery = useNavigateWithQuery();
const sensitiveDataClient = useSensitiveDataClient();
const [errorMessage, setErrorMessage] = useState('');
const { email, uid, sessionToken, keyFetchToken, unwrapBKey, verified } =
sensitiveDataClient.getData('accountResetData');
const handleOAuthRedirectError = (error: AuthError) => {
if (
error.errno === AuthUiErrors.TOTP_REQUIRED.errno ||
error.errno === AuthUiErrors.INSUFFICIENT_ACR_VALUES.errno
) {
navigateWithQuery(`/inline_totp_setup`, {
state: {
email,
uid,
sessionToken,
verified,
keyFetchToken,
unwrapBKey,
},
});
} else {
GleanMetrics.login.error({ event: { reason: error.message } });
setErrorMessage(error.message);
}
};
const getOauthRedirect = async () => {
const { error, redirect } = await finishOAuthFlowHandler(
uid,
sessionToken,
keyFetchToken,
unwrapBKey
);
return { redirect, error };
};
const continueWithVerifiedSession = async () => {
if (isOAuthIntegration(integration)) {
const { redirect, error } = await getOauthRedirect();
if (error) {
handleOAuthRedirectError(error);
return;
}
if (redirect) {
hardNavigate(redirect);
return;
}
}
navigateWithQuery(`/settings`);
};
if (oAuthDataError) {
return <OAuthDataError error={oAuthDataError} />;
}
if (!verified) {
hardNavigate(`/${location.search}`);
return;
}
return (
<ResetPasswordConfirmed
continueHandler={continueWithVerifiedSession}
{...{ errorMessage, serviceName }}
/>
);
};
export default ResetPasswordConfirmedContainer;

Просмотреть файл

@ -0,0 +1,5 @@
## ResetPasswordConfirmed
reset-password-complete-header = Your password has been reset
# $serviceName is a product name such as Monitor, Pocket, Relay
reset-password-confirmed-cta = Continue to { $serviceName }

Просмотреть файл

@ -6,9 +6,7 @@ import React from 'react';
import ResetPasswordConfirmed from '.';
import { MozServices } from '../../../lib/types';
import { Meta } from '@storybook/react';
import { renderStoryWithHistory } from '../../../lib/storybook-utils';
import { withLocalization } from 'fxa-react/lib/storybooks';
import { ResetPasswordConfirmedProps } from './interfaces';
export default {
title: 'Pages/ResetPassword/ResetPasswordConfirmed',
@ -16,49 +14,17 @@ export default {
decorators: [withLocalization],
} as Meta;
function renderStory(
incomingProps: Partial<ResetPasswordConfirmedProps> = {},
queryParams: string
) {
const defaultProps: ResetPasswordConfirmedProps = {
isSignedIn: true,
serviceName: MozServices.Default,
};
export const Default = () => (
<ResetPasswordConfirmed
continueHandler={() => {}}
serviceName={MozServices.Monitor}
/>
);
const props: ResetPasswordConfirmedProps = {
...defaultProps,
...incomingProps,
};
return renderStoryWithHistory(
<ResetPasswordConfirmed {...props} />,
'/reset_password_verified',
undefined,
queryParams
);
}
export const DefaultSignedIn = () => renderStory({ isSignedIn: true }, ``);
export const DefaultIsSync = () =>
renderStory(
{ isSignedIn: true, serviceName: MozServices.FirefoxSync },
'service=sync'
);
export const DefaultSignedOut = () =>
renderStory({ isSignedIn: false }, 'service=');
export const WithRelyingPartyNoContinueAction = () =>
renderStory({ isSignedIn: true }, `service=${MozServices.MozillaVPN}`);
export const WithRelyingPartyAndContinueAction = () =>
renderStory(
{
isSignedIn: true,
continueHandler: () => {
console.log('Arbitrary action');
},
},
`service=`
);
export const WithErrorMessage = () => (
<ResetPasswordConfirmed
errorMessage="An error occurred"
serviceName={MozServices.Default}
continueHandler={() => {}}
/>
);

Просмотреть файл

@ -3,71 +3,54 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
import ResetPasswordConfirmed, { viewName } from '.';
import { logViewEvent, usePageViewEvent } from '../../../lib/metrics';
import { REACT_ENTRYPOINT } from '../../../constants';
import ResetPasswordConfirmed from '.';
import { MozServices } from '../../../lib/types';
import userEvent from '@testing-library/user-event';
jest.mock('../../../lib/metrics', () => ({
logViewEvent: jest.fn(),
usePageViewEvent: jest.fn(),
}));
jest.mock('../../../lib/glean', () => ({
passwordReset: {
createNewSuccess: jest.fn(),
},
}));
const mockContinueHandler = jest.fn();
describe('ResetPasswordConfirmed', () => {
async function renderResetPasswordConfirmed(
props: {
isSignedIn: boolean;
serviceName: MozServices;
continueHandler?: Function;
} = {
isSignedIn: false,
serviceName: MozServices.Default,
}
) {
renderWithLocalizationProvider(<ResetPasswordConfirmed {...props} />);
await waitFor(() => new Promise((r) => setTimeout(r, 100)));
}
it('renders Ready component as expected when signed in', async () => {
await renderResetPasswordConfirmed({
isSignedIn: true,
serviceName: MozServices.Default,
});
const passwordResetConfirmation = screen.getByText(
'Your password has been reset'
it('renders as expected', async () => {
renderWithLocalizationProvider(
<ResetPasswordConfirmed
continueHandler={mockContinueHandler}
serviceName={MozServices.Monitor}
/>
);
const serviceAvailabilityConfirmation = screen.getByText(
'Youre now ready to use account settings'
expect(
screen.getByText('Your password has been reset')
).toBeInTheDocument();
const submitButton = screen.getByRole('button');
expect(submitButton).toHaveTextContent('Continue to Mozilla Monitor');
expect(submitButton).toHaveAttribute(
'data-glean-id',
'password_reset_success_continue_to_relying_party_submit'
);
expect(passwordResetConfirmation).toBeInTheDocument();
expect(serviceAvailabilityConfirmation).toBeInTheDocument();
});
it('emits the expected metrics on render', async () => {
await renderResetPasswordConfirmed();
expect(usePageViewEvent).toHaveBeenCalledWith(viewName, REACT_ENTRYPOINT);
it('renders an error message when one is provided', async () => {
const errorMessage = 'An error occurred';
renderWithLocalizationProvider(
<ResetPasswordConfirmed
continueHandler={mockContinueHandler}
serviceName={MozServices.Monitor}
{...{ errorMessage }}
/>
);
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
it('emits the expected metrics when a user clicks `Continue`', async () => {
await renderResetPasswordConfirmed({
isSignedIn: false,
serviceName: MozServices.Default,
continueHandler: () => {},
});
const passwordResetContinueButton = screen.getByText('Continue');
fireEvent.click(passwordResetContinueButton);
expect(logViewEvent).toHaveBeenCalledWith(
viewName,
`flow.${viewName}.continue`,
REACT_ENTRYPOINT
it('handles submit correctly', async () => {
const user = userEvent.setup();
renderWithLocalizationProvider(
<ResetPasswordConfirmed
continueHandler={mockContinueHandler}
serviceName={MozServices.Monitor}
/>
);
user.click(screen.getByRole('button'));
await waitFor(() => expect(mockContinueHandler).toHaveBeenCalledTimes(1));
});
});

Просмотреть файл

@ -4,20 +4,42 @@
import React from 'react';
import { RouteComponentProps } from '@reach/router';
import Ready from '../../../components/Ready';
import AppLayout from '../../../components/AppLayout';
import { ResetPasswordConfirmedProps } from './interfaces';
export const viewName = 'reset-password-confirmed';
import {
CheckmarkCircleFullIcon,
PasswordSuccessImage,
} from '../../../components/images';
import { FtlMsg } from 'fxa-react/lib/utils';
import Banner, { BannerType } from '../../../components/Banner';
const ResetPasswordConfirmed = ({
continueHandler,
isSignedIn,
errorMessage,
serviceName,
}: ResetPasswordConfirmedProps & RouteComponentProps) => {
return (
<AppLayout>
<Ready {...{ continueHandler, isSignedIn, viewName, serviceName }} />
{errorMessage && (
<Banner type={BannerType.error} additionalClassNames="mb-6">
{errorMessage}
</Banner>
)}
<div className="flex items-top justify-center">
<CheckmarkCircleFullIcon className="shrink-0 me-4" />
<FtlMsg id="reset-password-complete-header">
<h1 className="card-header -mt-1">Your password has been reset</h1>
</FtlMsg>
</div>
<PasswordSuccessImage className="mt-4 mb-8 mx-auto" />
<FtlMsg id="reset-password-confirmed-cta" vars={{ serviceName }}>
<button
className="cta-primary cta-xl w-full"
type="button"
data-glean-id="password_reset_success_continue_to_relying_party_submit"
onClick={() => continueHandler()}
>{`Continue to ${serviceName}`}</button>
</FtlMsg>
</AppLayout>
);
};

Просмотреть файл

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export type ResetPasswordConfirmedProps = {
continueHandler?: Function;
isSignedIn: boolean;
continueHandler: Function;
serviceName: string;
errorMessage?: string;
};

Просмотреть файл

@ -0,0 +1,16 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Integration, IntegrationType } from '../../../models';
export const mockResetPasswordOAuthIntegration = () => {
const mockIntegration = {
type: IntegrationType.OAuth,
getService: () => 'sync',
isSync: () => true,
wantsKeys: () => true,
} as Integration;
return mockIntegration;
};

Просмотреть файл

@ -143,6 +143,7 @@ const SigninContainer = ({
hasLinkedAccount: hasLinkedAccountFromLocationState,
hasPassword: hasPasswordFromLocationState,
localizedErrorMessage: localizedErrorFromLocationState,
bannerSuccessMessage,
} = location.state || {};
const [accountStatus, setAccountStatus] = useState({
@ -475,6 +476,7 @@ const SigninContainer = ({
avatarLoading,
localizedErrorFromLocationState,
finishOAuthFlowHandler,
bannerSuccessMessage,
}}
/>
);

Просмотреть файл

@ -58,6 +58,7 @@ const Signin = ({
avatarLoading,
localizedErrorFromLocationState,
finishOAuthFlowHandler,
bannerSuccessMessage,
}: SigninProps & RouteComponentProps) => {
usePageViewEvent(viewName, REACT_ENTRYPOINT);
const location = useLocation();
@ -312,6 +313,9 @@ const Signin = ({
return (
<AppLayout>
{bannerSuccessMessage && (
<Banner type={BannerType.success}>{bannerSuccessMessage}</Banner>
)}
{isPasswordNeededRef.current ? (
<CardHeader
headingText="Enter your password"

Просмотреть файл

@ -37,6 +37,7 @@ export interface LocationState {
hasLinkedAccount?: boolean;
hasPassword?: boolean;
localizedErrorMessage?: string;
bannerSuccessMessage?: string;
}
export interface SigninProps {
@ -53,6 +54,7 @@ export interface SigninProps {
avatarLoading: boolean;
localizedErrorFromLocationState?: string;
finishOAuthFlowHandler: FinishOAuthFlowHandler;
bannerSuccessMessage?: string;
}
export type BeginSigninHandler = (

Просмотреть файл

@ -90,6 +90,30 @@ config.theme.extend = {
'0%, 50%': { strokeDashoffset: 100, strokeDasharray: 100 },
'60%, 100%': { strokeDashoffset: 0 },
},
'draw-stroke-and-fade-out': {
'0%': {
strokeDasharray: 100,
strokeDashoffset: 100,
opacity: 0,
},
'10%': {
opacity: 1,
},
'50%': {
strokeDasharray: 100,
strokeDashoffset: 100,
},
'60%': {
strokeDashoffset: 0,
},
'80%': {
opacity: 1,
},
'100%': {
opacity: 0,
strokeDashoffset: 0,
},
},
'pulse-stroke': {
'0%, 100%': { 'stroke-dashoffset': 6, 'stroke-dasharray': 10 },
'50%': { 'stroke-dashoffset': 0, 'stroke-dasharray': 10 },
@ -134,6 +158,7 @@ config.theme.extend = {
'pulse-second': 'twinkle 3s infinite ease-in-out alternate',
'pulse-third': 'twinkle 3.5s infinite ease-in-out alternate',
'draw-stroke': 'draw-stroke 5s ease-in forwards',
'draw-stroke-repeat': 'draw-stroke-and-fade-out 5s ease-in infinite',
'wait-and-appear': 'wait-and-appear 2s ease-out 1',
'glide-right': 'glide-right 3s 1 ease-in-out',
'appear-first': 'appear-first 3s ease-in-out ',