From 7884e95bd336c85b7e60d14c0598630bfaeb859a Mon Sep 17 00:00:00 2001 From: rhahao <26148770+rhahao@users.noreply.github.com> Date: Sun, 10 Nov 2024 12:39:38 +0300 Subject: [PATCH 1/2] feat(features): use stepper for congregation creation workflow --- converter/svg/convert.js | 7 +- .../svg/sources/name=congregation-access.svg | 9 + src/RootWrap.tsx | 3 + .../icons/IconCongregationAccess.tsx | 56 ++++++ src/components/icons/index.ts | 3 +- .../account_chooser/useAccountChooser.tsx | 12 +- .../criteria/index.tsx | 0 .../congregation_access_code/index.tsx | 177 +++++++++++++++++ .../useCongregationAccessCode.tsx | 101 ++++++++++ .../index.tsx | 20 +- .../useCongregationDetails.tsx} | 14 +- .../criteria/index.tsx | 0 .../congregation_master_key/index.tsx | 179 ++++++++++++++++++ .../useCongregationMasterKey.tsx | 105 ++++++++++ .../vip/congregation_create/index.tsx | 71 +++++-- .../useCongregationCreate.tsx | 37 +++- .../congregation_access_code/index.tsx | 110 +---------- .../useCongregationAccessCode.tsx | 79 +------- .../congregation_master_key/index.tsx | 111 +---------- .../useCongregationMasterKey.tsx | 79 +------- .../vip/oauth/button_base/useButtonBase.tsx | 10 +- src/features/app_start/vip/startup/index.tsx | 12 +- .../app_start/vip/startup/useStartup.tsx | 114 +++++++++-- .../app_start/vip/terms_use/useTermsUse.tsx | 2 +- .../index.tsx | 15 +- .../useUserAccountCreated.tsx | 19 ++ .../app_start/vip/vip_info_tip/index.tsx | 52 ++--- .../language_switcher/useLanguage.tsx | 10 +- src/hooks/useUserAutoLogin.tsx | 14 +- src/locales/en/onboarding.json | 5 +- src/services/i18n/index.ts | 2 +- src/states/app.ts | 12 +- 32 files changed, 967 insertions(+), 473 deletions(-) create mode 100644 converter/svg/sources/name=congregation-access.svg create mode 100644 src/components/icons/IconCongregationAccess.tsx rename src/features/app_start/vip/{congregation_encryption => congregation_create}/congregation_access_code/criteria/index.tsx (100%) create mode 100644 src/features/app_start/vip/congregation_create/congregation_access_code/index.tsx create mode 100644 src/features/app_start/vip/congregation_create/congregation_access_code/useCongregationAccessCode.tsx rename src/features/app_start/vip/congregation_create/{congregation_info => congregation_details}/index.tsx (89%) rename src/features/app_start/vip/congregation_create/{congregation_info/useCongregationInfo.tsx => congregation_details/useCongregationDetails.tsx} (94%) rename src/features/app_start/vip/{congregation_encryption => congregation_create}/congregation_master_key/criteria/index.tsx (100%) create mode 100644 src/features/app_start/vip/congregation_create/congregation_master_key/index.tsx create mode 100644 src/features/app_start/vip/congregation_create/congregation_master_key/useCongregationMasterKey.tsx rename src/features/app_start/vip/{congregation_create/account_created => user_account_created}/index.tsx (81%) create mode 100644 src/features/app_start/vip/user_account_created/useUserAccountCreated.tsx diff --git a/converter/svg/convert.js b/converter/svg/convert.js index 23acba71f2..f1f20bcedb 100644 --- a/converter/svg/convert.js +++ b/converter/svg/convert.js @@ -63,9 +63,12 @@ for await (const svgFile of svgFiles) { ); if (componentName === 'IconLoading') { - data.replace(' animation,', ' animation: rotate 2s linear infinite,'); + data = data.replace( + ' animation,', + ' animation: "rotate 2s linear infinite",' + ); } else { - data.replace(' animation,', ''); + data = data.replace(' animation,', ''); } data = data.replace('${iconName}', originalName); diff --git a/converter/svg/sources/name=congregation-access.svg b/converter/svg/sources/name=congregation-access.svg new file mode 100644 index 0000000000..01ef3ab154 --- /dev/null +++ b/converter/svg/sources/name=congregation-access.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/RootWrap.tsx b/src/RootWrap.tsx index fcbdaab2ab..2d80078baa 100644 --- a/src/RootWrap.tsx +++ b/src/RootWrap.tsx @@ -54,6 +54,9 @@ const theme = createTheme({ span: { fontFamily: `${font} !important`, }, + text: { + fontFamily: `${font} !important`, + }, }, }, }, diff --git a/src/components/icons/IconCongregationAccess.tsx b/src/components/icons/IconCongregationAccess.tsx new file mode 100644 index 0000000000..afad858544 --- /dev/null +++ b/src/components/icons/IconCongregationAccess.tsx @@ -0,0 +1,56 @@ +import { SvgIcon, SxProps, Theme } from '@mui/material'; + +type IconProps = { + color?: string; + width?: number; + height?: number; + sx?: SxProps; + className?: string; +}; + +const IconCongregationAccess = ({ + color = '#222222', + width = 24, + height = 24, + sx = {}, + className, +}: IconProps) => { + return ( + + + + + + + + + + + + ); +}; + +export default IconCongregationAccess; diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts index e1445f9929..3e72214d20 100644 --- a/src/components/icons/index.ts +++ b/src/components/icons/index.ts @@ -59,6 +59,7 @@ export { default as IconCompassOn } from './IconCompassOn'; export { default as IconComputerVideo } from './IconComputerVideo'; export { default as IconComputer } from './IconComputer'; export { default as IconConference } from './IconConference'; +export { default as IconCongregationAccess } from './IconCongregationAccess'; export { default as IconCongregation } from './IconCongregation'; export { default as IconContactUs } from './IconContactUs'; export { default as IconContact } from './IconContact'; @@ -186,8 +187,8 @@ export { default as IconPause } from './IconPause'; export { default as IconPermissionsPending } from './IconPermissionsPending'; export { default as IconPersonSearch } from './IconPersonSearch'; export { default as IconPerson } from './IconPerson'; -export { default as IconPersonPlaceholder } from './IconPersonPlaceholder'; export { default as IconPersonalDay } from './IconPersonalDay'; +export { default as IconPersonPlaceholder } from './IconPersonPlaceholder'; export { default as IconPhone } from './IconPhone'; export { default as IconPinCode } from './IconPinCode'; export { default as IconPin } from './IconPin'; diff --git a/src/features/app_start/shared/account_chooser/useAccountChooser.tsx b/src/features/app_start/shared/account_chooser/useAccountChooser.tsx index 57bf36e23e..696e474a4f 100644 --- a/src/features/app_start/shared/account_chooser/useAccountChooser.tsx +++ b/src/features/app_start/shared/account_chooser/useAccountChooser.tsx @@ -1,15 +1,21 @@ -import { setIsAccountChoose } from '@services/recoil/app'; import { dbAppSettingsUpdate } from '@services/dexie/settings'; +import { useSetRecoilState } from 'recoil'; +import { isAccountChooseState, isUserAccountCreatedState } from '@states/app'; const useAccountChooser = () => { + const setIsAccountChoose = useSetRecoilState(isAccountChooseState); + const setIsUserAccountCreated = useSetRecoilState(isUserAccountCreatedState); + const handleChoosePocket = async () => { await dbAppSettingsUpdate({ 'user_settings.account_type': 'pocket' }); - await setIsAccountChoose(false); + setIsUserAccountCreated(false); + setIsAccountChoose(false); }; const handleChooseVIP = async () => { await dbAppSettingsUpdate({ 'user_settings.account_type': 'vip' }); - await setIsAccountChoose(false); + setIsUserAccountCreated(false); + setIsAccountChoose(false); }; return { handleChoosePocket, handleChooseVIP }; diff --git a/src/features/app_start/vip/congregation_encryption/congregation_access_code/criteria/index.tsx b/src/features/app_start/vip/congregation_create/congregation_access_code/criteria/index.tsx similarity index 100% rename from src/features/app_start/vip/congregation_encryption/congregation_access_code/criteria/index.tsx rename to src/features/app_start/vip/congregation_create/congregation_access_code/criteria/index.tsx diff --git a/src/features/app_start/vip/congregation_create/congregation_access_code/index.tsx b/src/features/app_start/vip/congregation_create/congregation_access_code/index.tsx new file mode 100644 index 0000000000..c7583a2816 --- /dev/null +++ b/src/features/app_start/vip/congregation_create/congregation_access_code/index.tsx @@ -0,0 +1,177 @@ +import { Box } from '@mui/material'; +import { IconCongregationAccess, IconError, IconLoading } from '@icons/index'; +import { useAppTranslation } from '@hooks/index'; +import useCongregationAccessCode from './useCongregationAccessCode'; +import Button from '@components/button'; +import Criteria from './criteria'; +import InfoMessage from '@components/info-message'; +import TextField from '@components/textfield'; +import Typography from '@components/typography'; +import VipInfoTip from '@features/app_start/vip/vip_info_tip'; + +const CongregationAccessCode = () => { + const { t } = useAppTranslation(); + + const { + isLengthPassed, + isNumberPassed, + isLowerCasePassed, + isUpperCasePassed, + isSpecialSymbolPassed, + isProcessing, + hideMessage, + message, + title, + variant, + isMatch, + setTmpAccessCode, + setTmpAccessCodeVerify, + tmpAccessCode, + tmpAccessCodeVerify, + btnActionDisabled, + handleSetAccessCode, + } = useCongregationAccessCode(); + + return ( + + + + + + {t('tr_congregationAccessCodeNotice')} + + + + + + setTmpAccessCode(e.target.value)} + startIcon={} + resetHelperPadding={true} + /> + setTmpAccessCodeVerify(e.target.value)} + startIcon={} + resetHelperPadding={true} + helperText={ + + + + + + + + + } + /> + + + + + + + + + + + } + messageHeader={title} + message={message} + onClose={hideMessage} + /> + + + + ); +}; + +export default CongregationAccessCode; diff --git a/src/features/app_start/vip/congregation_create/congregation_access_code/useCongregationAccessCode.tsx b/src/features/app_start/vip/congregation_create/congregation_access_code/useCongregationAccessCode.tsx new file mode 100644 index 0000000000..28309fbb5f --- /dev/null +++ b/src/features/app_start/vip/congregation_create/congregation_access_code/useCongregationAccessCode.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from 'react'; +import { useAppTranslation } from '@hooks/index'; +import { encryptData, generateKey } from '@services/encryption/index'; +import { displayOnboardingFeedback } from '@services/recoil/app'; +import { getMessageByCode } from '@services/i18n/translation'; +import { apiSetCongregationAccessCode } from '@services/api/congregation'; +import { dbAppSettingsUpdate } from '@services/dexie/settings'; +import useFeedback from '@features/app_start/shared/hooks/useFeedback'; + +const useCongregationAccessCode = () => { + const { t } = useAppTranslation(); + + const { hideMessage, message, showMessage, title, variant } = useFeedback(); + + const [tmpAccessCode, setTmpAccessCode] = useState(''); + const [tmpAccessCodeVerify, setTmpAccessCodeVerify] = useState(''); + const [isLengthPassed, setIsLengthPassed] = useState(false); + const [isNumberPassed, setIsNumberPassed] = useState(false); + const [isLowerCasePassed, setIsLowerCasePassed] = useState(false); + const [isUpperCasePassed, setIsUpperCasePassed] = useState(false); + const [isSpecialSymbolPassed, setIsSpecialSymbolPassed] = useState(false); + const [isMatch, setIsMatch] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + + const btnActionDisabled = + !isLengthPassed || + !isNumberPassed || + !isLowerCasePassed || + !isUpperCasePassed || + !isSpecialSymbolPassed || + !isMatch; + + const handleSetAccessCode = async () => { + if (isProcessing) return; + hideMessage(); + setIsProcessing(true); + + try { + const encryptionKey = generateKey(); + const encryptedKey = encryptData(encryptionKey, tmpAccessCode); + + const { status, data } = await apiSetCongregationAccessCode(encryptedKey); + + if (status !== 200) { + await displayOnboardingFeedback({ + title: t('tr_errorGeneric'), + message: getMessageByCode(data.message), + }); + showMessage(); + + setIsProcessing(false); + return; + } + + await dbAppSettingsUpdate({ + 'cong_settings.cong_access_code': tmpAccessCode, + }); + } catch (err) { + await displayOnboardingFeedback({ + title: t('tr_errorGeneric'), + message: getMessageByCode(err.message), + }); + showMessage(); + + setIsProcessing(false); + } + }; + + useEffect(() => { + setIsLengthPassed(tmpAccessCode.length >= 8); + setIsNumberPassed(/\d/.test(tmpAccessCode)); + setIsLowerCasePassed(/[a-z]/.test(tmpAccessCode)); + setIsUpperCasePassed(/[A-Z]/.test(tmpAccessCode)); + setIsSpecialSymbolPassed(/[!@#$%^&*(),.?"’:{}|<>]/.test(tmpAccessCode)); + setIsMatch( + tmpAccessCode.length > 0 && tmpAccessCode === tmpAccessCodeVerify + ); + }, [tmpAccessCode, tmpAccessCodeVerify]); + + return { + tmpAccessCode, + setTmpAccessCode, + isLengthPassed, + isNumberPassed, + isLowerCasePassed, + isUpperCasePassed, + isSpecialSymbolPassed, + isProcessing, + message, + title, + hideMessage, + variant, + isMatch, + setTmpAccessCodeVerify, + tmpAccessCodeVerify, + btnActionDisabled, + handleSetAccessCode, + }; +}; + +export default useCongregationAccessCode; diff --git a/src/features/app_start/vip/congregation_create/congregation_info/index.tsx b/src/features/app_start/vip/congregation_create/congregation_details/index.tsx similarity index 89% rename from src/features/app_start/vip/congregation_create/congregation_info/index.tsx rename to src/features/app_start/vip/congregation_create/congregation_details/index.tsx index 8fd365cecd..4265673e04 100644 --- a/src/features/app_start/vip/congregation_create/congregation_info/index.tsx +++ b/src/features/app_start/vip/congregation_create/congregation_details/index.tsx @@ -1,21 +1,16 @@ import { Box } from '@mui/material'; import { IconAccount, IconError, IconLoading } from '@icons/index'; import { useAppTranslation } from '@hooks/index'; -import useCongregationInfo from './useCongregationInfo'; +import useCongregationDetails from './useCongregationDetails'; import Button from '@components/button'; import Checkbox from '@components/checkbox'; import CongregationSelector from '@components/congregation_selector'; import CountrySelector from '@components/country_selector'; import InfoMessage from '@components/info-message'; import TextField from '@components/textfield'; -import PageHeader from '@features/app_start/shared/page_header'; import VipInfoTip from '@features/app_start/vip/vip_info_tip'; -const CongregationInfo = ({ - setIsCreate, -}: { - setIsCreate: (value: boolean) => void; -}) => { +const CongregationDetails = () => { const { t } = useAppTranslation(); const { @@ -35,7 +30,7 @@ const CongregationInfo = ({ handleToggleApproval, isElderApproved, congregation, - } = useCongregationInfo(); + } = useCongregationDetails(); return ( - setIsCreate(false)} - /> - { +const useCongregationDetails = () => { const { t } = useAppTranslation(); const { hideMessage, message, showMessage, title, variant } = useFeedback(); + const setCurrentStep = useSetRecoilState(congregationCreateStepState); + const settings = useRecoilValue(settingsState); const [isProcessing, setIsProcessing] = useState(false); @@ -141,8 +142,7 @@ const useCongregationInfo = () => { setUserID(result.user_id); - setIsCongAccountCreate(false); - setIsEncryptionCodeOpen(true); + setCurrentStep(1); } catch (err) { setIsProcessing(false); @@ -176,4 +176,4 @@ const useCongregationInfo = () => { }; }; -export default useCongregationInfo; +export default useCongregationDetails; diff --git a/src/features/app_start/vip/congregation_encryption/congregation_master_key/criteria/index.tsx b/src/features/app_start/vip/congregation_create/congregation_master_key/criteria/index.tsx similarity index 100% rename from src/features/app_start/vip/congregation_encryption/congregation_master_key/criteria/index.tsx rename to src/features/app_start/vip/congregation_create/congregation_master_key/criteria/index.tsx diff --git a/src/features/app_start/vip/congregation_create/congregation_master_key/index.tsx b/src/features/app_start/vip/congregation_create/congregation_master_key/index.tsx new file mode 100644 index 0000000000..14e30efdfc --- /dev/null +++ b/src/features/app_start/vip/congregation_create/congregation_master_key/index.tsx @@ -0,0 +1,179 @@ +import { Box } from '@mui/material'; +import { IconEncryptionKey, IconError, IconLoading } from '@icons/index'; +import { useAppTranslation } from '@hooks/index'; +import useCongregationMasterKey from './useCongregationMasterKey'; +import Button from '@components/button'; +import Criteria from './criteria'; +import InfoMessage from '@components/info-message'; +import TextField from '@components/textfield'; +import Typography from '@components/typography'; +import Markup from '@components/text_markup'; + +const CongregationMasterKey = () => { + const { t } = useAppTranslation(); + + const { + isLengthPassed, + isNumberPassed, + isLowerCasePassed, + isUpperCasePassed, + isSpecialSymbolPassed, + isProcessing, + hideMessage, + message, + title, + variant, + isMatch, + setTmpMasterKey, + setTmpMasterKeyVerify, + tmpMasterKey, + tmpMasterKeyVerify, + btnActionDisabled, + handleSetMasterKey, + } = useCongregationMasterKey(); + + return ( + + + + + + + {t('tr_encryptionCodeNotice')} + + + + + + setTmpMasterKey(e.target.value)} + startIcon={} + resetHelperPadding={true} + /> + setTmpMasterKeyVerify(e.target.value)} + startIcon={} + resetHelperPadding={true} + helperText={ + + + + + + + + + } + /> + + + + + + + + } + messageHeader={title} + message={message} + onClose={hideMessage} + /> + + + ); +}; + +export default CongregationMasterKey; diff --git a/src/features/app_start/vip/congregation_create/congregation_master_key/useCongregationMasterKey.tsx b/src/features/app_start/vip/congregation_create/congregation_master_key/useCongregationMasterKey.tsx new file mode 100644 index 0000000000..500f2b8e0f --- /dev/null +++ b/src/features/app_start/vip/congregation_create/congregation_master_key/useCongregationMasterKey.tsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { useAppTranslation } from '@hooks/index'; +import { encryptData, generateKey } from '@services/encryption/index'; +import { displayOnboardingFeedback } from '@services/recoil/app'; +import { getMessageByCode } from '@services/i18n/translation'; +import { apiSetCongregationMasterKey } from '@services/api/congregation'; +import { dbAppSettingsUpdate } from '@services/dexie/settings'; +import { congregationCreateStepState } from '@states/app'; +import useFeedback from '@features/app_start/shared/hooks/useFeedback'; + +const useCongregationMasterKey = () => { + const { t } = useAppTranslation(); + + const { hideMessage, message, showMessage, title, variant } = useFeedback(); + + const setCurrentStep = useSetRecoilState(congregationCreateStepState); + + const [tmpMasterKey, setTmpMasterKey] = useState(''); + const [tmpMasterKeyVerify, setTmpMasterKeyVerify] = useState(''); + const [isLengthPassed, setIsLengthPassed] = useState(false); + const [isNumberPassed, setIsNumberPassed] = useState(false); + const [isLowerCasePassed, setIsLowerCasePassed] = useState(false); + const [isUpperCasePassed, setIsUpperCasePassed] = useState(false); + const [isSpecialSymbolPassed, setIsSpecialSymbolPassed] = useState(false); + const [isMatch, setIsMatch] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + + const btnActionDisabled = + !isLengthPassed || + !isNumberPassed || + !isLowerCasePassed || + !isUpperCasePassed || + !isSpecialSymbolPassed || + !isMatch; + + const handleSetMasterKey = async () => { + if (isProcessing) return; + hideMessage(); + setIsProcessing(true); + + try { + const encryptionKey = generateKey(); + const encryptedKey = encryptData(encryptionKey, tmpMasterKey); + + const { status, data } = await apiSetCongregationMasterKey(encryptedKey); + + if (status !== 200) { + await displayOnboardingFeedback({ + title: t('tr_errorGeneric'), + message: getMessageByCode(data.message), + }); + showMessage(); + + setIsProcessing(false); + return; + } + + await dbAppSettingsUpdate({ + 'cong_settings.cong_master_key': tmpMasterKey, + }); + + setCurrentStep(2); + } catch (err) { + await displayOnboardingFeedback({ + title: t('tr_errorGeneric'), + message: getMessageByCode(err.message), + }); + showMessage(); + + setIsProcessing(false); + } + }; + + useEffect(() => { + setIsLengthPassed(tmpMasterKey.length >= 16); + setIsNumberPassed(/\d/.test(tmpMasterKey)); + setIsLowerCasePassed(/[a-z]/.test(tmpMasterKey)); + setIsUpperCasePassed(/[A-Z]/.test(tmpMasterKey)); + setIsSpecialSymbolPassed(/[!@#$%^&*(),.?"’:{}|<>]/.test(tmpMasterKey)); + setIsMatch(tmpMasterKey.length > 0 && tmpMasterKey === tmpMasterKeyVerify); + }, [tmpMasterKey, tmpMasterKeyVerify]); + + return { + handleSetMasterKey, + tmpMasterKey, + setTmpMasterKey, + tmpMasterKeyVerify, + setTmpMasterKeyVerify, + isLengthPassed, + isNumberPassed, + isLowerCasePassed, + isUpperCasePassed, + isSpecialSymbolPassed, + isProcessing, + message, + title, + hideMessage, + variant, + isMatch, + btnActionDisabled, + }; +}; + +export default useCongregationMasterKey; diff --git a/src/features/app_start/vip/congregation_create/index.tsx b/src/features/app_start/vip/congregation_create/index.tsx index b72770e2fc..9acd3b76bd 100644 --- a/src/features/app_start/vip/congregation_create/index.tsx +++ b/src/features/app_start/vip/congregation_create/index.tsx @@ -1,19 +1,68 @@ +import { Box, Step, StepLabel, Stepper } from '@mui/material'; +import { useAppTranslation } from '@hooks/index'; import useCongregationCreate from './useCongregationCreate'; -import UserAccountCreated from './account_created'; -import CongregationInfo from './congregation_info'; +import PageHeader from '@features/app_start/shared/page_header'; +import Typography from '@components/typography'; const CongregationCreate = () => { - const { isCreate, setIsCreate } = useCongregationCreate(); + const { t } = useAppTranslation(); + + const { steps, currentStep } = useCongregationCreate(); return ( - <> - {!isCreate && ( - setIsCreate(value)} /> - )} - {isCreate && ( - setIsCreate(value)} /> - )} - + + + + + {steps.map((step, index) => ( + + + + {step.label} + + + + ))} + + + {steps[currentStep].Component} + ); }; diff --git a/src/features/app_start/vip/congregation_create/useCongregationCreate.tsx b/src/features/app_start/vip/congregation_create/useCongregationCreate.tsx index b8a89e067d..f99885a7a2 100644 --- a/src/features/app_start/vip/congregation_create/useCongregationCreate.tsx +++ b/src/features/app_start/vip/congregation_create/useCongregationCreate.tsx @@ -1,9 +1,40 @@ -import { useState } from 'react'; +import { useEffect, useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; +import { useAppTranslation } from '@hooks/index'; +import { congregationCreateStepState } from '@states/app'; +import CongregationAccessCode from './congregation_access_code'; +import CongregationDetails from './congregation_details'; +import CongregationMasterKey from './congregation_master_key'; const useCongregationCreate = () => { - const [isCreate, setIsCreate] = useState(false); + const { t } = useAppTranslation(); - return { isCreate, setIsCreate }; + const currentStep = useRecoilValue(congregationCreateStepState); + + const steps = useMemo(() => { + return [ + { + label: t('tr_congregationDetails'), + Component: , + }, + { label: t('tr_createMasterKey'), Component: }, + { + label: t('tr_createAccessCode'), + Component: , + }, + ]; + }, [t]); + + useEffect(() => { + const stepIconTexts: NodeListOf = + document.querySelectorAll('.MuiStepIcon-text'); + + stepIconTexts.forEach((text) => { + text.classList.add('label-small-medium'); + }); + }, []); + + return { steps, currentStep }; }; export default useCongregationCreate; diff --git a/src/features/app_start/vip/congregation_encryption/congregation_access_code/index.tsx b/src/features/app_start/vip/congregation_encryption/congregation_access_code/index.tsx index 6264a6f448..60fb189a58 100644 --- a/src/features/app_start/vip/congregation_encryption/congregation_access_code/index.tsx +++ b/src/features/app_start/vip/congregation_encryption/congregation_access_code/index.tsx @@ -1,39 +1,27 @@ import { Box } from '@mui/material'; -import { IconEncryptionKey, IconError, IconLoading } from '@icons/index'; +import { IconCongregationAccess, IconError, IconLoading } from '@icons/index'; import { useAppTranslation } from '@hooks/index'; import useCongregationAccessCode from './useCongregationAccessCode'; import PageHeader from '@features/app_start/shared/page_header'; import Button from '@components/button'; import InfoMessage from '@components/info-message'; import TextField from '@components/textfield'; -import Typography from '@components/typography'; import WaitingLoader from '@components/waiting_loader'; -import Criteria from './criteria'; -import VipInfoTip from '@features/app_start/vip/vip_info_tip'; const CongregationAccessCode = () => { const { t } = useAppTranslation(); const { isLoading, - isLengthPassed, - isNumberPassed, - isLowerCasePassed, - isUpperCasePassed, - isSpecialSymbolPassed, isProcessing, hideMessage, message, title, variant, - handleAction, - isSetupCode, - isMatch, setTmpAccessCode, - setTmpAccessCodeVerify, tmpAccessCode, - tmpAccessCodeVerify, btnActionDisabled, + handleValidateAccessCode, } = useCongregationAccessCode(); return ( @@ -60,32 +48,9 @@ const CongregationAccessCode = () => { > - {isSetupCode && ( - - - - {t('tr_congregationAccessCodeNotice')} - - - )} - { > setTmpAccessCode(e.target.value)} - startIcon={} + startIcon={} resetHelperPadding={true} /> - {isSetupCode && ( - setTmpAccessCodeVerify(e.target.value)} - startIcon={} - resetHelperPadding={true} - helperText={ - isSetupCode ? ( - - - - - - - - - ) : null - } - /> - )} - - { const congNumber = useRecoilValue(congNumberState); const [isLoading, setIsLoading] = useState(true); - const [isSetupCode, setIsSetupCode] = useState(true); const [tmpAccessCode, setTmpAccessCode] = useState(''); const [tmpAccessCodeVerify, setTmpAccessCodeVerify] = useState(''); const [isLengthPassed, setIsLengthPassed] = useState(false); - const [isNumberPassed, setIsNumberPassed] = useState(false); - const [isLowerCasePassed, setIsLowerCasePassed] = useState(false); - const [isUpperCasePassed, setIsUpperCasePassed] = useState(false); - const [isSpecialSymbolPassed, setIsSpecialSymbolPassed] = useState(false); - const [isMatch, setIsMatch] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [congAccessCode, setCongAccessCode] = useState(''); - const btnActionDisabled = - !isLengthPassed || - !isNumberPassed || - !isLowerCasePassed || - !isUpperCasePassed || - !isSpecialSymbolPassed || - (isSetupCode && !isMatch); - - const handleAction = () => { - if (isSetupCode) handleSetAccessCode(); - if (!isSetupCode) handleValidateAccessCode(); - }; - - const handleSetAccessCode = async () => { - if (isProcessing) return; - hideMessage(); - setIsProcessing(true); - - try { - const encryptionKey = generateKey(); - const encryptedKey = encryptData(encryptionKey, tmpAccessCode); - - const { status, data } = await apiSetCongregationAccessCode(encryptedKey); - - if (status !== 200) { - await displayOnboardingFeedback({ - title: t('tr_errorGeneric'), - message: getMessageByCode(data.message), - }); - showMessage(); - - setIsProcessing(false); - return; - } - - await dbAppSettingsUpdate({ - 'cong_settings.cong_access_code': tmpAccessCode, - }); - } catch (err) { - await displayOnboardingFeedback({ - title: t('tr_errorGeneric'), - message: getMessageByCode(err.message), - }); - showMessage(); - - setIsProcessing(false); - } - }; + const btnActionDisabled = !isLengthPassed; const handleValidateAccessCode = async () => { if (isProcessing) return; @@ -137,7 +78,6 @@ const useCongregationAccessCode = () => { await setCongID(result.cong_id); setCongAccessCode(result.cong_access_code); - setIsSetupCode(result.cong_access_code.length === 0); setIsLoading(false); }; @@ -147,32 +87,19 @@ const useCongregationAccessCode = () => { useEffect(() => { setIsLengthPassed(tmpAccessCode.length >= 8); - setIsNumberPassed(/\d/.test(tmpAccessCode)); - setIsLowerCasePassed(/[a-z]/.test(tmpAccessCode)); - setIsUpperCasePassed(/[A-Z]/.test(tmpAccessCode)); - setIsSpecialSymbolPassed(/[!@#$%^&*(),.?"’:{}|<>]/.test(tmpAccessCode)); - setIsMatch( - tmpAccessCode.length > 0 && tmpAccessCode === tmpAccessCodeVerify - ); }, [tmpAccessCode, tmpAccessCodeVerify]); return { isLoading, - isSetupCode, tmpAccessCode, setTmpAccessCode, isLengthPassed, - isNumberPassed, - isLowerCasePassed, - isUpperCasePassed, - isSpecialSymbolPassed, isProcessing, - handleAction, + handleValidateAccessCode, message, title, hideMessage, variant, - isMatch, setTmpAccessCodeVerify, tmpAccessCodeVerify, btnActionDisabled, diff --git a/src/features/app_start/vip/congregation_encryption/congregation_master_key/index.tsx b/src/features/app_start/vip/congregation_encryption/congregation_master_key/index.tsx index 7a0bbcb213..2b2c5f7a9f 100644 --- a/src/features/app_start/vip/congregation_encryption/congregation_master_key/index.tsx +++ b/src/features/app_start/vip/congregation_encryption/congregation_master_key/index.tsx @@ -1,38 +1,27 @@ import { Box } from '@mui/material'; -import PageHeader from '@features/app_start/shared/page_header'; +import { IconEncryptionKey, IconError, IconLoading } from '@icons/index'; +import { useAppTranslation } from '@hooks/index'; +import useCongregationMasterKey from './useCongregationMasterKey'; import Button from '@components/button'; import InfoMessage from '@components/info-message'; +import PageHeader from '@features/app_start/shared/page_header'; import TextField from '@components/textfield'; -import Typography from '@components/typography'; import WaitingLoader from '@components/waiting_loader'; -import Criteria from './criteria'; -import { IconEncryptionKey, IconError, IconLoading } from '@icons/index'; -import { useAppTranslation } from '@hooks/index'; -import useCongregationMasterKey from './useCongregationMasterKey'; const CongregationEncryption = () => { const { t } = useAppTranslation(); const { - isLoading, - isLengthPassed, - isNumberPassed, - isLowerCasePassed, - isUpperCasePassed, - isSpecialSymbolPassed, isProcessing, hideMessage, message, title, variant, - handleAction, - isSetupCode, - isMatch, setTmpMasterKey, - setTmpMasterKeyVerify, tmpMasterKey, - tmpMasterKeyVerify, btnActionDisabled, + isLoading, + handleValidateMasterKey, } = useCongregationMasterKey(); return ( @@ -59,32 +48,9 @@ const CongregationEncryption = () => { > - {isSetupCode && ( - - - - {t('tr_encryptionCodeNotice')} - - - )} - { > { startIcon={} resetHelperPadding={true} /> - {isSetupCode && ( - setTmpMasterKeyVerify(e.target.value)} - startIcon={} - resetHelperPadding={true} - helperText={ - isSetupCode ? ( - - - - - - - - - ) : null - } - /> - )} diff --git a/src/features/app_start/vip/congregation_encryption/congregation_master_key/useCongregationMasterKey.tsx b/src/features/app_start/vip/congregation_encryption/congregation_master_key/useCongregationMasterKey.tsx index 9a24a8e88c..94e73986de 100644 --- a/src/features/app_start/vip/congregation_encryption/congregation_master_key/useCongregationMasterKey.tsx +++ b/src/features/app_start/vip/congregation_encryption/congregation_master_key/useCongregationMasterKey.tsx @@ -4,15 +4,9 @@ import { handleDeleteDatabase } from '@services/app'; import { useAppTranslation, useFirebaseAuth } from '@hooks/index'; import { userSignOut } from '@services/firebase/auth'; import useFeedback from '@features/app_start/shared/hooks/useFeedback'; -import { - decryptData, - encryptData, - generateKey, -} from '@services/encryption/index'; +import { decryptData } from '@services/encryption/index'; import { apiValidateMe } from '@services/api/user'; import { displayOnboardingFeedback, setCongID } from '@services/recoil/app'; -import { getMessageByCode } from '@services/i18n/translation'; -import { apiSetCongregationMasterKey } from '@services/api/congregation'; import { dbAppSettingsUpdate } from '@services/dexie/settings'; import { congNumberState } from '@states/settings'; @@ -26,66 +20,13 @@ const useCongregationMasterKey = () => { const congNumber = useRecoilValue(congNumberState); const [isLoading, setIsLoading] = useState(true); - const [isSetupCode, setIsSetupCode] = useState(true); const [tmpMasterKey, setTmpMasterKey] = useState(''); const [tmpMasterKeyVerify, setTmpMasterKeyVerify] = useState(''); const [isLengthPassed, setIsLengthPassed] = useState(false); - const [isNumberPassed, setIsNumberPassed] = useState(false); - const [isLowerCasePassed, setIsLowerCasePassed] = useState(false); - const [isUpperCasePassed, setIsUpperCasePassed] = useState(false); - const [isSpecialSymbolPassed, setIsSpecialSymbolPassed] = useState(false); - const [isMatch, setIsMatch] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [congMasterKey, setCongMasterKey] = useState(''); - const btnActionDisabled = - !isLengthPassed || - !isNumberPassed || - !isLowerCasePassed || - !isUpperCasePassed || - !isSpecialSymbolPassed || - (isSetupCode && !isMatch); - - const handleAction = () => { - if (isSetupCode) handleSetMasterKey(); - if (!isSetupCode) handleValidateMasterKey(); - }; - - const handleSetMasterKey = async () => { - if (isProcessing) return; - hideMessage(); - setIsProcessing(true); - - try { - const encryptionKey = generateKey(); - const encryptedKey = encryptData(encryptionKey, tmpMasterKey); - - const { status, data } = await apiSetCongregationMasterKey(encryptedKey); - - if (status !== 200) { - await displayOnboardingFeedback({ - title: t('tr_errorGeneric'), - message: getMessageByCode(data.message), - }); - showMessage(); - - setIsProcessing(false); - return; - } - - await dbAppSettingsUpdate({ - 'cong_settings.cong_master_key': tmpMasterKey, - }); - } catch (err) { - await displayOnboardingFeedback({ - title: t('tr_errorGeneric'), - message: getMessageByCode(err.message), - }); - showMessage(); - - setIsProcessing(false); - } - }; + const btnActionDisabled = !isLengthPassed; const handleValidateMasterKey = async () => { if (isProcessing) return; @@ -135,10 +76,7 @@ const useCongregationMasterKey = () => { } await setCongID(result.cong_id); - setCongMasterKey(result.cong_master_key); - setIsSetupCode(result.cong_master_key.length === 0); - setIsLoading(false); }; @@ -147,32 +85,21 @@ const useCongregationMasterKey = () => { useEffect(() => { setIsLengthPassed(tmpMasterKey.length >= 16); - setIsNumberPassed(/\d/.test(tmpMasterKey)); - setIsLowerCasePassed(/[a-z]/.test(tmpMasterKey)); - setIsUpperCasePassed(/[A-Z]/.test(tmpMasterKey)); - setIsSpecialSymbolPassed(/[!@#$%^&*(),.?"’:{}|<>]/.test(tmpMasterKey)); - setIsMatch(tmpMasterKey.length > 0 && tmpMasterKey === tmpMasterKeyVerify); }, [tmpMasterKey, tmpMasterKeyVerify]); return { isLoading, - isSetupCode, tmpMasterKey, setTmpMasterKey, tmpMasterKeyVerify, setTmpMasterKeyVerify, isLengthPassed, - isNumberPassed, - isLowerCasePassed, - isUpperCasePassed, - isSpecialSymbolPassed, isProcessing, - handleAction, + handleValidateMasterKey, message, title, hideMessage, variant, - isMatch, btnActionDisabled, }; }; diff --git a/src/features/app_start/vip/oauth/button_base/useButtonBase.tsx b/src/features/app_start/vip/oauth/button_base/useButtonBase.tsx index ee209cf6ec..145a71cdb7 100644 --- a/src/features/app_start/vip/oauth/button_base/useButtonBase.tsx +++ b/src/features/app_start/vip/oauth/button_base/useButtonBase.tsx @@ -2,10 +2,10 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { currentProviderState, isAuthProcessingState, - isCongAccountCreateState, isEmailAuthState, isEncryptionCodeOpenState, isUnauthorizedRoleState, + isUserAccountCreatedState, isUserMfaVerifyState, isUserSignInState, tokenDevState, @@ -33,7 +33,7 @@ const useButtonBase = ({ provider, isEmail }) => { const [isUserSignIn, setIsUserSignIn] = useRecoilState(isUserSignInState); const setUserMfaVerify = useSetRecoilState(isUserMfaVerifyState); - const setIsCongAccountCreate = useSetRecoilState(isCongAccountCreateState); + const setIsUserAccountCreated = useSetRecoilState(isUserAccountCreatedState); const setIsUnauthorizedRole = useSetRecoilState(isUnauthorizedRoleState); const setIsEncryptionCodeOpen = useSetRecoilState(isEncryptionCodeOpenState); const setIsEmailAuth = useSetRecoilState(isEmailAuthState); @@ -107,14 +107,14 @@ const useButtonBase = ({ provider, isEmail }) => { if (nextStep.isVerifyMFA) { setTokenDev(code); setIsUserSignIn(false); - setIsCongAccountCreate(false); + setIsUserAccountCreated(false); setIsUnauthorizedRole(false); setUserMfaVerify(true); } if (nextStep.createCongregation) { setIsUserSignIn(false); - setIsCongAccountCreate(true); + setIsUserAccountCreated(true); } if (nextStep.encryption) { @@ -162,7 +162,7 @@ const useButtonBase = ({ provider, isEmail }) => { const handleUnauthorizedUser = () => { setUserMfaVerify(true); - setIsCongAccountCreate(false); + setIsUserAccountCreated(false); setIsUnauthorizedRole(true); }; diff --git a/src/features/app_start/vip/startup/index.tsx b/src/features/app_start/vip/startup/index.tsx index be2e296e83..49aca4382b 100644 --- a/src/features/app_start/vip/startup/index.tsx +++ b/src/features/app_start/vip/startup/index.tsx @@ -1,28 +1,34 @@ +import useStartup from './useStartup'; import CongregationCreate from '../congregation_create'; import CongregationEncryption from '../congregation_encryption'; import EmailAuth from '../email_auth'; import EmailLinkAuthentication from '../email_link_authentication'; import Signin from '../signin'; import TermsUse from '../terms_use'; +import UserAccountCreated from '../user_account_created'; import VerifyMFA from '../verify_mfa'; -import useStartup from './useStartup'; +import WaitingLoader from '@components/waiting_loader'; const VipStartup = () => { const { isUserSignIn, isUserMfaVerify, - isCongAccountCreate, isEmailAuth, isEmailLinkAuth, isEncryptionCodeOpen, + isUserAccountCreated, + isCongCreate, + isLoading, } = useStartup(); return ( <> + {!isCongCreate && !isEncryptionCodeOpen && isLoading && } {isUserSignIn && } {isUserMfaVerify && } - {isCongAccountCreate && } + {isUserAccountCreated && } + {isCongCreate && } {isEmailAuth && } {isEmailLinkAuth && } {isEncryptionCodeOpen && } diff --git a/src/features/app_start/vip/startup/useStartup.tsx b/src/features/app_start/vip/startup/useStartup.tsx index 39e42dbc3f..fba292ec0e 100644 --- a/src/features/app_start/vip/startup/useStartup.tsx +++ b/src/features/app_start/vip/startup/useStartup.tsx @@ -1,13 +1,15 @@ -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { useFirebaseAuth } from '@hooks/index'; import { + congIDState, + congregationCreateStepState, cookiesConsentState, isCongAccountCreateState, isEmailAuthState, isEmailLinkAuthenticateState, isEncryptionCodeOpenState, + isUserAccountCreatedState, isUserMfaVerifyState, isUserSignInState, offlineOverrideState, @@ -26,27 +28,37 @@ import { congNameState, congAccessCodeState, congRoleState, + congMasterKeyState, + congNumberState, } from '@states/settings'; -import { APP_ROLES } from '@constants/index'; -import { loadApp, runUpdater } from '@services/app'; +import { APP_ROLES, VIP_ROLES } from '@constants/index'; +import { handleDeleteDatabase, loadApp, runUpdater } from '@services/app'; +import { apiValidateMe } from '@services/api/user'; +import { userSignOut } from '@services/firebase/auth'; const useStartup = () => { - const { isAuthenticated } = useFirebaseAuth(); - const [searchParams] = useSearchParams(); const setCookiesConsent = useSetRecoilState(cookiesConsentState); + const setCongCreate = useSetRecoilState(isCongAccountCreateState); + const setCurrentStep = useSetRecoilState(congregationCreateStepState); + const setCongID = useSetRecoilState(congIDState); const isEmailLinkAuth = useRecoilValue(isEmailLinkAuthenticateState); const isEmailAuth = useRecoilValue(isEmailAuthState); const isUserSignIn = useRecoilValue(isUserSignInState); const isUserMfaVerify = useRecoilValue(isUserMfaVerifyState); - const isCongAccountCreate = useRecoilValue(isCongAccountCreateState); + const isUserAccountCreated = useRecoilValue(isUserAccountCreatedState); const isOfflineOverride = useRecoilValue(offlineOverrideState); const congName = useRecoilValue(congNameState); const congRole = useRecoilValue(congRoleState); const isEncryptionCodeOpen = useRecoilValue(isEncryptionCodeOpenState); const congAccessCode = useRecoilValue(congAccessCodeState); + const isCongCreate = useRecoilValue(isCongAccountCreateState); + const congMasterKey = useRecoilValue(congMasterKeyState); + const congNumber = useRecoilValue(congNumberState); + + const [isLoading, setIsLoading] = useState(true); const showSignin = useCallback(() => { setIsUserSignIn(true); @@ -55,30 +67,37 @@ const useStartup = () => { setUserMfaSetup(false); }, []); - const runNotAuthenticatedStep = useCallback(async () => { + const runStartupCheck = useCallback(async () => { + setIsLoading(true); + if (isOfflineOverride) { + setIsLoading(false); showSignin(); return; } if (congName.length === 0) { + setIsLoading(false); showSignin(); return; } const approvedRole = congRole.some((role) => APP_ROLES.includes(role)); + const masterKeyNeeded = congRole.some((role) => VIP_ROLES.includes(role)); if (!approvedRole) { + setIsLoading(false); showSignin(); return; } - if (congAccessCode.length === 0) { - setIsEncryptionCodeOpen(true); - return; - } + const allowOpen = + (masterKeyNeeded && + congMasterKey.length > 0 && + congAccessCode.length > 0) || + (!masterKeyNeeded && congAccessCode.length > 0); - if (congAccessCode.length > 0) { + if (allowOpen) { setIsSetup(false); await loadApp(); await runUpdater(); @@ -86,8 +105,67 @@ const useStartup = () => { setIsSetup(false); setIsAppLoad(false); }, 1000); + + return; + } + + const { status, result } = await apiValidateMe(); + + if (status === 403) { + await userSignOut(); + return; + } + + // congregation not found -> user not authorized and delete local data + if (status === 404) { + await handleDeleteDatabase(); + return; + } + + if (status === 200) { + if (congNumber.length > 0 && result.cong_number !== congNumber) { + await handleDeleteDatabase(); + return; + } } - }, [isOfflineOverride, congName, congRole, showSignin, congAccessCode]); + + const remoteMasterKey = result.cong_master_key; + const remoteAccessCode = result.cong_access_code; + + if (remoteMasterKey.length === 0 || remoteAccessCode.length === 0) { + setCongID(result.cong_id); + + if (masterKeyNeeded && remoteMasterKey.length === 0) { + setCurrentStep(1); + } + + if ( + masterKeyNeeded && + remoteMasterKey.length > 0 && + remoteAccessCode.length === 0 + ) { + setCurrentStep(2); + } + + setIsLoading(false); + setCongCreate(true); + return; + } + + if (congAccessCode.length === 0) { + setIsEncryptionCodeOpen(true); + } + + setIsLoading(false); + }, [ + isOfflineOverride, + congName, + congRole, + showSignin, + congAccessCode, + congMasterKey, + congNumber, + ]); useEffect(() => { const checkLink = async () => { @@ -99,8 +177,8 @@ const useStartup = () => { }, [searchParams]); useEffect(() => { - if (!isAuthenticated) runNotAuthenticatedStep(); - }, [isAuthenticated, runNotAuthenticatedStep]); + runStartupCheck(); + }, [runStartupCheck]); useEffect(() => { const checkCookiesConsent = async () => { @@ -115,9 +193,11 @@ const useStartup = () => { isEmailAuth, isUserSignIn, isUserMfaVerify, - isCongAccountCreate, + isUserAccountCreated, isEmailLinkAuth, isEncryptionCodeOpen, + isCongCreate, + isLoading, }; }; diff --git a/src/features/app_start/vip/terms_use/useTermsUse.tsx b/src/features/app_start/vip/terms_use/useTermsUse.tsx index b9c711c38f..ac7b729275 100644 --- a/src/features/app_start/vip/terms_use/useTermsUse.tsx +++ b/src/features/app_start/vip/terms_use/useTermsUse.tsx @@ -19,7 +19,7 @@ const useTermsUse = () => { const font = params.get('font') || 'Inter'; localStorage.setItem('userConsent', 'accept'); - localStorage.setItem('app_lang', lang); + localStorage.setItem('ui_lang', lang); localStorage.setItem('app_font', font); setCookiesConsent(true); diff --git a/src/features/app_start/vip/congregation_create/account_created/index.tsx b/src/features/app_start/vip/user_account_created/index.tsx similarity index 81% rename from src/features/app_start/vip/congregation_create/account_created/index.tsx rename to src/features/app_start/vip/user_account_created/index.tsx index 537219369a..f883bfbe96 100644 --- a/src/features/app_start/vip/congregation_create/account_created/index.tsx +++ b/src/features/app_start/vip/user_account_created/index.tsx @@ -1,16 +1,15 @@ import { Box } from '@mui/material'; +import { useAppTranslation } from '@hooks/index'; +import useUserAccountCreated from './useUserAccountCreated'; import Button from '@components/button'; -import Typography from '@components/typography'; import PageHeader from '@features/app_start/shared/page_header'; -import { useAppTranslation } from '@hooks/index'; +import Typography from '@components/typography'; -const UserAccountCreated = ({ - setIsCreate, -}: { - setIsCreate: (value: boolean) => void; -}) => { +const UserAccountCreated = () => { const { t } = useAppTranslation(); + const { handleCreateCongregation } = useUserAccountCreated(); + return ( {t('tr_congregationCreateLabel')} - diff --git a/src/features/app_start/vip/user_account_created/useUserAccountCreated.tsx b/src/features/app_start/vip/user_account_created/useUserAccountCreated.tsx new file mode 100644 index 0000000000..9cc1bdef88 --- /dev/null +++ b/src/features/app_start/vip/user_account_created/useUserAccountCreated.tsx @@ -0,0 +1,19 @@ +import { useSetRecoilState } from 'recoil'; +import { + isCongAccountCreateState, + isUserAccountCreatedState, +} from '@states/app'; + +const useUserAccountCreated = () => { + const setCongCreate = useSetRecoilState(isCongAccountCreateState); + const setUserCreated = useSetRecoilState(isUserAccountCreatedState); + + const handleCreateCongregation = () => { + setUserCreated(false); + setCongCreate(true); + }; + + return { handleCreateCongregation }; +}; + +export default useUserAccountCreated; diff --git a/src/features/app_start/vip/vip_info_tip/index.tsx b/src/features/app_start/vip/vip_info_tip/index.tsx index b32f04efab..c89a4f0454 100644 --- a/src/features/app_start/vip/vip_info_tip/index.tsx +++ b/src/features/app_start/vip/vip_info_tip/index.tsx @@ -12,32 +12,34 @@ const VipInfoTip = (props: VipInfoTipProps) => { return ( <> - - + {messageShown && ( + + + + + + + + )} - - - -