diff --git a/lang/ca.json b/lang/ca.json index 7023c9de42..ff79705c6c 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -383,6 +383,18 @@ "label.eligible_networks_for_matching": "Xarxes aptes per a la concordança QF", "label.email": "correu electrònic", "label.email_address": "Adreça electrònica", + "label.email_verified": "Correu electrònic verificat", + "label.email_verify": "Verifica el correu electrònic", + "label.email_already_verified": "El teu correu electrònic ha estat verificat. Ara pots desar la informació del teu perfil.", + "label.email_used": "Aquesta adreça de correu electrònic s'utilitzarà per enviar-te comunicacions importants.", + "label.email_used_another": "Aquest correu electrònic ja ha estat verificat en un altre perfil!", + "label.email_sent_to": "Codi de verificació enviat a {email}", + "label.email_please_verify": "Si us plau, verifica el teu correu electrònic. Introdueix el codi de confirmació enviat al teu correu.", + "label.email_get_resend": "No has rebut el correu? Revisa la teva carpeta de correu brossa o ", + "label.email_confirm_code": "Confirma el codi", + "label.email_verify_banner": "Obté l'accés complet als teus comptes i projectes. Verifica la teva adreça de correu electrònic! ", + "label.email_actions_text": "Verifica el teu correu electrònic per gestionar els teus projectes!", + "label.email_error_verify": "Error de verificació del correu electrònic", "label.enable_change": "Habilita el canvi", "label.enable_recurring_donations": "Habilitar Donacions Recurrents", "label.ends_on": "acaba el", @@ -610,7 +622,7 @@ "label.loading": "Carregant", "label.loading_data": "Carregant Dades", "label.location": "Ubicació", - "label.location_optional": "ubicació (opcional)", + "label.location_optional": "Ubicació (opcional)", "label.locekd_giv": "GIV Bloquejat", "label.locked_for": "Bloquejat per", "label.locked_giv_details": "Detalls del GIV bloquejat", @@ -1204,7 +1216,7 @@ "label.wallet": "CARTERA", "label.wallet_connect": "Connexió de la cartera", "label.want_to_use_another_wallet": "Vols usar una altra cartera?", - "label.website_or_url": "lloc web o URL", + "label.website_or_url": "Lloc web o URL", "label.week": "setmana", "label.welcome_giver": "Benvingut, Giver", "label.welcome_to_the": "Benvingut a", diff --git a/lang/en.json b/lang/en.json index 552db6df57..13e9659b3a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -381,8 +381,20 @@ "label.elevate_projects": "Elevate Projects", "label.eligible_for_matching": "Eligible for Matching", "label.eligible_networks_for_matching": "Eligible networks for QF matching", - "label.email": "email", + "label.email": "Email", "label.email_address": "Email Address", + "label.email_verified": "Email Verified", + "label.email_verify": "Verify Email", + "label.email_already_verified": "Your email has been verified. You can now save your profile information.", + "label.email_used": "This email address will be used to send you important communications.", + "label.email_used_another": "This email that has already been verified on another profile!", + "label.email_sent_to": "Verification code sent to {email}", + "label.email_please_verify": "Please Verify your email. Enter the confirmation code sent to your email.", + "label.email_get_resend": "Didn't get the email? Check your spam folder or ", + "label.email_confirm_code": "Confirm Code", + "label.email_verify_banner": "Get the full access to your accounts and projects. Verify your email address! ", + "label.email_actions_text": "Verify your email to manage your projects!", + "label.email_error_verify": "Error verification email", "label.enable_change": "Enable Change", "label.enable_recurring_donations": "Enable Recurring Donations", "label.ends_on": "ends on", @@ -610,7 +622,7 @@ "label.loading": "Loading", "label.loading_data": "Loading Data", "label.location": "Location", - "label.location_optional": "location (optional)", + "label.location_optional": "Location (optional)", "label.locekd_giv": "Locked GIV", "label.locked_for": "Locked for", "label.locked_giv_details": "Locked GIV Details", @@ -1204,7 +1216,7 @@ "label.wallet": "WALLET", "label.wallet_connect": "Wallet Connect", "label.want_to_use_another_wallet": "Want to use another wallet?", - "label.website_or_url": "website or url", + "label.website_or_url": "Website or url", "label.week": "week", "label.welcome_giver": "Welcome, Giver", "label.welcome_to_the": "Welcome to the", diff --git a/lang/es.json b/lang/es.json index 410ca22183..fc3a0c5389 100644 --- a/lang/es.json +++ b/lang/es.json @@ -383,6 +383,18 @@ "label.eligible_networks_for_matching": "Redes elegibles para la asignación de QF", "label.email": "Email", "label.email_address": "Dirección de Email", + "label.email_verified": "Correo electrónico verificado", + "label.email_verify": "Verificar correo electrónico", + "label.email_already_verified": "Tu correo electrónico ha sido verificado. Ahora puedes guardar la información de tu perfil.", + "label.email_used": "Esta dirección de correo electrónico se utilizará para enviarte comunicaciones importantes.", + "label.email_used_another": "¡Este correo electrónico ya ha sido verificado en otro perfil!", + "label.email_sent_to": "Código de verificación enviado a {email}", + "label.email_please_verify": "Por favor, verifica tu correo electrónico. Ingresa el código de confirmación enviado a tu correo.", + "label.email_get_resend": "¿No recibiste el correo? Revisa tu carpeta de spam o ", + "label.email_confirm_code": "Confirmar código", + "label.email_verify_banner": "Obtén acceso completo a tus cuentas y proyectos. ¡Verifica tu dirección de correo electrónico! ", + "label.email_actions_text": "¡Verifica tu correo electrónico para gestionar tus proyectos!", + "label.email_error_verify": "Error de verificación del correo electrónico", "label.enable_change": "Ayuda al Cambio", "label.enable_recurring_donations": "Habilitar Donaciones Recurrentes", "label.ends_on": "termina el", diff --git a/pages/account.tsx b/pages/account.tsx index 1d13e12c95..4714c88d4f 100644 --- a/pages/account.tsx +++ b/pages/account.tsx @@ -6,6 +6,7 @@ import WalletNotConnected from '@/components/WalletNotConnected'; import UserNotSignedIn from '@/components/UserNotSignedIn'; import UserProfileView from '@/components/views/userProfile/UserProfile.view'; import { ProfileProvider } from '@/context/profile.context'; +import VerifyEmailBanner from '@/components/views/userProfile/VerifyEmailBanner'; const AccountRoute = () => { const { isSignedIn, isEnabled, userData, isLoading } = useAppSelector( @@ -27,6 +28,7 @@ const AccountRoute = () => { {userData?.name} | Giveth + {!userData?.isEmailVerified && } diff --git a/pages/project/[projectIdSlug]/index.tsx b/pages/project/[projectIdSlug]/index.tsx index d4f4e58c07..3b701dd79f 100644 --- a/pages/project/[projectIdSlug]/index.tsx +++ b/pages/project/[projectIdSlug]/index.tsx @@ -10,8 +10,6 @@ import { ProjectProvider } from '@/context/project.context'; const ProjectRoute: FC = ({ project }) => { useReferral(); - console.log({ project }); - return ( diff --git a/src/apollo/gql/gqlUser.ts b/src/apollo/gql/gqlUser.ts index b3dfd670fa..975eb9da1b 100644 --- a/src/apollo/gql/gqlUser.ts +++ b/src/apollo/gql/gqlUser.ts @@ -256,3 +256,18 @@ export const FETCH_USERS_GIVPOWER_BY_ADDRESS = ` } } }`; + +export const SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW = gql` + mutation SendUserEmailConfirmationCodeFlow($email: String!) { + sendUserEmailConfirmationCodeFlow(email: $email) + } +`; + +export const SEND_USER_CONFIRMATION_CODE_FLOW = gql` + mutation SendUserConfirmationCodeFlow( + $verifyCode: String! + $email: String! + ) { + sendUserConfirmationCodeFlow(verifyCode: $verifyCode, email: $email) + } +`; diff --git a/src/apollo/types/types.ts b/src/apollo/types/types.ts index b461427456..5187903452 100644 --- a/src/apollo/types/types.ts +++ b/src/apollo/types/types.ts @@ -237,6 +237,7 @@ export interface IUser { wasReferred?: boolean; isReferrer?: boolean; chainvineId?: string; + isEmailVerified?: boolean; } export interface IPassportInfo { diff --git a/src/components/InputUserEmailVerify.tsx b/src/components/InputUserEmailVerify.tsx new file mode 100644 index 0000000000..4d05855d49 --- /dev/null +++ b/src/components/InputUserEmailVerify.tsx @@ -0,0 +1,670 @@ +import { + GLink, + IIconProps, + neutralColors, + semanticColors, + SublineBold, + FlexCenter, + brandColors, + Flex, + IconEmptyCircle, + IconCheckCircleFilled, + IconAlertCircle, +} from '@giveth/ui-design-system'; +import React, { + forwardRef, + InputHTMLAttributes, + ReactElement, + useCallback, + useId, + useRef, + useState, +} from 'react'; +import styled, { css } from 'styled-components'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { EInputValidation, IInputValidation } from '@/types/inputValidation'; +import InputStyled from './styled-components/Input'; +import { getTextWidth } from '@/helpers/text'; +import { + inputSizeToFontSize, + inputSizeToPaddingLeft, + inputSizeToVerticalPadding, +} from '@/helpers/styledComponents'; +import { Spinner } from './Spinner'; +import { useProfileContext } from '@/context/profile.context'; +import { + SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, + SEND_USER_CONFIRMATION_CODE_FLOW, +} from '@/apollo/gql/gqlUser'; +import { client } from '@/apollo/apolloClient'; +import { showToastError } from '@/lib/helpers'; +import type { + DeepRequired, + FieldError, + FieldErrorsImpl, + Merge, + RegisterOptions, + UseFormRegister, +} from 'react-hook-form'; +export enum InputSize { + SMALL, + MEDIUM, + LARGE, +} + +interface IInputLabelProps { + $required?: boolean; + $disabled?: boolean; +} + +interface IInput extends InputHTMLAttributes { + label?: string; + caption?: string; + isValidating?: boolean; + size?: InputSize; + LeftIcon?: ReactElement; + error?: ICustomInputError; + suffix?: ReactElement; +} + +interface ICustomInputError { + message?: string; +} + +interface IInputWithRegister extends IInput { + register: UseFormRegister; + registerName: string; + registerOptions?: RegisterOptions; + error?: + | FieldError + | undefined + | ICustomInputError + | Merge>>>; +} + +const InputSizeToLinkSize = (size: InputSize) => { + switch (size) { + case InputSize.SMALL: + return 'Tiny'; + case InputSize.MEDIUM: + return 'Small'; + case InputSize.LARGE: + return 'Medium'; + default: + return 'Small'; + } +}; + +type InputType = + | IInputWithRegister + | ({ + registerName?: never; + register?: never; + registerOptions?: never; + } & IInput); + +interface IExtendedInputLabelProps extends IInputLabelProps { + $validation?: EInputValidation; +} + +const InputUserEmailVerify = forwardRef( + (props, inputRef) => { + const { formatMessage } = useIntl(); + const { user, updateUser } = useProfileContext(); + + const [email, setEmail] = useState(user.email); + const [verified, setVerified] = useState(user.isEmailVerified); + const [disableVerifyButton, setDisableVerifyButton] = useState( + !user.isEmailVerified && !user.email, + ); + const [isVerificationProcess, setIsVerificationProcess] = + useState(false); + const [inputDescription, setInputDescription] = useState( + verified + ? formatMessage({ + id: 'label.email_already_verified', + }) + : formatMessage({ + id: 'label.email_used', + }), + ); + const codeInputRef = useRef(null); + + const { + label, + size = InputSize.MEDIUM, + disabled, + LeftIcon, + register, + registerName, + registerOptions = { required: false }, + error, + maxLength, + value, + isValidating, + suffix, + className, + ...rest + } = props; + const id = useId(); + const canvasRef = useRef(); + + const [validationStatus, setValidationStatus] = useState( + !error || isValidating + ? EInputValidation.NORMAL + : EInputValidation.ERROR, + ); + + const [validationCodeStatus, setValidationCodeStatus] = useState( + EInputValidation.SUCCESS, + ); + const [disableCodeVerifyButton, setDisableCodeVerifyButton] = + useState(true); + + // const inputRef = useRef(null); + + const calcLeft = useCallback(() => { + if ( + suffix && + !canvasRef.current && + typeof document !== 'undefined' + ) { + canvasRef.current = document.createElement('canvas'); + } + if (canvasRef.current) { + const width = getTextWidth( + value?.toString() || '', + `normal ${inputSizeToFontSize(size)}px Red Hat Text`, + canvasRef.current, + ); + return inputSizeToPaddingLeft(size, !!LeftIcon) + width; + } + return 15; + }, [suffix, value, size, LeftIcon]); + + const { ref = undefined, ...restRegProps } = + registerName && register + ? register(registerName, registerOptions) + : {}; + + // Setup label button on condition + let labelButton = verified + ? formatMessage({ + id: 'label.email_verified', + }) + : formatMessage({ + id: 'label.email_verify', + }); + + // Enable verification process "button" if email input value was empty and not verified yet + // and setup email if input value was changed and has more than 3 characters + const handleInputChange = (e: React.ChangeEvent) => { + if (e.target.value.length > 3) { + setEmail(e.target.value); + setDisableVerifyButton(false); + } else { + setDisableVerifyButton(true); + } + + // Check if user is changing email address + if (e.target.value !== user.email) { + setVerified(false); + } else if (e.target.value !== user.email && user.isEmailVerified) { + setVerified(true); + } + }; + + // Verification email handler, it will be called on button click + // It will send request to backend to check if email exists and if it's not verified yet + // or email is already exist on another user account + // If email isn't verified it will send email with verification code to user + const verificationEmailHandler = async () => { + try { + const { data } = await client.mutate({ + mutation: SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, + variables: { + email: email, + }, + }); + + if (data.sendUserEmailConfirmationCodeFlow === 'EMAIL_EXIST') { + setValidationStatus(EInputValidation.WARNING); + setDisableVerifyButton(true); + setInputDescription( + formatMessage({ + id: 'label.email_used_another', + }), + ); + } + + if ( + data.sendUserEmailConfirmationCodeFlow === + 'VERIFICATION_SENT' + ) { + setIsVerificationProcess(true); + setValidationStatus(EInputValidation.NORMAL); + setInputDescription( + formatMessage({ + id: 'label.email_used', + }), + ); + } + } catch (error) { + if (error instanceof Error) { + showToastError(error.message); + } + console.log(error); + } + }; + + // Verification code handler, it will be called on button click + const handleInputCodeChange = ( + e: React.ChangeEvent, + ) => { + const value = e.target.value; + value.length >= 5 + ? setDisableCodeVerifyButton(false) + : setDisableCodeVerifyButton(true); + }; + + // Sent verification code to backend to check if it's correct + const handleButtonCodeChange = async () => { + try { + const { data } = await client.mutate({ + mutation: SEND_USER_CONFIRMATION_CODE_FLOW, + variables: { + verifyCode: codeInputRef.current?.value, + email: email, + }, + }); + + if ( + data.sendUserConfirmationCodeFlow === 'VERIFICATION_SUCCESS' + ) { + // Reset states + setIsVerificationProcess(false); + setDisableCodeVerifyButton(true); + setVerified(true); + setValidationCodeStatus(EInputValidation.SUCCESS); + + // Update user data + updateUser({ + email: email, + isEmailVerified: true, + }); + } + } catch (error) { + if (error instanceof Error) { + showToastError(error.message); + } + console.log(error); + } + }; + + return ( + + {label && ( + + )} + + {LeftIcon && ( + + {LeftIcon} + + )} + { + ref !== undefined && ref(e); + if (inputRef) (inputRef as any).current = e; + }} + data-testid='styled-input' + onChange={handleInputChange} + /> + + {suffix} + + {!isVerificationProcess && ( + + + + {!verified && } + {verified && } + {labelButton} + + + + )} + + {isValidating && } + {maxLength && ( + + {value ? String(value)?.length : 0}/{maxLength} + + )} + + + {error?.message ? ( + + {error.message as string} + + ) : ( + + {inputDescription} + + )} + {isVerificationProcess && ( + + + + {formatMessage( + { + id: 'label.email_sent_to', + }, + { email }, + )} + + + + + + + + {!verified && } + {verified && } + + {formatMessage({ + id: 'label.email_confirm_code', + })} + + + + + + + ( + + ), + }} + /> + + + )} + + ); + }, +); + +InputUserEmailVerify.displayName = 'Input'; + +const Absolute = styled(FlexCenter)` + position: absolute; + right: 10px; + top: 0; + bottom: 0; +`; + +const CharLength = styled(SublineBold)` + display: flex; + justify-content: center; + align-items: center; + font-size: 12px; + background: ${neutralColors.gray[300]}; + color: ${neutralColors.gray[700]}; + font-weight: 500; + border-radius: 64px; + width: 52px; + height: 30px; + margin-right: 6px; +`; + +const InputContainer = styled.div` + flex: 1; +`; + +const InputLabel = styled(GLink)` + padding-bottom: 4px; + color: ${props => + props.$validation === EInputValidation.ERROR + ? semanticColors.punch[600] + : neutralColors.gray[900]}; + &::after { + content: '*'; + display: ${props => (props.$required ? 'inline-block' : 'none')}; + padding: 0 4px; + color: ${semanticColors.punch[500]}; + } +`; + +const InputDesc = styled(GLink)<{ $validationstatus: EInputValidation }>` + padding-top: 4px; + color: ${props => { + switch (props.$validationstatus) { + case EInputValidation.NORMAL: + return neutralColors.gray[900]; + case EInputValidation.WARNING: + return semanticColors.golden[600]; + case EInputValidation.ERROR: + return semanticColors.punch[500]; + case EInputValidation.SUCCESS: + return semanticColors.jade[500]; + default: + return neutralColors.gray[300]; + } + }}; + display: block; +`; + +const InputValidation = styled(GLink)` + padding-top: 4px; + display: block; + color: ${props => { + switch (props.$validation) { + case EInputValidation.NORMAL: + return neutralColors.gray[900]; + case EInputValidation.WARNING: + return semanticColors.golden[600]; + case EInputValidation.ERROR: + return semanticColors.punch[500]; + case EInputValidation.SUCCESS: + return semanticColors.jade[500]; + default: + return neutralColors.gray[300]; + } + }}; +`; + +const InputWrapper = styled.div` + position: relative; + display: flex; +`; + +interface IInputWrapper { + $inputSize: InputSize; +} + +const LeftIconWrapper = styled.div` + position: absolute; + transform: translateY(-50%); + + border-right: 1px solid ${neutralColors.gray[400]}; + top: 50%; + left: 0; + overflow: hidden; + ${props => { + switch (props.$inputSize) { + case InputSize.SMALL: + return css` + width: 28px; + height: 16px; + padding-left: 8px; + `; + case InputSize.MEDIUM: + return css` + width: 36px; + height: 24px; + padding-top: 4px; + padding-left: 16px; + `; + case InputSize.LARGE: + return css` + width: 36px; + height: 24px; + padding-top: 4px; + padding-left: 16px; + `; + } + }} + padding-right: 4px; +`; + +const SuffixWrapper = styled.span` + position: absolute; + /* width: 16px; + height: 16px; */ +`; + +type VerifyInputButtonWrapperProps = { + $verified?: boolean; +}; + +const VerifyInputButtonWrapper = styled.button` + outline: none; + cursor: pointer; + background-color: ${({ $verified }) => + $verified ? 'transparent' : brandColors.giv[50]}; + border: 1px solid + ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[50]}; + border-radius: 8px; + padding: 3px 8px; + span { + color: ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[500]}; + font-size: 10px; + font-weight: 400; + line-height: 13.23px; + text-align: left; + } + svg { + color: ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[500]}; + } + &:disabled { + opacity: 0.5; + } +`; + +const ValidationCode = styled(Flex)` + flex-direction: column; + margin-top: 30px; + margin-bottom: 25px; +`; + +const EmailSentNotification = styled(Flex)` + width: 100%; + margin-bottom: 20px; + border: 1px solid ${brandColors.giv[200]}; + padding: 16px; + border-radius: 8px; + font-size: 12px; + font-weight: 400; + line-height: 15.88px; + text-align: left; + color: ${brandColors.giv[500]}; + svg { + color: ${brandColors.giv[500]}; + } +`; + +const InputCodeDesc = styled(GLink)` + padding-top: 4px; + font-size: 0.625rem; + line-height: 132%; + & button { + background: none; + border: none; + padding: 0; + color: ${brandColors.pinky[400]}; + font-size: 0.625rem; + line-height: 132%; + cursor: pointer; + } +`; + +export default InputUserEmailVerify; diff --git a/src/components/modals/EditUserModal.tsx b/src/components/modals/EditUserModal.tsx index 341d6bdcad..64f7dead4f 100644 --- a/src/components/modals/EditUserModal.tsx +++ b/src/components/modals/EditUserModal.tsx @@ -23,6 +23,7 @@ import { requiredOptions, validators } from '@/lib/constants/regex'; import { useModalAnimation } from '@/hooks/useModalAnimation'; import useUpload from '@/hooks/useUpload'; import { useGeneralWallet } from '@/providers/generalWalletProvider'; +import InputUserEmailVerify from '../InputUserEmailVerify'; interface IEditUserModal extends IModal { user: IUser; @@ -164,28 +165,36 @@ const EditUserModal = ({
- {inputFields.map(field => ( - - ))} - + ), + }} + /> + + + + + {formatMessage({ + id: 'label.email_confirm_code', + })} + + + + )} Where are you?
@@ -178,7 +436,7 @@ const InfoStep: FC = ({ setStep }) => { @@ -208,4 +466,102 @@ const SectionHeader = styled(H6)` border-bottom: 1px solid ${neutralColors.gray[400]}; `; +type VerifyInputButtonWrapperProps = { + $verified?: boolean; +}; + +const VerifyInputButtonWrapper = styled.button` + outline: none; + cursor: pointer; + margin-top: 24px; + background-color: ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[500]}; + border: 1px solid + ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[50]}; + border-radius: 8px; + padding: 20px 20px; + color: #ffffff; + font-size: 16px; + font-weight: 500; + line-height: 13.23px; + text-align: left; + &:hover { + opacity: 0.85; + } + &:disabled { + opacity: 0.5; + } +`; + +const VerifyCodeButtonWrapper = styled.button` + outline: none; + cursor: pointer; + margin-top: 48px; + background-color: ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[500]}; + border: 1px solid + ${({ $verified }) => + $verified ? semanticColors.jade[500] : brandColors.giv[50]}; + border-radius: 8px; + padding: 20px 20px; + color: #ffffff; + font-size: 16px; + font-weight: 500; + line-height: 13.23px; + text-align: left; + &:hover { + opacity: 0.85; + } + &:disabled { + opacity: 0.5; + } +`; + +const EmailSentNotification = styled(Flex)` + width: 100%; + margin-top: 20px; + margin-bottom: 20px; + border: 1px solid ${brandColors.giv[200]}; + padding: 16px; + border-radius: 8px; + font-size: 1em; + font-weight: 400; + line-height: 15.88px; + text-align: left; + color: ${brandColors.giv[500]}; + svg { + color: ${brandColors.giv[500]}; + } +`; + +const InputLabel = styled(GLink)` + padding-bottom: 4px; + color: ${props => + props.$validation === EInputValidation.ERROR + ? semanticColors.punch[600] + : neutralColors.gray[900]}; + &::after { + content: '*'; + display: ${props => (props.$required ? 'inline-block' : 'none')}; + padding: 0 4px; + color: ${semanticColors.punch[500]}; + } +`; + +const InputCodeDesc = styled(GLink)` + padding-top: 4px; + font-size: 0.75rem; + line-height: 132%; + & button { + background: none; + border: none; + padding: 0; + color: ${brandColors.pinky[400]}; + font-size: 0.75rem; + line-height: 132%; + cursor: pointer; + } +`; + export default InfoStep; diff --git a/src/components/views/project/ProjectIndex.tsx b/src/components/views/project/ProjectIndex.tsx index 0e9cc06347..dd2c8e00ff 100644 --- a/src/components/views/project/ProjectIndex.tsx +++ b/src/components/views/project/ProjectIndex.tsx @@ -49,6 +49,7 @@ import Routes from '@/lib/constants/Routes'; import { ChainType } from '@/types/config'; import { useAppSelector } from '@/features/hooks'; import { EndaomentProjectsInfo } from '@/components/views/project/EndaomentProjectsInfo'; +import VerifyEmailBanner from '../userProfile/VerifyEmailBanner'; const ProjectDonations = dynamic( () => import('./projectDonations/ProjectDonations.index'), @@ -84,6 +85,7 @@ const ProjectIndex: FC = () => { hasActiveQFRound, isCancelled, isAdmin, + isAdminEmailVerified, isLoading, } = useProjectContext(); @@ -134,6 +136,7 @@ const ProjectIndex: FC = () => { return ( + {!isAdminEmailVerified && } {hasActiveQFRound && !isOnSolana && } {title && `${title} |`} Giveth diff --git a/src/components/views/project/projectActionCard/AdminActions.tsx b/src/components/views/project/projectActionCard/AdminActions.tsx index d4c59c6b13..35abcfc87b 100644 --- a/src/components/views/project/projectActionCard/AdminActions.tsx +++ b/src/components/views/project/projectActionCard/AdminActions.tsx @@ -9,6 +9,7 @@ import { Flex, FlexCenter, IconArrowDownCircle16, + semanticColors, } from '@giveth/ui-design-system'; import React, { FC, useState } from 'react'; import { useIntl } from 'react-intl'; @@ -55,6 +56,8 @@ export const AdminActions = () => { const { switchChain } = useSwitchChain(); const chainId = chain?.id; + const { isAdminEmailVerified } = useProjectContext(); + const isVerificationDisabled = isGivbackEligible || verificationFormStatus === EVerificationStatus.SUBMITTED || @@ -132,86 +135,109 @@ export const AdminActions = () => { }; return !isMobile ? ( - - - {showVerificationModal && ( - setShowVerificationModal(false)} - /> - )} - {deactivateModal && ( - + <> + {!isAdminEmailVerified && ( + + {formatMessage({ + id: 'label.email_actions_text', + })} + )} - {showShareModal && ( - + - )} - {showClaimModal && ( - - )} - + {showVerificationModal && ( + setShowVerificationModal(false)} + /> + )} + {deactivateModal && ( + + )} + {showShareModal && ( + + )} + {showClaimModal && ( + + )} + + ) : ( - setShowMobileActionsModal(true)} - > -
Project Actions
- - {showMobileActionsModal && ( - - {options.map(option => ( - - - {option.icon} -
{option.label}
-
-
- ))} - {showVerificationModal && ( - setShowVerificationModal(false)} - /> - )} - {deactivateModal && ( - - )} - {showShareModal && ( - - )} -
- )} - {showClaimModal && ( - + <> + {!isAdminEmailVerified && ( + + {formatMessage({ + id: 'label.email_actions_text', + })} + )} -
+ setShowMobileActionsModal(true)} + $verified={isAdminEmailVerified} + > +
Project Actions
+ + {showMobileActionsModal && ( + + {options.map(option => ( + + + + {option.icon} + +
{option.label}
+
+
+ ))} + {showVerificationModal && ( + setShowVerificationModal(false)} + /> + )} + {deactivateModal && ( + + )} + {showShareModal && ( + + )} +
+ )} + {showClaimModal && ( + + )} +
+ ); }; @@ -232,18 +258,26 @@ const MobileActionsModal: FC = ({ ); }; -const Wrapper = styled.div` +interface WrapperProps { + $verified: boolean; +} + +const Wrapper = styled.div` order: 1; margin-bottom: 16px; + opacity: ${({ $verified }) => ($verified ? 1 : 0.5)}; + pointer-events: ${({ $verified }) => ($verified ? 'auto' : 'none')}; ${mediaQueries.tablet} { margin-bottom: 5px; order: unset; } `; -const MobileWrapper = styled(FlexCenter)` +const MobileWrapper = styled(FlexCenter)` padding: 10px 16px; background-color: ${neutralColors.gray[300]}; + opacity: ${({ $verified }) => ($verified ? 1 : 0.5)}; + pointer-events: ${({ $verified }) => ($verified ? 'auto' : 'none')}; border-radius: 8px; `; @@ -251,3 +285,9 @@ const MobileActionModalItem = styled(Flex)` padding: 16px 24px; border-bottom: ${neutralColors.gray[400]} 1px solid; `; + +const VerifyNotification = styled.div` + font-size: 14px; + text-align: center; + color: ${semanticColors.golden[600]}; +`; diff --git a/src/components/views/userProfile/VerifyEmailBanner.tsx b/src/components/views/userProfile/VerifyEmailBanner.tsx new file mode 100644 index 0000000000..ea992647a8 --- /dev/null +++ b/src/components/views/userProfile/VerifyEmailBanner.tsx @@ -0,0 +1,59 @@ +import styled from 'styled-components'; +import { useRouter } from 'next/router'; +import { brandColors, FlexCenter } from '@giveth/ui-design-system'; +import { FormattedMessage } from 'react-intl'; +import Routes from '@/lib/constants/Routes'; + +const VerifyEmailBanner = () => { + const router = useRouter(); + return ( + + + ( + + ), + }} + /> + + + ); +}; + +const PStyled = styled.div` + display: flex; + gap: 4px; + @media (max-width: 768px) { + flex-direction: column; + } + + & button { + background: none; + border: none; + padding: 0; + font-size: 16px; + color: ${brandColors.giv[500]}; + cursor: pointer; + } +`; + +const Wrapper = styled(FlexCenter)` + flex-wrap: wrap; + padding: 16px; + text-align: center; + gap: 4px; + background: ${brandColors.mustard[200]}; + z-index: 99; + position: sticky; +`; + +export default VerifyEmailBanner; diff --git a/src/components/views/userProfile/projectsTab/ProfileProjectsTab.tsx b/src/components/views/userProfile/projectsTab/ProfileProjectsTab.tsx index 388a42d693..f41d40c928 100644 --- a/src/components/views/userProfile/projectsTab/ProfileProjectsTab.tsx +++ b/src/components/views/userProfile/projectsTab/ProfileProjectsTab.tsx @@ -49,7 +49,7 @@ const ProfileProjectsTab: FC = () => { )} )} - + {!isLoading && data?.totalCount === 0 ? ( = () => { ); }; -export const ProjectsContainer = styled.div` +interface ProjectsContainerProps { + $verified: boolean; +} + +export const ProjectsContainer = styled.div` margin-bottom: 40px; + background-color: ${({ $verified }) => + $verified ? 'transparent' : '#f0f0f0'}; + opacity: ${({ $verified }) => ($verified ? 1 : 0.5)}; + pointer-events: ${({ $verified }) => ($verified ? 'auto' : 'none')}; `; export const Loading = styled(Flex)` diff --git a/src/context/profile.context.tsx b/src/context/profile.context.tsx index 51d4283cfe..2b5ebaafb4 100644 --- a/src/context/profile.context.tsx +++ b/src/context/profile.context.tsx @@ -12,12 +12,14 @@ interface ProfileContext { user: IUser; myAccount: boolean; givpowerBalance: string; + updateUser: (updatedUser: Partial) => void; } const ProfileContext = createContext({ user: {} as IUser, myAccount: false, givpowerBalance: '0', + updateUser: () => {}, }); ProfileContext.displayName = 'ProfileContext'; @@ -27,9 +29,18 @@ export const ProfileProvider = (props: { myAccount: boolean; children: ReactNode; }) => { - const { user, myAccount, children } = props; + const { user: initialUser, myAccount, children } = props; + const [user, setUser] = useState(initialUser); const [balance, setBalance] = useState('0'); + // Update user data + const updateUser = (updatedUser: Partial) => { + setUser(prevUser => ({ + ...prevUser, + ...updatedUser, + })); + }; + useEffect(() => { const fetchTotal = async () => { try { @@ -52,6 +63,7 @@ export const ProfileProvider = (props: { user, myAccount, givpowerBalance: balance, + updateUser, }} > {children} diff --git a/src/context/project.context.tsx b/src/context/project.context.tsx index 925bb815aa..0b2e95ecee 100644 --- a/src/context/project.context.tsx +++ b/src/context/project.context.tsx @@ -56,6 +56,7 @@ interface IProjectContext { isActive: boolean; isDraft: boolean; isAdmin: boolean; + isAdminEmailVerified: boolean; hasActiveQFRound: boolean; totalDonationsCount: number; isCancelled: boolean; @@ -73,6 +74,7 @@ const ProjectContext = createContext({ isActive: true, isDraft: false, isAdmin: false, + isAdminEmailVerified: false, hasActiveQFRound: false, totalDonationsCount: 0, isCancelled: false, @@ -110,6 +112,8 @@ export const ProjectProvider = ({ user?.walletAddress, ); + const isAdminEmailVerified = !!(isAdmin && user?.isEmailVerified); + const hasActiveQFRound = hasActiveRound(projectData?.qfRounds); const fetchProjectBySlug = useCallback(async () => { @@ -313,6 +317,7 @@ export const ProjectProvider = ({ isActive, isDraft, isAdmin, + isAdminEmailVerified, hasActiveQFRound, totalDonationsCount, isCancelled, diff --git a/src/features/user/user.queries.ts b/src/features/user/user.queries.ts index 8705b6d20b..426e221b22 100644 --- a/src/features/user/user.queries.ts +++ b/src/features/user/user.queries.ts @@ -22,6 +22,7 @@ export const GET_USER_BY_ADDRESS = `query UserByAddress($address: String!) { isReferrer wasReferred activeQFMBDScore + isEmailVerified } }`;