From 9dd67ca801f620345c7ee2eacc52cbf191c6ec7c Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:07:53 +1200 Subject: [PATCH] Add success animation to security key and TOTP flows (#227) * Clear notices when moving through webauthn flow * Add success animation to end of register key flow * Reuse success animation for totp --- settings/src/components/success.js | 36 ++++++++ settings/src/components/success.scss | 29 +++++++ settings/src/components/totp.js | 83 ++++++++++++------- .../src/components/webauthn/register-key.js | 42 ++-------- settings/src/components/webauthn/webauthn.js | 23 ++++- settings/src/style.scss | 1 + 6 files changed, 146 insertions(+), 68 deletions(-) create mode 100644 settings/src/components/success.js create mode 100644 settings/src/components/success.scss diff --git a/settings/src/components/success.js b/settings/src/components/success.js new file mode 100644 index 00000000..37aae16e --- /dev/null +++ b/settings/src/components/success.js @@ -0,0 +1,36 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { Icon, check } from '@wordpress/icons'; + +/** + * Render the "Success" component. + * + * Shows a message and animation, then calls the `afterTimeout` callback + * + * @param props + * @param props.afterTimeout + * @param props.message + */ +export default function Success( { message, afterTimeout } ) { + const [ hasTimer, setHasTimer ] = useState( false ); + + if ( ! hasTimer ) { + // Time matches the length of the CSS animation property on .wporg-2fa__success + setTimeout( afterTimeout, 4000 ); + setHasTimer( true ); + } + + return ( + <> +

{ message }

+ +

+

+ +
+

+ + ); +} diff --git a/settings/src/components/success.scss b/settings/src/components/success.scss new file mode 100644 index 00000000..e5538472 --- /dev/null +++ b/settings/src/components/success.scss @@ -0,0 +1,29 @@ +@keyframes success { + 0% { + transform: scale(0); + } + 10% { + transform: scale(1.5); + } + 20% { + transform: scale(1); + } + 80% { + transform: scale(1); + } + 100% { + transform: scale(0); + } +} + +.wporg-2fa__success { + transform: scale(0); + background: #33F078; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + animation: success 4s ease-in-out; +} diff --git a/settings/src/components/totp.js b/settings/src/components/totp.js index 2b38125f..96e156f2 100644 --- a/settings/src/components/totp.js +++ b/settings/src/components/totp.js @@ -13,28 +13,49 @@ import ScreenLink from './screen-link'; import AutoTabbingInput from './auto-tabbing-input'; import { refreshRecord } from '../utilities/common'; import { GlobalContext } from '../script'; +import Success from './success'; export default function TOTP() { const { user: { totpEnabled }, + navigateToScreen, } = useContext( GlobalContext ); + const [ success, setSuccess ] = useState( false ); + + const afterTimeout = useCallback( () => { + setSuccess( false ); + navigateToScreen( 'backup-codes' ); + }, [ navigateToScreen ] ); + + if ( success ) { + return ( + + ); + } if ( totpEnabled ) { return ; } - return ; + return ; } /** * Setup the TOTP provider. + * + * @param props + * @param props.setSuccess */ -function Setup() { +function Setup( { setSuccess } ) { const { - navigateToScreen, - setGlobalNotice, user: { userRecord }, } = useContext( GlobalContext ); + const { + record: { id: userId }, + } = userRecord; const [ secretKey, setSecretKey ] = useState( '' ); const [ qrCodeUrl, setQrCodeUrl ] = useState( '' ); const [ error, setError ] = useState( '' ); @@ -46,7 +67,7 @@ function Setup() { // useEffect callbacks can't be async directly, because that'd return the promise as a "cleanup" function. const fetchSetupData = async () => { const response = await apiFetch( { - path: '/wporg-two-factor/1.0/totp-setup?user_id=' + userRecord.record.id, + path: '/wporg-two-factor/1.0/totp-setup?user_id=' + userId, } ); setSecretKey( response.secret_key ); @@ -54,33 +75,35 @@ function Setup() { }; fetchSetupData(); - }, [] ); + }, [ userId ] ); // Enable TOTP when button clicked. - const handleEnable = useCallback( async ( event ) => { - event.preventDefault(); - - const code = inputs.join( '' ); - - try { - await apiFetch( { - path: '/two-factor/1.0/totp/', - method: 'POST', - data: { - user_id: userRecord.record.id, - key: secretKey, - code, - enable_provider: true, - }, - } ); - - await refreshRecord( userRecord ); - navigateToScreen( 'backup-codes' ); - setGlobalNotice( 'Successfully enabled One Time Passwords.' ); // Must be After `clickScreenEvent` clears it. - } catch ( handleEnableError ) { - setError( handleEnableError.message ); - } - } ); + const handleEnable = useCallback( + async ( event ) => { + event.preventDefault(); + + const code = inputs.join( '' ); + + try { + await apiFetch( { + path: '/two-factor/1.0/totp/', + method: 'POST', + data: { + user_id: userId, + key: secretKey, + code, + enable_provider: true, + }, + } ); + + await refreshRecord( userRecord ); + setSuccess( true ); + } catch ( handleEnableError ) { + setError( handleEnableError.message ); + } + }, + [ inputs, secretKey, userId, userRecord, setSuccess ] + ); return ( ; + return ( + + ); } return registerCeremonyActive ? ( @@ -138,35 +144,3 @@ function WaitingForSecurityKey() { ); } - -/** - * Render the "Success" component. - * - * The user sees this once their security key has successfully been registered. - * - * @param props - * @param props.newKeyName - * @param props.afterTimeout - */ -function Success( { newKeyName, afterTimeout } ) { - const [ hasTimer, setHasTimer ] = useState( false ); - - if ( ! hasTimer ) { - // TODO need to sync this timing with the animation below - setTimeout( afterTimeout, 2000 ); - setHasTimer( true ); - } - - return ( - <> -

- Success! Your { newKeyName } is successfully registered. -

- - { /* TODO replace w/ custom animation */ } -

- -

- - ); -} diff --git a/settings/src/components/webauthn/webauthn.js b/settings/src/components/webauthn/webauthn.js index 4f6e2984..e17fb3c3 100644 --- a/settings/src/components/webauthn/webauthn.js +++ b/settings/src/components/webauthn/webauthn.js @@ -34,6 +34,18 @@ export default function WebAuthn() { const [ statusWaiting, setStatusWaiting ] = useState( false ); const [ confirmingDisable, setConfirmingDisable ] = useState( false ); + /** + * Clear any notices then move to the desired step in the flow + */ + const updateFlow = useCallback( + ( nextFlow ) => { + setGlobalNotice( '' ); + setStatusError( '' ); + setFlow( nextFlow ); + }, + [ setGlobalNotice ] + ); + /** * Enable the WebAuthn provider. */ @@ -71,8 +83,8 @@ export default function WebAuthn() { await toggleProvider(); } - setFlow( 'manage' ); - }, [ webAuthnEnabled, toggleProvider ] ); + updateFlow( 'manage' ); + }, [ webAuthnEnabled, toggleProvider, updateFlow ] ); /** * Display the modal to confirm disabling the WebAuthn provider. @@ -90,7 +102,10 @@ export default function WebAuthn() { if ( 'register' === flow ) { return ( - setFlow( 'manage' ) } /> + updateFlow( 'manage' ) } + /> ); } @@ -106,7 +121,7 @@ export default function WebAuthn() { { keys.length > 0 && }

- diff --git a/settings/src/style.scss b/settings/src/style.scss index 6dffa08d..969b4f42 100644 --- a/settings/src/style.scss +++ b/settings/src/style.scss @@ -91,3 +91,4 @@ $alert-blue: #72aee6; @import "components/screen-navigation"; @import "components/auto-tabbing-input"; @import "components/revalidate-modal"; +@import "components/success";