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 && }
-