diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1ed1928599..e783858018 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -363,7 +363,7 @@ workflows:
filters:
branches:
only:
- - reskin-profile-settings
+ - feature/dice-setup
# This is beta env for production soft releases
- "build-prod-beta":
context : org-global
diff --git a/__tests__/shared/components/Settings/Account/__snapshots__/index.jsx.snap b/__tests__/shared/components/Settings/Account/__snapshots__/index.jsx.snap
index 608ab744be..3b2db9879e 100644
--- a/__tests__/shared/components/Settings/Account/__snapshots__/index.jsx.snap
+++ b/__tests__/shared/components/Settings/Account/__snapshots__/index.jsx.snap
@@ -708,6 +708,702 @@ exports[`renders account setting page correctly 1`] = `
updatePassword={[Function]}
updateProfile={[Function]}
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/images/account/security/dicelogo.png b/src/assets/images/account/security/dicelogo.png
new file mode 100644
index 0000000000..723b63b2e6
Binary files /dev/null and b/src/assets/images/account/security/dicelogo.png differ
diff --git a/src/assets/images/account/security/dicelogobig.png b/src/assets/images/account/security/dicelogobig.png
new file mode 100644
index 0000000000..b18935081d
Binary files /dev/null and b/src/assets/images/account/security/dicelogobig.png differ
diff --git a/src/assets/images/account/security/dicelogosmall.png b/src/assets/images/account/security/dicelogosmall.png
new file mode 100644
index 0000000000..c2be4e4a80
Binary files /dev/null and b/src/assets/images/account/security/dicelogosmall.png differ
diff --git a/src/assets/images/account/security/google-play.png b/src/assets/images/account/security/google-play.png
new file mode 100644
index 0000000000..5311fbf031
Binary files /dev/null and b/src/assets/images/account/security/google-play.png differ
diff --git a/src/assets/images/account/security/green-close.svg b/src/assets/images/account/security/green-close.svg
new file mode 100644
index 0000000000..d3ff15c89d
--- /dev/null
+++ b/src/assets/images/account/security/green-close.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/assets/images/account/security/mfa.svg b/src/assets/images/account/security/mfa.svg
new file mode 100644
index 0000000000..33b5ab0cc7
--- /dev/null
+++ b/src/assets/images/account/security/mfa.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/images/account/security/unsuccessful.svg b/src/assets/images/account/security/unsuccessful.svg
new file mode 100644
index 0000000000..29ae859bd8
--- /dev/null
+++ b/src/assets/images/account/security/unsuccessful.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/shared/actions/mfa.js b/src/shared/actions/mfa.js
new file mode 100644
index 0000000000..6551e28ec9
--- /dev/null
+++ b/src/shared/actions/mfa.js
@@ -0,0 +1,90 @@
+import { createActions } from 'redux-actions';
+import { getService } from '../services/mfa';
+
+/**
+ * @static
+ * @desc Creates an action that get user 2fa settings
+ * @param {String} userId Operation User id.
+ * @param {String} tokenV3 v3 auth token.
+ * @return {Action}
+ */
+async function getUser2faDone(userId, tokenV3) {
+ return getService(tokenV3).getUser2fa(userId);
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of updating user 2fa settings.
+ * @return {Action}
+ */
+function updateUser2faInit() { }
+
+/**
+ * @static
+ * @desc Update user 2fa settings
+ * @param {Number} userId User id.
+ * @param {Boolean} mfaEnabled 2fa flag.
+ * @return {Action}
+ */
+async function updateUser2faDone(userId, mfaEnabled, tokenV3) {
+ return getService(tokenV3).updateUser2fa(userId, mfaEnabled);
+}
+
+/**
+ * @static
+ * @desc Update user dice settings
+ * @param {Number} userId User id.
+ * @param {Boolean} diceEnabled dice flag.
+ * @return {Action}
+ */
+async function updateUserDiceDone(userId, diceEnabled, tokenV3) {
+ return getService(tokenV3).updateUserDice(userId, diceEnabled);
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of getting new dice connection
+ * @return {Action}
+ */
+function getNewDiceConnectionInit() { }
+
+/**
+ * @static
+ * @desc Get new Dice connection
+ * @param {Number} userId User id.
+ * @return {Action}
+ */
+async function getNewDiceConnectionDone(userId, tokenV3) {
+ return getService(tokenV3).getNewDiceConnection(userId);
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of getting dice connection
+ * @return {Action}
+ */
+function getDiceConnectionInit() { }
+
+/**
+ * @static
+ * @desc Get Dice connection
+ * @param {Number} userId User id.
+ * @param {Number} connectionId User id.
+ * @return {Action}
+ */
+async function getDiceConnectionDone(userId, connectionId, tokenV3) {
+ return getService(tokenV3).getDiceConnection(userId, connectionId);
+}
+
+export default createActions({
+ USERMFA: {
+ GET_USER2FA_DONE: getUser2faDone,
+ UPDATE_USER2FA_INIT: updateUser2faInit,
+ UPDATE_USER2FA_DONE: updateUser2faDone,
+ UPDATE_USER_DICE_DONE: updateUserDiceDone,
+ GET_NEW_DICE_CONNECTION_INIT: getNewDiceConnectionInit,
+ GET_NEW_DICE_CONNECTION_DONE: getNewDiceConnectionDone,
+ GET_DICE_CONNECTION_INIT: getDiceConnectionInit,
+ GET_DICE_CONNECTION_DONE: getDiceConnectionDone,
+ },
+});
diff --git a/src/shared/components/Settings/Account/MyAccount/styles.scss b/src/shared/components/Settings/Account/MyAccount/styles.scss
index cc6afae006..ba61547185 100644
--- a/src/shared/components/Settings/Account/MyAccount/styles.scss
+++ b/src/shared/components/Settings/Account/MyAccount/styles.scss
@@ -349,7 +349,7 @@
padding: $pad-xxxxl;
background-color: $color-tc-white;
border-radius: 4px;
- margin: $margin-sm 0 0;
+ margin: $margin-sm 0 $margin-xxxxl 0;
.password-form {
display: flex;
diff --git a/src/shared/components/Settings/Account/Security/Modal/index.jsx b/src/shared/components/Settings/Account/Security/Modal/index.jsx
new file mode 100644
index 0000000000..3e33c982e9
--- /dev/null
+++ b/src/shared/components/Settings/Account/Security/Modal/index.jsx
@@ -0,0 +1,80 @@
+import React, { useEffect } from 'react';
+import PT from 'prop-types';
+import DiceImage from 'assets/images/account/security/dicelogosmall.png';
+import CloseButton from 'assets/images/account/security/green-close.svg';
+
+import './style.scss';
+
+export default function DiceModal({
+ children, onCancel, leftButtonName, leftButtonDisabled, leftButtonClick,
+ rightButtonName, rightButtonDisabled, rightButtonClick, rightButtonHide,
+}) {
+ useEffect(() => {
+ document.body.style.overflow = 'hidden';
+ return () => { document.body.style.overflow = 'unset'; };
+ }, []);
+
+ return (
+ (
+
+ event.stopPropagation()}
+ >
+
+
+
+
+
DICE ID authenticator setup
+
+
+
+
+ {children}
+
+
+
+
+ {leftButtonName}
+
+ {!rightButtonHide && (
+
+ {rightButtonName}
+
+ )}
+
+
+
+
+ )
+ );
+}
+DiceModal.defaultProps = {
+ children: null,
+ leftButtonDisabled: false,
+ rightButtonDisabled: false,
+ rightButtonHide: false,
+};
+DiceModal.propTypes = {
+ children: PT.node,
+ onCancel: PT.func.isRequired,
+ leftButtonName: PT.string.isRequired,
+ leftButtonDisabled: PT.bool,
+ leftButtonClick: PT.func.isRequired,
+ rightButtonName: PT.string.isRequired,
+ rightButtonDisabled: PT.bool,
+ rightButtonClick: PT.func.isRequired,
+ rightButtonHide: PT.bool,
+};
diff --git a/src/shared/components/Settings/Account/Security/Modal/style.scss b/src/shared/components/Settings/Account/Security/Modal/style.scss
new file mode 100644
index 0000000000..1f972a52ec
--- /dev/null
+++ b/src/shared/components/Settings/Account/Security/Modal/style.scss
@@ -0,0 +1,162 @@
+@import "../../../style";
+@import "~styles/mixins";
+
+.overlay {
+ background: #0c0c0c;
+ border: none;
+ height: 100%;
+ left: 0;
+ opacity: 0.85;
+ outline: none;
+ position: fixed;
+ top: 0;
+ width: 100%;
+ z-index: 950;
+}
+
+.container {
+ background: #fff;
+ position: fixed;
+ max-width: 1000px;
+ height: 752px;
+ max-height: 99%;
+ top: 50%;
+ left: 50%;
+ padding: 32px;
+ box-shadow: 0 0 1px 5px rgba(0, 0, 0, 0.2);
+ border-radius: 8px;
+ overflow: auto;
+ transform: translate(-50%, -50%);
+ z-index: 951;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+
+ .header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ margin-bottom: 24px;
+
+ .icon-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 150px;
+ height: 40px;
+ background: #fff;
+ border-radius: 4px;
+ margin-right: $margin-md;
+
+ img {
+ display: block;
+ }
+ }
+
+ .title {
+ @include barlow-semi-bold;
+
+ flex: 1;
+ font-size: 22px;
+ line-height: 26px;
+ color: $tco-black;
+ text-transform: uppercase;
+ }
+
+ .close-button {
+ cursor: pointer;
+ height: 15px;
+ width: 15px;
+ }
+ }
+
+ .body {
+ flex: 1;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 24px;
+ }
+
+ .divider {
+ width: 100%;
+ min-height: 2px;
+ background-color: #e9e9e9;
+ border-radius: 1px;
+ margin-bottom: 24px;
+ }
+
+ .footer {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+
+ .left-button {
+ @include roboto-bold;
+
+ text-align: center;
+ margin: 0;
+ height: 48px;
+ font-size: 16px;
+ line-height: 21px;
+ letter-spacing: 0.008em;
+ text-transform: uppercase;
+ color: $color-turq-160;
+ padding: 12px 24px;
+ background: $tc-white;
+ border: 2px solid $color-turq-160;
+ border-radius: 50px;
+ cursor: pointer;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:focus-visible {
+ outline: none;
+ }
+
+ &.disabled {
+ background: #f4f4f4;
+ color: #767676;
+ border: none;
+ line-height: 24px;
+ pointer-events: none;
+ }
+ }
+
+ .right-button {
+ @include roboto-bold;
+
+ text-align: center;
+ margin: 0;
+ height: 48px;
+ font-size: 16px;
+ line-height: 24px;
+ letter-spacing: 0.008em;
+ text-transform: uppercase;
+ color: $tc-white;
+ padding: 12px 24px;
+ background: $color-turq-160;
+ border-radius: 50px;
+ cursor: pointer;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:focus-visible {
+ outline: none;
+ }
+
+ &.disabled {
+ background: #f4f4f4;
+ color: #767676;
+ pointer-events: none;
+ }
+ }
+ }
+}
diff --git a/src/shared/components/Settings/Account/Security/VerificationListener/index.jsx b/src/shared/components/Settings/Account/Security/VerificationListener/index.jsx
new file mode 100644
index 0000000000..26e0ee9e54
--- /dev/null
+++ b/src/shared/components/Settings/Account/Security/VerificationListener/index.jsx
@@ -0,0 +1,32 @@
+import { useEffect, useCallback } from 'react';
+import PT from 'prop-types';
+
+export default function VerificationListener({
+ event, callback, origin, type, onProcessing, startType,
+}) {
+ const messageHandler = useCallback((e) => {
+ if (e.origin === origin && e.data && e.data.type) {
+ if (e.data.type === startType) {
+ onProcessing();
+ } else if (e.data.type === type) {
+ callback(e.data);
+ }
+ }
+ }, [origin, type, startType]);
+
+ useEffect(() => {
+ window.addEventListener(event, messageHandler);
+ return () => window.removeEventListener(event, messageHandler);
+ }, [event, messageHandler]);
+
+ return false;
+}
+
+VerificationListener.propTypes = {
+ event: PT.string.isRequired,
+ callback: PT.func.isRequired,
+ origin: PT.string.isRequired,
+ type: PT.string.isRequired,
+ onProcessing: PT.func.isRequired,
+ startType: PT.string.isRequired,
+};
diff --git a/src/shared/components/Settings/Account/Security/index.jsx b/src/shared/components/Settings/Account/Security/index.jsx
new file mode 100644
index 0000000000..cf24894066
--- /dev/null
+++ b/src/shared/components/Settings/Account/Security/index.jsx
@@ -0,0 +1,413 @@
+import React, { useState, useEffect, useRef } from 'react';
+import PT from 'prop-types';
+import _ from 'lodash';
+import { config } from 'topcoder-react-utils';
+import QRCode from 'react-qr-code';
+import { SettingBannerV2 as Collapse } from 'components/Settings/SettingsBanner';
+import MfaImage from 'assets/images/account/security/mfa.svg';
+import DiceLogo from 'assets/images/account/security/dicelogo.png';
+import DiceLogoBig from 'assets/images/account/security/dicelogobig.png';
+import GooglePlay from 'assets/images/account/security/google-play.png';
+import AppleStore from 'assets/images/account/security/apple-store.svg';
+import UnsuccessfulIcon from 'assets/images/account/security/unsuccessful.svg';
+import Modal from './Modal';
+import VerificationListener from './VerificationListener';
+
+
+import './styles.scss';
+
+export default function Security({
+ usermfa, updateUser2fa, updateUserDice, getNewDiceConnection,
+ getDiceConnection, tokenV3, handle, emailAddress,
+}) {
+ const [setupStep, setSetupStep] = useState(-1);
+ const [isConnVerifyRunning, setIsConnVerifyRunning] = useState(false);
+ const [connVerifyCounter, setConnVerifyCounter] = useState(0);
+ const [isVerificationProcessing, setIsVerificationProcessing] = useState(false);
+ const diceVerifyUrl = config.DICE_VERIFY_URL;
+ const useInterval = (callback, delay) => {
+ const savedCallback = useRef();
+
+ useEffect(() => {
+ savedCallback.current = callback;
+ }, [callback]);
+
+ // eslint-disable-next-line consistent-return
+ useEffect(() => {
+ function tick() {
+ savedCallback.current();
+ }
+ if (delay !== null) {
+ const id = setInterval(tick, delay);
+ return () => clearInterval(id);
+ }
+ setConnVerifyCounter(0);
+ }, [delay]);
+ };
+
+ const getMfaOption = () => {
+ const mfaEnabled = _.get(usermfa, 'user2fa.mfaEnabled');
+ if (mfaEnabled) return true;
+ return false;
+ };
+
+ const getDiceOption = () => {
+ const diceEnabled = _.get(usermfa, 'user2fa.diceEnabled');
+ if (diceEnabled) return true;
+ return false;
+ };
+
+ const mfaChecked = getMfaOption();
+ const diceChecked = getDiceOption();
+ const userId = _.get(usermfa, 'user2fa.userId');
+ const diceConnection = _.get(usermfa, 'diceConnection');
+
+ const onUpdateMfaOption = () => {
+ if (usermfa.updatingUser2fa) {
+ return;
+ }
+ updateUser2fa(userId, !mfaChecked, tokenV3);
+ };
+
+ const goToConnection = () => {
+ if (mfaChecked && !usermfa.gettingNewDiceConnection) {
+ getNewDiceConnection(userId, tokenV3);
+ }
+ setSetupStep(1);
+ setIsConnVerifyRunning(true);
+ };
+
+ const getConnectionAccepted = () => {
+ if (diceConnection.accepted) return true;
+ return false;
+ };
+
+ const openSetup = () => {
+ setSetupStep(0);
+ };
+
+ const closeSetup = () => {
+ setSetupStep(-1);
+ setIsVerificationProcessing(false);
+ };
+
+ const verifyConnection = () => {
+ if (getConnectionAccepted() || usermfa.diceConnectionError) {
+ setIsConnVerifyRunning(false);
+ } else if (!usermfa.gettingDiceConnection && diceConnection.id) {
+ if (connVerifyCounter >= 36) {
+ closeSetup();
+ } else {
+ getDiceConnection(userId, diceConnection.id, tokenV3);
+ setConnVerifyCounter(connVerifyCounter + 1);
+ }
+ }
+ };
+
+ const goToVerification = () => {
+ if (!getConnectionAccepted()) {
+ return;
+ }
+ setSetupStep(2);
+ };
+
+ const verificationCallback = (data) => {
+ if (data.success) {
+ const userEmail = _.get(data, 'user.profile.Email');
+ if (!_.isUndefined(userEmail) && _.lowerCase(userEmail) === _.lowerCase(emailAddress)) {
+ updateUserDice(userId, true, tokenV3);
+ setSetupStep(3);
+ } else {
+ setSetupStep(4);
+ }
+ } else {
+ setSetupStep(4);
+ }
+ };
+
+ const onStartProcessing = () => {
+ setIsVerificationProcessing(true);
+ };
+
+ useInterval(verifyConnection, setupStep === 1 && isConnVerifyRunning ? 5000 : null);
+
+ const getVerificationStepTitle = () => {
+ if (isVerificationProcessing) {
+ return 'Processing...';
+ }
+ return 'STEP 3 OF 3';
+ };
+
+ const getVerificationStepText = () => {
+ if (isVerificationProcessing) {
+ return 'Please wait while your credentials are validated.';
+ }
+ return 'Scan the following DICE ID QR Code in your DICE ID mobile application to confirm your credential.';
+ };
+
+ const setupStepNodes = [
+
+
+
+ STEP 1 OF 3
+
+
+ First, please download the DICE ID App from the
+ Google Play Store or the iOS App Store.
+
+
+
+
+
+
+
+
+
+ After you have downloaded and installed the mobile app,
+ make sure to complete the configuration process.
+ When ready, click next below.
+
+
+ ,
+
+
+
+ STEP 2 OF 3
+
+
+ Scan the following DICE ID QR Code in your DICE ID mobile application.
+
+
+ {diceConnection.connection
+ ?
+ : 'Loading'}
+
+
+ Once the connection is established, the service will offer you a Verifiable Credential.
+ Press the ACCEPT button in your DICE ID App.
+ If you DECLINE the invitation, please try again after 5 minutes.
+
+
+ ,
+
{}}
+ rightButtonHide
+ >
+
+
+ {getVerificationStepTitle()}
+
+
+ {getVerificationStepText()}
+
+
+
+ {isVerificationProcessing && (
+
+ Powered by DICE ID
+
+ )}
+
+ ,
+
+
+
+ Setup completed!
+
+
+ Hello {handle},
+ Your credentials have been verified and you are all set
+ for MFA using your decentralized identity (DICE ID).
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+ Unsuccessful Verification
+
+
+
+ Hello {handle},
+ Your credentials could not be verified,
+ you won't be able to connect to MFA using your decentralized identity (DICE ID).
+
+
+
+
+
+ Please try again your process after few minutes.
+ Please click Finish bellow.
+
+
+ ,
+ ];
+ return (
+
+ {setupStep >= 0 && (
+ setupStepNodes[setupStep]
+ )}
+
+
+
+ Security
+
+
+
+
+
+ Multi Factor Authentication (MFA) Status
+
+
+ Status of MFA for your Topcoder account.
+ If enabled, MFA will be enforced during the Topcoder login process.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DICE ID Authenticator App
+
+
+ DICE ID authentication application
+
+
+ {diceChecked ? 'DICE ID Authenticator is enabled.' : 'Please set up DICE ID Authenticator from your desktop device'}
+
+
+ {diceChecked
+ ? (
+
+ { }}
+ className="onoffswitch-checkbox"
+ disabled
+ />
+
+
+
+
+
+
+ )
+ : (
+
+ Setup DICE ID Authentication
+
+ )
+ }
+
+
+
+
+ );
+}
+
+Security.propTypes = {
+ usermfa: PT.shape().isRequired,
+ updateUser2fa: PT.func.isRequired,
+ updateUserDice: PT.func.isRequired,
+ getNewDiceConnection: PT.func.isRequired,
+ getDiceConnection: PT.func.isRequired,
+ tokenV3: PT.string.isRequired,
+ handle: PT.string.isRequired,
+ emailAddress: PT.string.isRequired,
+};
diff --git a/src/shared/components/Settings/Account/Security/styles.scss b/src/shared/components/Settings/Account/Security/styles.scss
new file mode 100644
index 0000000000..cc4fc155b4
--- /dev/null
+++ b/src/shared/components/Settings/Account/Security/styles.scss
@@ -0,0 +1,282 @@
+@import "../../style";
+@import "~styles/mixins";
+
+.security-container {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding: $pad-xxxxl;
+ background-color: $color-tc-white;
+ border-radius: 4px;
+
+ @media (max-width: #{$screen-md - 1px}) {
+ padding: 26px 16px;
+ }
+
+ .security-title {
+ @include barlow-semi-bold;
+
+ font-size: 20px;
+ line-height: 22px;
+ color: #2a2a2a;
+ text-transform: uppercase;
+ width: 100%;
+ }
+
+ .factor-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ padding: $pad-lg;
+ border: 1px solid $color-black-20;
+ border-radius: 8px;
+ width: 100%;
+ margin-top: 32px;
+
+ @media (max-width: #{$screen-md - 1px}) {
+ flex-direction: column;
+ align-items: flex-start;
+ margin-top: 24px;
+ }
+
+ .first-line {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ width: auto;
+
+ @media (max-width: #{$screen-md - 1px}) {
+ width: 100%;
+ }
+ }
+
+ .icon-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 74px;
+ width: 74px;
+ height: 74px;
+ background: $color-black-5;
+ border-radius: 4px;
+ margin-right: 16px;
+
+ @media (max-width: #{$screen-md - 1px}) {
+ align-self: flex-start;
+ margin-bottom: 8px;
+ }
+
+ img {
+ display: block;
+ }
+ }
+
+ .info {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+
+ .info-first-line {
+ @include roboto-regular;
+
+ font-size: 16px;
+ line-height: 20px;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+ text-transform: capitalize;
+ color: $tco-black;
+
+ @media (max-width: #{$screen-md - 1px}) {
+ margin-bottom: 8px;
+ }
+ }
+
+ .info-second-line {
+ @include roboto-regular;
+
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 24px;
+ color: $color-black-60;
+
+ &.dice-info-mobile {
+ display: none;
+ }
+
+ @media (max-width: #{$screen-md - 1px}) {
+ padding-right: 0;
+
+ &.dice-info {
+ display: none;
+ }
+
+ &.dice-info-mobile {
+ display: block;
+ }
+ }
+ }
+ }
+
+ .mfa-switch {
+ display: unset;
+
+ @media (max-width: #{$screen-md - 1px}) {
+ display: none;
+ }
+ }
+
+ .mfa-switch-mobile {
+ display: none;
+
+ @media (max-width: #{$screen-md - 1px}) {
+ display: unset;
+ align-self: flex-start;
+ margin-top: 10px;
+ }
+ }
+
+ .dice-switch {
+ @media (max-width: #{$screen-md - 1px}) {
+ display: none;
+ }
+ }
+
+ .disabled-toggle {
+ cursor: not-allowed;
+ background-color: #7fb5a6 !important;
+ }
+
+ .button {
+ @include roboto-bold;
+
+ text-align: center;
+ margin: 0;
+ height: 48px;
+ font-size: 16px;
+ line-height: 21px;
+ letter-spacing: 0.008em;
+ text-transform: uppercase;
+ color: $color-turq-160;
+ padding: 12px 24px;
+ background: $tc-white;
+ border: 2px solid $color-turq-160;
+ border-radius: 50px;
+ cursor: pointer;
+
+ &.disabled {
+ background: #f4f4f4;
+ color: #767676;
+ border: none;
+ line-height: 24px;
+ pointer-events: none;
+ }
+
+ @media (max-width: #{$screen-md - 1px}) {
+ display: none;
+ }
+ }
+ }
+}
+
+.step-body {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+
+ .step-title-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ .icon-unsuccessful {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 32px;
+ width: 32px;
+ height: 32px;
+ margin-bottom: 8px;
+
+ svg {
+ display: block;
+ }
+ }
+ }
+
+ .step-title {
+ @include barlow-condensed-medium;
+
+ font-size: 26px;
+ line-height: 28px;
+ color: $tco-black;
+ text-transform: uppercase;
+ margin-bottom: 16px;
+
+ &.error {
+ color: #ef476f;
+ }
+
+ &.no-margin {
+ margin-bottom: 0;
+ }
+ }
+
+ .step-content {
+ @include roboto-regular;
+
+ font-size: 20px;
+ line-height: 26px;
+ color: $tco-black;
+ margin-bottom: 16px;
+
+ &.no-margin {
+ margin-bottom: 0;
+ }
+ }
+
+ .app-store {
+ display: flex;
+ flex-direction: row;
+ padding: 16px 0;
+ align-items: flex-start;
+ width: 100%;
+ justify-content: center;
+ margin-bottom: 16px;
+
+ .market {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &.mmr {
+ margin-right: 100px;
+ }
+
+ .market-link {
+ margin-bottom: 32px;
+ }
+ }
+ }
+
+ .body-logo {
+ display: flex;
+ flex-direction: row;
+ padding: 57px 0;
+ align-items: flex-start;
+ gap: 100px;
+ width: 100%;
+ justify-content: center;
+ margin-bottom: 16px;
+ }
+}
+
+.step-footer {
+ @include roboto-regular;
+
+ font-size: 12px;
+ line-height: 18px;
+ text-align: center;
+ color: #767676;
+ margin-top: auto;
+}
diff --git a/src/shared/components/Settings/Account/index.jsx b/src/shared/components/Settings/Account/index.jsx
index e772b96a9f..01cefef021 100644
--- a/src/shared/components/Settings/Account/index.jsx
+++ b/src/shared/components/Settings/Account/index.jsx
@@ -3,6 +3,7 @@ import React from 'react';
import PT from 'prop-types';
import { PrimaryButton } from 'topcoder-react-ui-kit';
import MyAccount from './MyAccount';
+import Security from './Security';
import ErrorWrapper from '../ErrorWrapper';
import styles from './styles.scss';
@@ -78,6 +79,9 @@ export default class Account extends React.Component {
{...this.props}
ref={this.myAccountRef}
/>
+