From a2e672517f2fb9d37f4ebaf723a3c0f8c6be5eb9 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Fri, 8 Nov 2024 12:06:43 +0100 Subject: [PATCH 01/18] Feat/Allow project boost if project is vouched --- lang/ca.json | 1 + lang/en.json | 1 + lang/es.json | 1 + .../views/project/ProjectGIVbackToast.tsx | 25 ++++- .../projectActionCard/AdminActions.tsx | 103 ++++++++++-------- .../ProjectCardNotification.tsx | 45 ++++++++ 6 files changed, 128 insertions(+), 48 deletions(-) create mode 100644 src/components/views/project/projectActionCard/ProjectCardNotification.tsx diff --git a/lang/ca.json b/lang/ca.json index 7023c9de42..e0184160d2 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -1678,6 +1678,7 @@ "project.givback_toast.title.verified_public_2": " del valor de la teva donació!", "project.givback_toast.title.verified_public_3": "Rep recompenses de fins a {percent}%", "project.givback_toast.title.verified_public_not_eligible": "Impulsa aquest projecte amb GIVpower!", + "project.givback_toast.complete_eligibility": "Completa el teu formulari d’elegibilitat per als GIVbacks", "projects_all": "Tots els Projectes", "projects_all_desc": "SUPORT A PROJECTES GLOBALS DE BÉS PÚBLICS, SOSTENIBILITAT I REGENERACIÓ AMB CRYPTODONACIONS", "projects_art-and-culture": "Art i Cultura", diff --git a/lang/en.json b/lang/en.json index 552db6df57..f9e629f0f7 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1679,6 +1679,7 @@ "project.givback_toast.title.verified_public_2": " of your donation value!", "project.givback_toast.title.verified_public_3": "Get rewarded with up to {percent}%", "project.givback_toast.title.verified_public_not_eligible": "Boost this project with GIVpower!", + "project.givback_toast.complete_eligibility": "Complete your GIVbacks Eligibility form", "projects_all": "All Projects", "projects_all_desc": "SUPPORT GLOBAL PROJECTS IN PUBLIC GOODS, SUSTAINABILITY, AND REGENERATION WITH CRYPTO DONATIONS", "projects_art-and-culture": "Art & Culture", diff --git a/lang/es.json b/lang/es.json index 410ca22183..602043da5e 100644 --- a/lang/es.json +++ b/lang/es.json @@ -1679,6 +1679,7 @@ "project.givback_toast.title.verified_public_2": " del valor de tu donación!", "project.givback_toast.title.verified_public_3": "Recibe recompensas de hasta {percent}%", "project.givback_toast.title.verified_public_not_eligible": "Boostea este proyecto con GIVpower!", + "project.givback_toast.complete_eligibility": "Complete su formulario de elegibilidad para GIVbacks", "projects_all": "Todos los proyectos", "projects_all_desc": "APOYE PROYECTOS GLOBALES EN BIENES PÚBLICOS, SOSTENIBILIDAD Y REGENERACIÓN CON CRIPTODONACIONES", "projects_art-and-culture": "Arte & Cultura", diff --git a/src/components/views/project/ProjectGIVbackToast.tsx b/src/components/views/project/ProjectGIVbackToast.tsx index 62e26d268b..37829103ee 100644 --- a/src/components/views/project/ProjectGIVbackToast.tsx +++ b/src/components/views/project/ProjectGIVbackToast.tsx @@ -53,7 +53,7 @@ const ProjectGIVbackToast = () => { const isOwnerVerifiedNotEligible = isVerified && isAdmin && !isGivbackEligible; - const color = isOwnerGivbackEligible + let color = isOwnerGivbackEligible ? semanticColors.golden[600] : neutralColors.gray[900]; const { formatMessage } = useIntl(); @@ -227,6 +227,29 @@ const ProjectGIVbackToast = () => { }); icon = ; link = links.CANCELLED_PROJECTS_DOCS; + } else if (isOwnerVerifiedNotEligible) { + title = formatMessage( + { + id: `${useIntlTitle}verified_owner`, + }, + { + percent: givbackFactorPercent, + value: GIVBACKS_DONATION_QUALIFICATION_VALUE_USD, + }, + ); + description = formatMessage({ + id: `${useIntlDescription}verified_owner_not_eligible`, + }); + color = semanticColors.golden[600]; + icon = ; + link = links.GIVPOWER_DOC; + Button = ( + } + /> + ); } else { title = formatMessage({ id: `${useIntlTitle}non_verified_owner`, diff --git a/src/components/views/project/projectActionCard/AdminActions.tsx b/src/components/views/project/projectActionCard/AdminActions.tsx index d4c59c6b13..34b28d2577 100644 --- a/src/components/views/project/projectActionCard/AdminActions.tsx +++ b/src/components/views/project/projectActionCard/AdminActions.tsx @@ -31,6 +31,7 @@ import { EVerificationStatus } from '@/apollo/types/types'; import ClaimRecurringDonationModal from '../../userProfile/projectsTab/ClaimRecurringDonationModal'; import config from '@/configuration'; import { findAnchorContractAddress } from '@/helpers/superfluid'; +import { ProjectCardNotification } from './ProjectCardNotification'; interface IMobileActionsModalProps { setShowModal: (value: boolean) => void; @@ -163,55 +164,63 @@ export const AdminActions = () => { project={project} /> )} + ) : ( - setShowMobileActionsModal(true)} - > -
Project Actions
- - {showMobileActionsModal && ( - - {options.map(option => ( - - - {option.icon} -
{option.label}
-
-
- ))} - {showVerificationModal && ( - setShowVerificationModal(false)} - /> - )} - {deactivateModal && ( - - )} - {showShareModal && ( - - )} -
- )} - {showClaimModal && ( - - )} -
+ <> + setShowMobileActionsModal(true)} + > +
Project Actions
+ + {showMobileActionsModal && ( + + {options.map(option => ( + + + + {option.icon} + +
{option.label}
+
+
+ ))} + {showVerificationModal && ( + setShowVerificationModal(false)} + /> + )} + {deactivateModal && ( + + )} + {showShareModal && ( + + )} +
+ )} + {showClaimModal && ( + + )} +
+ + ); }; diff --git a/src/components/views/project/projectActionCard/ProjectCardNotification.tsx b/src/components/views/project/projectActionCard/ProjectCardNotification.tsx new file mode 100644 index 0000000000..b2154971af --- /dev/null +++ b/src/components/views/project/projectActionCard/ProjectCardNotification.tsx @@ -0,0 +1,45 @@ +import { useIntl } from 'react-intl'; +import styled from 'styled-components'; + +import { + semanticColors, + IconAlertCircle16, + Flex, +} from '@giveth/ui-design-system'; +import { useProjectContext } from '@/context/project.context'; + +export const ProjectCardNotification = () => { + const { formatMessage } = useIntl(); + const { isAdmin, projectData } = useProjectContext(); + + const isVerified = projectData?.verified; + const isGivbackEligible = projectData?.isGivbackEligible; + + const isOwnerVerifiedNotEligible = + isVerified && isAdmin && !isGivbackEligible; + + if (!isOwnerVerifiedNotEligible) { + return null; + } + + return ( + + + + {formatMessage({ + id: `project.givback_toast.complete_eligibility`, + })} + + + ); +}; + +const ProjectCardNotificationWrapper = styled(Flex)` + align-items: center; + padding-top: 12px; + font-size: 14px; + color: ${semanticColors.golden[600]}; + span { + margin-left: 8px; + } +`; From 1a3e4206ce17d74fdbacf0ad7838e09184c4443d Mon Sep 17 00:00:00 2001 From: kkatusic Date: Mon, 11 Nov 2024 10:21:15 +0100 Subject: [PATCH 02/18] started verification of the user email --- src/apollo/gql/gqlUser.ts | 6 ++++++ src/components/modals/EditUserModal.tsx | 24 +++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/apollo/gql/gqlUser.ts b/src/apollo/gql/gqlUser.ts index b3dfd670fa..f0dbc63949 100644 --- a/src/apollo/gql/gqlUser.ts +++ b/src/apollo/gql/gqlUser.ts @@ -256,3 +256,9 @@ export const FETCH_USERS_GIVPOWER_BY_ADDRESS = ` } } }`; + +export const SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW = gql` + mutation SendUserEmailConfirmationCodeFlow($email: String!) { + sendUserEmailConfirmationCodeFlow(email: $email) + } +`; diff --git a/src/components/modals/EditUserModal.tsx b/src/components/modals/EditUserModal.tsx index 341d6bdcad..f229549964 100644 --- a/src/components/modals/EditUserModal.tsx +++ b/src/components/modals/EditUserModal.tsx @@ -8,7 +8,10 @@ import { captureException } from '@sentry/nextjs'; import { useForm } from 'react-hook-form'; import { Modal } from './Modal'; import { client } from '@/apollo/apolloClient'; -import { UPDATE_USER } from '@/apollo/gql/gqlUser'; +import { + SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, + UPDATE_USER, +} from '@/apollo/gql/gqlUser'; import { IUser } from '@/apollo/types/types'; import { gToast, ToastType } from '../toasts'; import { @@ -88,6 +91,20 @@ const EditUserModal = ({ } }; + const testMe = async () => { + try { + const { data } = await client.mutate({ + mutation: SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, + variables: { + email: 'test£gmail.com', + }, + }); + console.log(data); + } catch (error) { + console.log(error); + } + }; + const onSubmit = async (formData: Inputs) => { setIsLoading(true); try { @@ -163,6 +180,11 @@ const EditUserModal = ({
+
+ +
{inputFields.map(field => ( Date: Thu, 14 Nov 2024 14:57:22 +0100 Subject: [PATCH 03/18] stil testing --- src/components/modals/EditUserModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modals/EditUserModal.tsx b/src/components/modals/EditUserModal.tsx index f229549964..a50855f6fd 100644 --- a/src/components/modals/EditUserModal.tsx +++ b/src/components/modals/EditUserModal.tsx @@ -96,7 +96,7 @@ const EditUserModal = ({ const { data } = await client.mutate({ mutation: SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, variables: { - email: 'test£gmail.com', + email: 'kkatusic@gmail.com', }, }); console.log(data); From cd0c808aeb6a579fdf98aa75f64ecc214ee9d80c Mon Sep 17 00:00:00 2001 From: kkatusic Date: Fri, 15 Nov 2024 15:28:24 +0100 Subject: [PATCH 04/18] Fix/Mobile search --- src/components/SearchInput.tsx | 65 ++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/components/SearchInput.tsx b/src/components/SearchInput.tsx index 50751654e7..06314630b1 100644 --- a/src/components/SearchInput.tsx +++ b/src/components/SearchInput.tsx @@ -42,36 +42,49 @@ export const SearchInput: FC = ({ setTerm, className }) => { setValue(event.target.value); } + function handleFormSubmit(inputValue: string) { + if (inputValue.length > 2) { + setTerm(inputValue); + } + } + const [inputRef] = useFocus(); return ( - - - {value.length > 0 ? ( - { - setValue(''); - setTerm(''); - }} - > - - - ) : ( - - - - )} - + { + e.preventDefault(); + handleFormSubmit(value); + }} + > + + + {value.length > 0 ? ( + { + setValue(''); + setTerm(''); + }} + > + + + ) : ( + + + + )} + + {value.length > 0 ? ( value.length > 2 ? ( From 89c774704435a0ef90e7527bf6e0567ab3b006d4 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Sat, 16 Nov 2024 18:13:07 +0100 Subject: [PATCH 05/18] created separated input for user email verification --- lang/ca.json | 7 +- lang/en.json | 9 +- lang/es.json | 3 + src/apollo/types/types.ts | 1 + src/components/InputUserEmailVerify.tsx | 443 ++++++++++++++++++++++++ src/components/modals/EditUserModal.tsx | 59 ++-- src/features/user/user.queries.ts | 1 + 7 files changed, 495 insertions(+), 28 deletions(-) create mode 100644 src/components/InputUserEmailVerify.tsx diff --git a/lang/ca.json b/lang/ca.json index 7023c9de42..1d185cc249 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -383,6 +383,9 @@ "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.enable_change": "Habilita el canvi", "label.enable_recurring_donations": "Habilitar Donacions Recurrents", "label.ends_on": "acaba el", @@ -610,7 +613,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 +1207,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..45c6487c75 100644 --- a/lang/en.json +++ b/lang/en.json @@ -381,8 +381,11 @@ "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.enable_change": "Enable Change", "label.enable_recurring_donations": "Enable Recurring Donations", "label.ends_on": "ends on", @@ -610,7 +613,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 +1207,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..9438eaaed5 100644 --- a/lang/es.json +++ b/lang/es.json @@ -383,6 +383,9 @@ "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.enable_change": "Ayuda al Cambio", "label.enable_recurring_donations": "Habilitar Donaciones Recurrentes", "label.ends_on": "termina el", 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..a060bfbad5 --- /dev/null +++ b/src/components/InputUserEmailVerify.tsx @@ -0,0 +1,443 @@ +import { + GLink, + IIconProps, + neutralColors, + semanticColors, + SublineBold, + FlexCenter, + brandColors, + Flex, + IconEmptyCircle, + IconCheckCircleFilled, +} from '@giveth/ui-design-system'; +import React, { + forwardRef, + InputHTMLAttributes, + ReactElement, + useCallback, + useId, + useRef, + useState, +} from 'react'; +import styled, { css } from 'styled-components'; +import { 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 } from '@/apollo/gql/gqlUser'; +import { client } from '@/apollo/apolloClient'; +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); + +const InputUserEmailVerify = forwardRef( + (props, inputRef) => { + const { formatMessage } = useIntl(); + const { user } = 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 { + label, + caption, + 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 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); + } + }; + + const verificationEmailHandler = async () => { + console.log({ email }); + try { + const { data } = await client.mutate({ + mutation: SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, + variables: { + email: 'kkatusic@gmail.com', + }, + }); + if (data.sendUserEmailConfirmationCodeFlow === 'EMAIL_EXIST') { + setValidationStatus(EInputValidation.WARNING); + setDisableVerifyButton(true); + } + } catch (error) { + 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 && ( + + )} + {!isVerificationProcess && verified && ( + + )} + {!!isVerificationProcess && ( + + )} + {labelButton} + + + + + {isValidating && } + {maxLength && ( + + {value ? String(value)?.length : 0}/{maxLength} + + )} + + + {error?.message ? ( + + {error.message as string} + + ) : ( + + {verified && + formatMessage({ + id: 'label.email_already_verified', + })} + //hidden char + )} + + ); + }, +); + +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.$disabled ? neutralColors.gray[600] : neutralColors.gray[900]}; + &::after { + content: '*'; + display: ${props => (props.$required ? 'inline-block' : 'none')}; + padding: 0 4px; + color: ${semanticColors.punch[500]}; + } +`; + +const InputDesc = styled(GLink)` + padding-top: 4px; + color: ${neutralColors.gray[900]}; + 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; + } +`; + +export default InputUserEmailVerify; diff --git a/src/components/modals/EditUserModal.tsx b/src/components/modals/EditUserModal.tsx index a50855f6fd..ef25834db4 100644 --- a/src/components/modals/EditUserModal.tsx +++ b/src/components/modals/EditUserModal.tsx @@ -26,6 +26,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; @@ -186,28 +187,36 @@ const EditUserModal = ({ - {inputFields.map(field => ( - - ))} - ", + "label.email_confirm_code": "Confirma el codi", "label.enable_change": "Habilita el canvi", "label.enable_recurring_donations": "Habilitar Donacions Recurrents", "label.ends_on": "acaba el", diff --git a/lang/en.json b/lang/en.json index 45c6487c75..da6ef53f22 100644 --- a/lang/en.json +++ b/lang/en.json @@ -386,6 +386,12 @@ "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.enable_change": "Enable Change", "label.enable_recurring_donations": "Enable Recurring Donations", "label.ends_on": "ends on", diff --git a/lang/es.json b/lang/es.json index 9438eaaed5..dd18a568a4 100644 --- a/lang/es.json +++ b/lang/es.json @@ -386,6 +386,12 @@ "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.enable_change": "Ayuda al Cambio", "label.enable_recurring_donations": "Habilitar Donaciones Recurrentes", "label.ends_on": "termina el", diff --git a/src/apollo/gql/gqlUser.ts b/src/apollo/gql/gqlUser.ts index f0dbc63949..17b0d819f8 100644 --- a/src/apollo/gql/gqlUser.ts +++ b/src/apollo/gql/gqlUser.ts @@ -262,3 +262,9 @@ export const SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW = gql` sendUserEmailConfirmationCodeFlow(email: $email) } `; + +export const SEND_USER_CONFIRMATION_CODE_FLOW = gql` + mutation SendUserConfirmationCodeFlow($verifyCode: String!) { + sendUserConfirmationCodeFlow(verifyCode: $verifyCode) + } +`; diff --git a/src/components/InputUserEmailVerify.tsx b/src/components/InputUserEmailVerify.tsx index a060bfbad5..f46f7b301a 100644 --- a/src/components/InputUserEmailVerify.tsx +++ b/src/components/InputUserEmailVerify.tsx @@ -9,6 +9,7 @@ import { Flex, IconEmptyCircle, IconCheckCircleFilled, + IconAlertCircle, } from '@giveth/ui-design-system'; import React, { forwardRef, @@ -20,7 +21,7 @@ import React, { useState, } from 'react'; import styled, { css } from 'styled-components'; -import { useIntl } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { EInputValidation, IInputValidation } from '@/types/inputValidation'; import InputStyled from './styled-components/Input'; import { getTextWidth } from '@/helpers/text'; @@ -31,7 +32,10 @@ import { } from '@/helpers/styledComponents'; import { Spinner } from './Spinner'; import { useProfileContext } from '@/context/profile.context'; -import { SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW } from '@/apollo/gql/gqlUser'; +import { + SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, + SEND_USER_CONFIRMATION_CODE_FLOW, +} from '@/apollo/gql/gqlUser'; import { client } from '@/apollo/apolloClient'; import type { DeepRequired, @@ -98,6 +102,10 @@ type InputType = registerOptions?: never; } & IInput); +interface IExtendedInputLabelProps extends IInputLabelProps { + $validation?: EInputValidation; +} + const InputUserEmailVerify = forwardRef( (props, inputRef) => { const { formatMessage } = useIntl(); @@ -110,10 +118,19 @@ const InputUserEmailVerify = forwardRef( ); 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, - caption, size = InputSize.MEDIUM, disabled, LeftIcon, @@ -137,6 +154,12 @@ const InputUserEmailVerify = forwardRef( : EInputValidation.ERROR, ); + const [validationCodeStatus, setValidationCodeStatus] = useState( + EInputValidation.SUCCESS, + ); + const [disableCodeVerifyButton, setDisableCodeVerifyButton] = + useState(true); + // const inputRef = useRef(null); const calcLeft = useCallback(() => { @@ -183,18 +206,66 @@ const InputUserEmailVerify = forwardRef( } }; + // 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 () => { - console.log({ email }); try { const { data } = await client.mutate({ mutation: SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, variables: { - email: 'kkatusic@gmail.com', + 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); + } + } catch (error) { + 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, + }, + }); + + if ( + data.sendUserConfirmationCodeFlow === 'VERIFICATION_SUCCESS' + ) { + setIsVerificationProcess(false); + setDisableCodeVerifyButton(true); + setVerified(true); } } catch (error) { console.log(error); @@ -209,6 +280,11 @@ const InputUserEmailVerify = forwardRef( $disabled={disabled} size={InputSizeToLinkSize(size)} $required={Boolean(registerOptions.required)} + $validation={ + isVerificationProcess + ? EInputValidation.ERROR + : undefined + } > {label} @@ -221,13 +297,16 @@ const InputUserEmailVerify = forwardRef( )} { @@ -245,27 +324,22 @@ const InputUserEmailVerify = forwardRef( > {suffix} - - - - {!isVerificationProcess && !verified && ( - - )} - {!isVerificationProcess && verified && ( - - )} - {!!isVerificationProcess && ( - - )} - {labelButton} - - - + {!isVerificationProcess && ( + + + + {!verified && } + {verified && } + {labelButton} + + + + )} {isValidating && } {maxLength && ( @@ -283,12 +357,84 @@ const InputUserEmailVerify = forwardRef( {error.message as string} ) : ( - - {verified && - formatMessage({ - id: 'label.email_already_verified', - })} - //hidden char + + {inputDescription} + + )} + {isVerificationProcess && ( + + + + {formatMessage( + { + id: 'label.email_sent_to', + }, + { email }, + )} + + + + + + + + {!verified && } + {verified && } + + {formatMessage({ + id: 'label.email_confirm_code', + })} + + + + + + + ( + + ), + }} + /> + + )} ); @@ -322,10 +468,12 @@ const InputContainer = styled.div` flex: 1; `; -const InputLabel = styled(GLink)` +const InputLabel = styled(GLink)` padding-bottom: 4px; color: ${props => - props.$disabled ? neutralColors.gray[600] : neutralColors.gray[900]}; + props.$validation === EInputValidation.ERROR + ? semanticColors.punch[600] + : neutralColors.gray[900]}; &::after { content: '*'; display: ${props => (props.$required ? 'inline-block' : 'none')}; @@ -334,9 +482,22 @@ const InputLabel = styled(GLink)` } `; -const InputDesc = styled(GLink)` +const InputDesc = styled(GLink)<{ validationStatus: EInputValidation }>` padding-top: 4px; - color: ${neutralColors.gray[900]}; + 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; `; @@ -440,4 +601,41 @@ const VerifyInputButtonWrapper = styled.button` } `; +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; From 77373cd7a24e692f007b1961fc072e0761cef736 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Tue, 19 Nov 2024 14:31:22 +0100 Subject: [PATCH 07/18] completed all verification process and restriction --- lang/ca.json | 2 + lang/en.json | 2 + lang/es.json | 2 + pages/account.tsx | 2 + pages/project/[projectIdSlug]/index.tsx | 2 - src/apollo/gql/gqlUser.ts | 7 +- src/components/InputUserEmailVerify.tsx | 34 ++- src/components/modals/EditUserModal.tsx | 24 +-- src/components/views/project/ProjectIndex.tsx | 3 + .../projectActionCard/AdminActions.tsx | 196 +++++++++++------- .../views/userProfile/VerifyEmailBanner.tsx | 59 ++++++ .../projectsTab/ProfileProjectsTab.tsx | 12 +- src/context/profile.context.tsx | 14 +- src/context/project.context.tsx | 5 + 14 files changed, 252 insertions(+), 112 deletions(-) create mode 100644 src/components/views/userProfile/VerifyEmailBanner.tsx diff --git a/lang/ca.json b/lang/ca.json index fda44e8977..71a9150fcf 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -392,6 +392,8 @@ "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.enable_change": "Habilita el canvi", "label.enable_recurring_donations": "Habilitar Donacions Recurrents", "label.ends_on": "acaba el", diff --git a/lang/en.json b/lang/en.json index da6ef53f22..39961f8327 100644 --- a/lang/en.json +++ b/lang/en.json @@ -392,6 +392,8 @@ "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.enable_change": "Enable Change", "label.enable_recurring_donations": "Enable Recurring Donations", "label.ends_on": "ends on", diff --git a/lang/es.json b/lang/es.json index dd18a568a4..1a12d5fbdc 100644 --- a/lang/es.json +++ b/lang/es.json @@ -392,6 +392,8 @@ "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.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 17b0d819f8..975eb9da1b 100644 --- a/src/apollo/gql/gqlUser.ts +++ b/src/apollo/gql/gqlUser.ts @@ -264,7 +264,10 @@ export const SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW = gql` `; export const SEND_USER_CONFIRMATION_CODE_FLOW = gql` - mutation SendUserConfirmationCodeFlow($verifyCode: String!) { - sendUserConfirmationCodeFlow(verifyCode: $verifyCode) + mutation SendUserConfirmationCodeFlow( + $verifyCode: String! + $email: String! + ) { + sendUserConfirmationCodeFlow(verifyCode: $verifyCode, email: $email) } `; diff --git a/src/components/InputUserEmailVerify.tsx b/src/components/InputUserEmailVerify.tsx index f46f7b301a..4cc9845601 100644 --- a/src/components/InputUserEmailVerify.tsx +++ b/src/components/InputUserEmailVerify.tsx @@ -37,6 +37,7 @@ import { SEND_USER_CONFIRMATION_CODE_FLOW, } from '@/apollo/gql/gqlUser'; import { client } from '@/apollo/apolloClient'; +import { showToastError } from '@/lib/helpers'; import type { DeepRequired, FieldError, @@ -109,7 +110,7 @@ interface IExtendedInputLabelProps extends IInputLabelProps { const InputUserEmailVerify = forwardRef( (props, inputRef) => { const { formatMessage } = useIntl(); - const { user } = useProfileContext(); + const { user, updateUser } = useProfileContext(); const [email, setEmail] = useState(user.email); const [verified, setVerified] = useState(user.isEmailVerified); @@ -204,6 +205,13 @@ const InputUserEmailVerify = forwardRef( } else { setDisableVerifyButton(true); } + + // Check if user is changing email address + if (e.target.value !== user.email) { + setVerified(false); + } else { + setVerified(true); + } }; // Verification email handler, it will be called on button click @@ -234,6 +242,12 @@ const InputUserEmailVerify = forwardRef( 'VERIFICATION_SENT' ) { setIsVerificationProcess(true); + setValidationStatus(EInputValidation.NORMAL); + setInputDescription( + formatMessage({ + id: 'label.email_used', + }), + ); } } catch (error) { console.log(error); @@ -257,17 +271,29 @@ const InputUserEmailVerify = forwardRef( 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); } }; @@ -359,7 +385,7 @@ const InputUserEmailVerify = forwardRef( ) : ( {inputDescription} @@ -482,10 +508,10 @@ const InputLabel = styled(GLink)` } `; -const InputDesc = styled(GLink)<{ validationStatus: EInputValidation }>` +const InputDesc = styled(GLink)<{ $validationstatus: EInputValidation }>` padding-top: 4px; color: ${props => { - switch (props.validationStatus) { + switch (props.$validationstatus) { case EInputValidation.NORMAL: return neutralColors.gray[900]; case EInputValidation.WARNING: diff --git a/src/components/modals/EditUserModal.tsx b/src/components/modals/EditUserModal.tsx index ef25834db4..64f7dead4f 100644 --- a/src/components/modals/EditUserModal.tsx +++ b/src/components/modals/EditUserModal.tsx @@ -8,10 +8,7 @@ import { captureException } from '@sentry/nextjs'; import { useForm } from 'react-hook-form'; import { Modal } from './Modal'; import { client } from '@/apollo/apolloClient'; -import { - SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, - UPDATE_USER, -} from '@/apollo/gql/gqlUser'; +import { UPDATE_USER } from '@/apollo/gql/gqlUser'; import { IUser } from '@/apollo/types/types'; import { gToast, ToastType } from '../toasts'; import { @@ -92,20 +89,6 @@ const EditUserModal = ({ } }; - const testMe = async () => { - try { - const { data } = await client.mutate({ - mutation: SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, - variables: { - email: 'kkatusic@gmail.com', - }, - }); - console.log(data); - } catch (error) { - console.log(error); - } - }; - const onSubmit = async (formData: Inputs) => { setIsLoading(true); try { @@ -181,11 +164,6 @@ const EditUserModal = ({
-
- -
{inputFields.map(field => { // We load different input components for email becasue validation is different 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, From 681535cf8544f239f850e553f6f9851894c12ef3 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Wed, 20 Nov 2024 13:21:41 +0100 Subject: [PATCH 08/18] fixing small bugs bypassing first update of the user --- src/apollo/gql/gqlUser.ts | 2 ++ src/components/InputUserEmailVerify.tsx | 5 ++++- src/components/views/onboarding/InfoStep.tsx | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/apollo/gql/gqlUser.ts b/src/apollo/gql/gqlUser.ts index 975eb9da1b..d95f31deac 100644 --- a/src/apollo/gql/gqlUser.ts +++ b/src/apollo/gql/gqlUser.ts @@ -179,6 +179,7 @@ export const UPDATE_USER = gql` $firstName: String $avatar: String $newUser: Boolean + $isFirstUpdate: Boolean ) { updateUser( url: $url @@ -188,6 +189,7 @@ export const UPDATE_USER = gql` lastName: $lastName avatar: $avatar newUser: $newUser + isFirstUpdate: $isFirstUpdate ) } `; diff --git a/src/components/InputUserEmailVerify.tsx b/src/components/InputUserEmailVerify.tsx index 4cc9845601..4d05855d49 100644 --- a/src/components/InputUserEmailVerify.tsx +++ b/src/components/InputUserEmailVerify.tsx @@ -209,7 +209,7 @@ const InputUserEmailVerify = forwardRef( // Check if user is changing email address if (e.target.value !== user.email) { setVerified(false); - } else { + } else if (e.target.value !== user.email && user.isEmailVerified) { setVerified(true); } }; @@ -250,6 +250,9 @@ const InputUserEmailVerify = forwardRef( ); } } catch (error) { + if (error instanceof Error) { + showToastError(error.message); + } console.log(error); } }; diff --git a/src/components/views/onboarding/InfoStep.tsx b/src/components/views/onboarding/InfoStep.tsx index ceb5de5406..fc2bd04f8b 100644 --- a/src/components/views/onboarding/InfoStep.tsx +++ b/src/components/views/onboarding/InfoStep.tsx @@ -80,6 +80,7 @@ const InfoStep: FC = ({ setStep }) => { variables: { ...formData, newUser: true, + isFirstUpdate: true, }, }); if (response.updateUser) { From 035394a3f721a7caf81d5a4651e2c870a5b050ce Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 12:21:19 +0100 Subject: [PATCH 09/18] adding verification process to the new user update form and removing bypassing --- src/apollo/gql/gqlUser.ts | 2 - src/components/views/onboarding/InfoStep.tsx | 350 ++++++++++++++++++- 2 files changed, 345 insertions(+), 7 deletions(-) diff --git a/src/apollo/gql/gqlUser.ts b/src/apollo/gql/gqlUser.ts index d95f31deac..975eb9da1b 100644 --- a/src/apollo/gql/gqlUser.ts +++ b/src/apollo/gql/gqlUser.ts @@ -179,7 +179,6 @@ export const UPDATE_USER = gql` $firstName: String $avatar: String $newUser: Boolean - $isFirstUpdate: Boolean ) { updateUser( url: $url @@ -189,7 +188,6 @@ export const UPDATE_USER = gql` lastName: $lastName avatar: $avatar newUser: $newUser - isFirstUpdate: $isFirstUpdate ) } `; diff --git a/src/components/views/onboarding/InfoStep.tsx b/src/components/views/onboarding/InfoStep.tsx index fc2bd04f8b..80b89534d8 100644 --- a/src/components/views/onboarding/InfoStep.tsx +++ b/src/components/views/onboarding/InfoStep.tsx @@ -1,10 +1,25 @@ -import { FC, useEffect, useState } from 'react'; +import { FC, useEffect, useRef, useState } from 'react'; import { useMutation } from '@apollo/client'; -import { H6, neutralColors, Col, Row } from '@giveth/ui-design-system'; +import { + H6, + neutralColors, + Col, + Row, + brandColors, + semanticColors, + IconAlertCircle, + Flex, + GLink, +} from '@giveth/ui-design-system'; import styled from 'styled-components'; import { captureException } from '@sentry/nextjs'; import { useForm } from 'react-hook-form'; -import { UPDATE_USER } from '@/apollo/gql/gqlUser'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { + SEND_USER_CONFIRMATION_CODE_FLOW, + SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, + UPDATE_USER, +} from '@/apollo/gql/gqlUser'; import { SkipOnboardingModal } from '@/components/modals/SkipOnboardingModal'; import { gToast, ToastType } from '@/components/toasts'; import { @@ -21,6 +36,10 @@ import { setShowSignWithWallet } from '@/features/modal/modal.slice'; import { fetchUserByAddress } from '@/features/user/user.thunks'; import { requiredOptions, validators } from '@/lib/constants/regex'; import { useGeneralWallet } from '@/providers/generalWalletProvider'; +import { client } from '@/apollo/apolloClient'; +import { showToastError } from '@/lib/helpers'; +import InputStyled from '@/components/styled-components/Input'; +import { EInputValidation } from '@/types/inputValidation'; export interface IUserInfo { email: string; @@ -38,7 +57,17 @@ enum EUserInfo { URL = 'url', } +interface IInputLabelProps { + $required?: boolean; + $disabled?: boolean; +} + +interface IExtendedInputLabelProps extends IInputLabelProps { + $validation?: EInputValidation; +} + const InfoStep: FC = ({ setStep }) => { + const { formatMessage } = useIntl(); const [isLoading, setIsLoading] = useState(false); const [updateUser] = useMutation(UPDATE_USER); const [showModal, setShowModal] = useState(false); @@ -47,6 +76,141 @@ const InfoStep: FC = ({ setStep }) => { const dispatch = useAppDispatch(); const { isSignedIn, userData } = useAppSelector(state => state.user); + // States for email verification + const { userData: currentDBUser } = useAppSelector(state => state.user); + const [verified, setVerified] = useState( + currentDBUser?.isEmailVerified || false, + ); + const [disableVerifyButton, setDisableVerifyButton] = useState( + !currentDBUser?.isEmailVerified && !currentDBUser?.email, + ); + const [disableCodeVerifyButton, setDisableCodeVerifyButton] = + useState(true); + const [email, setEmail] = useState(currentDBUser?.email || ''); + const [isVerificationProcess, setIsVerificationProcess] = useState(false); + const [validationStatus, setValidationStatus] = useState( + EInputValidation.NORMAL, + ); + const [inputDescription, setInputDescription] = useState( + verified + ? formatMessage({ + id: 'label.email_already_verified', + }) + : formatMessage({ + id: 'label.email_used', + }), + ); + const codeInputRef = useRef(null); + const [validationCodeStatus, setValidationCodeStatus] = useState( + EInputValidation.SUCCESS, + ); + + // 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 !== currentDBUser?.email) { + setVerified(false); + } else if ( + e.target.value !== currentDBUser.email && + currentDBUser.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); + showToastError( + 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); + } + } catch (error) { + if (error instanceof Error) { + showToastError(error.message); + } + console.log(error); + } + }; + const { register, handleSubmit, @@ -80,7 +244,6 @@ const InfoStep: FC = ({ setStep }) => { variables: { ...formData, newUser: true, - isFirstUpdate: true, }, }); if (response.updateUser) { @@ -144,8 +307,87 @@ const InfoStep: FC = ({ setStep }) => { type='email' registerOptions={requiredOptions.email} error={errors.email} + caption={inputDescription} + onChange={handleInputChange} /> + {!isVerificationProcess && ( + + + {labelButton} + + + )} + {isVerificationProcess && ( + <> + + + + {formatMessage( + { + id: 'label.email_sent_to', + }, + { email }, + )} + + + + + + + ( + + ), + }} + /> + + + + + {formatMessage({ + id: 'label.email_confirm_code', + })} + + + + )} Where are you?
@@ -179,7 +421,7 @@ const InfoStep: FC = ({ setStep }) => { @@ -209,4 +451,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; From bfc5521a3382a8e99241f65461820afda549cade Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 14:16:06 +0100 Subject: [PATCH 10/18] fixing toast messages on user edit form --- lang/ca.json | 1 + lang/en.json | 1 + lang/es.json | 1 + src/components/views/onboarding/InfoStep.tsx | 23 ++++++++++++++++---- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lang/ca.json b/lang/ca.json index 71a9150fcf..ff79705c6c 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -394,6 +394,7 @@ "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", diff --git a/lang/en.json b/lang/en.json index 39961f8327..13e9659b3a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -394,6 +394,7 @@ "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", diff --git a/lang/es.json b/lang/es.json index 1a12d5fbdc..fc3a0c5389 100644 --- a/lang/es.json +++ b/lang/es.json @@ -394,6 +394,7 @@ "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/src/components/views/onboarding/InfoStep.tsx b/src/components/views/onboarding/InfoStep.tsx index 80b89534d8..dc0cc4cb9a 100644 --- a/src/components/views/onboarding/InfoStep.tsx +++ b/src/components/views/onboarding/InfoStep.tsx @@ -37,7 +37,6 @@ import { fetchUserByAddress } from '@/features/user/user.thunks'; import { requiredOptions, validators } from '@/lib/constants/regex'; import { useGeneralWallet } from '@/providers/generalWalletProvider'; import { client } from '@/apollo/apolloClient'; -import { showToastError } from '@/lib/helpers'; import InputStyled from '@/components/styled-components/Input'; import { EInputValidation } from '@/types/inputValidation'; @@ -151,10 +150,16 @@ const InfoStep: FC = ({ setStep }) => { if (data.sendUserEmailConfirmationCodeFlow === 'EMAIL_EXIST') { setValidationStatus(EInputValidation.WARNING); setDisableVerifyButton(true); - showToastError( + gToast( formatMessage({ id: 'label.email_used_another', }), + { + type: ToastType.DANGER, + title: formatMessage({ + id: 'label.email_error_verify', + }), + }, ); } @@ -171,7 +176,12 @@ const InfoStep: FC = ({ setStep }) => { } } catch (error) { if (error instanceof Error) { - showToastError(error.message); + gToast(error.message, { + type: ToastType.DANGER, + title: formatMessage({ + id: 'label.email_error_verify', + }), + }); } console.log(error); } @@ -205,7 +215,12 @@ const InfoStep: FC = ({ setStep }) => { } } catch (error) { if (error instanceof Error) { - showToastError(error.message); + gToast(error.message, { + type: ToastType.DANGER, + title: formatMessage({ + id: 'label.email_error_verify', + }), + }); } console.log(error); } From e8eaac8e91fa34645b23b67f174ab6c9e2cd6a11 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Mon, 25 Nov 2024 14:41:06 +0100 Subject: [PATCH 11/18] Fix/Email verification bugs --- lang/ca.json | 2 +- lang/en.json | 2 +- lang/es.json | 2 +- src/components/InputUserEmailVerify.tsx | 11 ++++++++++- src/components/modals/EditUserModal.tsx | 6 +++++- src/components/views/project/ProjectIndex.tsx | 2 +- 6 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lang/ca.json b/lang/ca.json index ff79705c6c..ee05908859 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -390,7 +390,7 @@ "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_get_resend": "No has rebut el correu electrònic? 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!", diff --git a/lang/en.json b/lang/en.json index 13e9659b3a..031b61d825 100644 --- a/lang/en.json +++ b/lang/en.json @@ -390,7 +390,7 @@ "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_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!", diff --git a/lang/es.json b/lang/es.json index fc3a0c5389..0ecd7837c8 100644 --- a/lang/es.json +++ b/lang/es.json @@ -390,7 +390,7 @@ "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_get_resend": "¿No recibiste el correo electrónico? 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!", diff --git a/src/components/InputUserEmailVerify.tsx b/src/components/InputUserEmailVerify.tsx index 4d05855d49..37244252ed 100644 --- a/src/components/InputUserEmailVerify.tsx +++ b/src/components/InputUserEmailVerify.tsx @@ -96,11 +96,14 @@ const InputSizeToLinkSize = (size: InputSize) => { }; type InputType = - | IInputWithRegister + | (IInputWithRegister & { + verifiedSaveButton?: (verified: boolean) => void; + }) | ({ registerName?: never; register?: never; registerOptions?: never; + verifiedSaveButton?: (verified: boolean) => void; } & IInput); interface IExtendedInputLabelProps extends IInputLabelProps { @@ -209,8 +212,13 @@ const InputUserEmailVerify = forwardRef( // Check if user is changing email address if (e.target.value !== user.email) { setVerified(false); + props.verifiedSaveButton && props.verifiedSaveButton(false); } else if (e.target.value !== user.email && user.isEmailVerified) { setVerified(true); + props.verifiedSaveButton && props.verifiedSaveButton(true); + } else if (e.target.value === user.email && user.isEmailVerified) { + setVerified(true); + props.verifiedSaveButton && props.verifiedSaveButton(true); } }; @@ -285,6 +293,7 @@ const InputUserEmailVerify = forwardRef( setIsVerificationProcess(false); setDisableCodeVerifyButton(true); setVerified(true); + props.verifiedSaveButton && props.verifiedSaveButton(true); setValidationCodeStatus(EInputValidation.SUCCESS); // Update user data diff --git a/src/components/modals/EditUserModal.tsx b/src/components/modals/EditUserModal.tsx index 64f7dead4f..235995dcff 100644 --- a/src/components/modals/EditUserModal.tsx +++ b/src/components/modals/EditUserModal.tsx @@ -58,6 +58,7 @@ const EditUserModal = ({ const { walletAddress: address } = useGeneralWallet(); const [updateUser] = useMutation(UPDATE_USER); + const [verified, setVerified] = useState(user.isEmailVerified); const { isAnimating, closeModal } = useModalAnimation(setShowModal); const onSaveAvatar = async () => { @@ -191,6 +192,9 @@ const EditUserModal = ({ register={register} error={(errors as any)[field.name]} registerOptions={field.registerOptions} + {...(field.type === 'email' && { + verifiedSaveButton: setVerified, + })} /> ); })} @@ -199,7 +203,7 @@ const EditUserModal = ({ label={formatMessage({ id: 'label.save', })} - disabled={isLoading} + disabled={isLoading || !verified} type='submit' /> = () => { return ( - {!isAdminEmailVerified && } + {!isAdminEmailVerified && isAdmin && } {hasActiveQFRound && !isOnSolana && } {title && `${title} |`} Giveth From 3920020e1c110683c8b9b208bb3e36ded9366418 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Mon, 25 Nov 2024 17:37:35 +0100 Subject: [PATCH 12/18] updating actions cards --- .../views/project/projectActionCard/AdminActions.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/project/projectActionCard/AdminActions.tsx b/src/components/views/project/projectActionCard/AdminActions.tsx index 6ad468605b..70fdf1c25f 100644 --- a/src/components/views/project/projectActionCard/AdminActions.tsx +++ b/src/components/views/project/projectActionCard/AdminActions.tsx @@ -32,6 +32,7 @@ import { EVerificationStatus } from '@/apollo/types/types'; import ClaimRecurringDonationModal from '../../userProfile/projectsTab/ClaimRecurringDonationModal'; import config from '@/configuration'; import { findAnchorContractAddress } from '@/helpers/superfluid'; +import { ProjectCardNotification } from './ProjectCardNotification'; interface IMobileActionsModalProps { setShowModal: (value: boolean) => void; @@ -174,8 +175,8 @@ export const AdminActions = () => { project={project} /> )} - - + + ) : ( <> @@ -238,6 +239,7 @@ export const AdminActions = () => { /> )} + ); }; From 3041ecf4f690624b2063e0610fa14e71951a0e10 Mon Sep 17 00:00:00 2001 From: Mitch Oz Date: Mon, 25 Nov 2024 14:51:02 -0600 Subject: [PATCH 13/18] update banner text for email verification --- lang/ca.json | 2 +- lang/en.json | 2 +- lang/es.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lang/ca.json b/lang/ca.json index 7b4cdd8f92..a27ad5f828 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -392,7 +392,7 @@ "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 electrònic? 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_verify_banner": " i verifica la propietat de la teva adreça de correu electrònic per recuperar l'accés als teus projectes.", "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", diff --git a/lang/en.json b/lang/en.json index d5de6b3ddd..9150f71c69 100644 --- a/lang/en.json +++ b/lang/en.json @@ -392,7 +392,7 @@ "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_verify_banner": " & verify ownership of your email address to regain access to your projects.", "label.email_actions_text": "Verify your email to manage your projects!", "label.email_error_verify": "Error verification email", "label.enable_change": "Enable Change", diff --git a/lang/es.json b/lang/es.json index 6c1a78c378..8aa45bc4cc 100644 --- a/lang/es.json +++ b/lang/es.json @@ -392,7 +392,7 @@ "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 electrónico? 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_verify_banner": " y verifica la propiedad de tu dirección de correo electrónico para recuperar el acceso a tus proyectos.", "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", From 304457bac2d00493952184e5fd0f94b4c66c0341 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Tue, 26 Nov 2024 15:25:37 +0100 Subject: [PATCH 14/18] Fixing bugs and adding new features --- lang/ca.json | 5 + lang/en.json | 8 ++ lang/es.json | 5 + src/components/Header/Header.tsx | 5 + src/components/InputUserEmailVerify.tsx | 4 +- src/components/controller/modal.ctrl.tsx | 10 ++ src/components/modals/VerifyEmailModal.tsx | 110 ++++++++++++++++++ .../views/project/ProjectGIVbackToast.tsx | 9 +- src/components/views/project/ProjectIndex.tsx | 6 +- src/components/views/project/ProjectTabs.tsx | 26 ++++- .../views/userProfile/UserProfile.view.tsx | 8 +- .../views/userProfile/VerifyEmailBanner.tsx | 9 +- src/features/modal/modal.slice.ts | 5 + 13 files changed, 199 insertions(+), 11 deletions(-) create mode 100644 src/components/modals/VerifyEmailModal.tsx diff --git a/lang/ca.json b/lang/ca.json index ee05908859..c29e58af10 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -395,6 +395,11 @@ "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.email_modal_verify_your": "Verifica la teva adreça de correu electrònic", + "label.email_modal_need_verify": "Hauràs de verificar la teva adreça de correu electrònic abans de poder crear un nou projecte.", + "label.email_modal_verifying": "Verificar la teva adreça de correu electrònic assegura que puguem comunicar-nos amb tu sobre qualsevol canvi important a la plataforma. La teva adreça de correu electrònic no es compartirà públicament.", + "label.email_modal_to_verifying": "Per verificar la teva adreça de correu electrònic, edita el teu perfil i actualitza el teu correu electrònic.", + "label.email_modal_button": "Actualitza el perfil", "label.enable_change": "Habilita el canvi", "label.enable_recurring_donations": "Habilitar Donacions Recurrents", "label.ends_on": "acaba el", diff --git a/lang/en.json b/lang/en.json index 031b61d825..3edd465357 100644 --- a/lang/en.json +++ b/lang/en.json @@ -395,6 +395,14 @@ "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.email_modal_verify_your": "Verify your email address", + "label.email_modal_need_verify": "You'll need to verify your email address before being able to create a new project.", + "label.email_modal_verifying": "Verifying your email address ensures we can communicate with you about any important changes on the platform. Your email address will not be shared publicly.", + "label.email_modal_to_verifying": "To verify your email address edit your profile and update your email.", + "label.email_modal_button": "Update profile", + + + "label.enable_change": "Enable Change", "label.enable_recurring_donations": "Enable Recurring Donations", "label.ends_on": "ends on", diff --git a/lang/es.json b/lang/es.json index 0ecd7837c8..ad2a7b3fb1 100644 --- a/lang/es.json +++ b/lang/es.json @@ -395,6 +395,11 @@ "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.email_modal_verify_your": "Verifica tu dirección de correo electrónico", + "label.email_modal_need_verify": "Necesitarás verificar tu dirección de correo electrónico antes de poder crear un nuevo proyecto.", + "label.email_modal_verifying": "Verificar tu dirección de correo electrónico asegura que podamos comunicarnos contigo sobre cualquier cambio importante en la plataforma. Tu dirección de correo electrónico no se compartirá públicamente.", + "label.email_modal_to_verifying": "Para verificar tu dirección de correo electrónico, edita tu perfil y actualiza tu correo electrónico.", + "label.email_modal_button": "Actualizar perfil", "label.enable_change": "Ayuda al Cambio", "label.enable_recurring_donations": "Habilitar Donaciones Recurrentes", "label.ends_on": "termina el", diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 2ef881021f..2e73698ce5 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -35,6 +35,7 @@ import { ETheme } from '@/features/general/general.slice'; import { setShowCompleteProfile, setShowSearchModal, + setShowVerifyEmailModal, } from '@/features/modal/modal.slice'; import { slugToProjectView } from '@/lib/routeCreators'; import { useModalCallback } from '@/hooks/useModalCallback'; @@ -138,6 +139,10 @@ const Header: FC = () => { openWalletConnectModal(); } else if (!isSignedIn) { signInThenCreate(); + } else if (!isUserRegistered(userData)) { + dispatch(setShowCompleteProfile(true)); + } else if (!userData?.isEmailVerified) { + dispatch(setShowVerifyEmailModal(true)); } else if (isUserRegistered(userData)) { router.push(Routes.CreateProject); } else { diff --git a/src/components/InputUserEmailVerify.tsx b/src/components/InputUserEmailVerify.tsx index 37244252ed..39ca986626 100644 --- a/src/components/InputUserEmailVerify.tsx +++ b/src/components/InputUserEmailVerify.tsx @@ -367,7 +367,7 @@ const InputUserEmailVerify = forwardRef( @@ -635,7 +635,7 @@ const VerifyInputButtonWrapper = styled.button` $verified ? semanticColors.jade[500] : brandColors.giv[500]}; } &:disabled { - opacity: 0.5; + opacity: ${({ $verified }) => ($verified ? '1' : '0.5')}; } `; diff --git a/src/components/controller/modal.ctrl.tsx b/src/components/controller/modal.ctrl.tsx index 84a25bc2a8..17934e8d77 100644 --- a/src/components/controller/modal.ctrl.tsx +++ b/src/components/controller/modal.ctrl.tsx @@ -4,6 +4,7 @@ import WelcomeModal from '@/components/modals/WelcomeModal'; import { FirstWelcomeModal } from '@/components/modals/FirstWelcomeModal'; import { SignWithWalletModal } from '@/components/modals/SignWithWalletModal'; import { CompleteProfileModal } from '@/components/modals/CompleteProfileModal'; +import { VerifyEmailModal } from '@/components/modals/VerifyEmailModal'; import { useIsSafeEnvironment } from '@/hooks/useSafeAutoConnect'; import { useAppDispatch, useAppSelector } from '@/features/hooks'; import { @@ -13,6 +14,7 @@ import { setShowSearchModal, setShowSwitchNetworkModal, setShowWelcomeModal, + setShowVerifyEmailModal, } from '@/features/modal/modal.slice'; import { isUserRegistered } from '@/lib/helpers'; import { SearchModal } from '../modals/SearchModal'; @@ -27,6 +29,7 @@ const ModalController = () => { showWelcomeModal, showSearchModal, showSwitchNetwork, + showVerifyEmailModal, } = useAppSelector(state => state.modal); const { userData, isSignedIn } = useAppSelector(state => state.user); @@ -103,6 +106,13 @@ const ModalController = () => { } /> )} + {showVerifyEmailModal && ( + + dispatch(setShowVerifyEmailModal(state)) + } + /> + )} ); }; diff --git a/src/components/modals/VerifyEmailModal.tsx b/src/components/modals/VerifyEmailModal.tsx new file mode 100644 index 0000000000..b9b30e95d8 --- /dev/null +++ b/src/components/modals/VerifyEmailModal.tsx @@ -0,0 +1,110 @@ +import { FC } from 'react'; +import router from 'next/router'; +import styled from 'styled-components'; +import { + brandColors, + Button, + H5, + IconProfile, + Lead, +} from '@giveth/ui-design-system'; +import { useIntl } from 'react-intl'; + +import { Modal } from '@/components/modals/Modal'; +import Routes from '@/lib/constants/Routes'; +import { IModal } from '@/types/common'; +import { useAppSelector } from '@/features/hooks'; +import { ETheme } from '@/features/general/general.slice'; +import { useModalAnimation } from '@/hooks/useModalAnimation'; + +export const VerifyEmailModal: FC = ({ setShowModal }) => { + const theme = useAppSelector(state => state.general.theme); + const { isAnimating, closeModal } = useModalAnimation(setShowModal); + const { formatMessage } = useIntl(); + + const handleClick = () => { + router.push( + { + pathname: Routes.MyAccount, + query: { opencheck: 'true' }, + }, + undefined, + { shallow: true }, + ); + closeModal(); + }; + + return ( + } + headerTitle={formatMessage({ id: 'label.complete_your_profile' })} + headerTitlePosition='left' + > + + + {formatMessage({ + id: 'label.email_modal_verify_your', + })} + +
+ {formatMessage({ + id: 'label.email_modal_need_verify', + })} +
+
+
+ {formatMessage({ + id: 'label.email_modal_verifying', + })} +
+
+
+ {formatMessage({ + id: 'label.email_modal_to_verifying', + })} +
+ + +
+
+ ); +}; + +const Container = styled(Lead)` + max-width: 528px; + padding: 24px; + text-align: left; + color: ${brandColors.deep[900]}; +`; + +const OkButton = styled(Button)` + width: 300px; + height: 48px; + margin: 48px auto 0; +`; + +const SkipButton = styled(Button)` + width: 300px; + margin: 0 auto 0; + background: transparent !important; + color: ${brandColors.pinky[500]} !important; + &:hover { + background: transparent !important; + color: ${brandColors.pinky[300]} !important; + } +`; + +const Title = styled(H5)` + margin: 24px 0; + font-weight: 700; +`; diff --git a/src/components/views/project/ProjectGIVbackToast.tsx b/src/components/views/project/ProjectGIVbackToast.tsx index 62e26d268b..d7aeda77f7 100644 --- a/src/components/views/project/ProjectGIVbackToast.tsx +++ b/src/components/views/project/ProjectGIVbackToast.tsx @@ -39,7 +39,8 @@ import { GIVBACKS_DONATION_QUALIFICATION_VALUE_USD } from '@/lib/constants/const const ProjectGIVbackToast = () => { const [showBoost, setShowBoost] = useState(false); const [showVerification, setShowVerification] = useState(false); - const { projectData, isAdmin, activateProject } = useProjectContext(); + const { projectData, isAdmin, activateProject, isAdminEmailVerified } = + useProjectContext(); const verStatus = projectData?.verificationFormStatus; const projectStatus = projectData?.status.name; const isGivbackEligible = projectData?.isGivbackEligible; @@ -74,6 +75,7 @@ const ProjectGIVbackToast = () => { const handleBoostClick = () => { if (isSSRMode) return; + if (!isAdminEmailVerified) return; if (!isEnabled) { openConnectModal?.(); } else if (!isSignedIn) { @@ -329,7 +331,7 @@ const ProjectGIVbackToast = () => { return ( <> - + {icon}
@@ -405,7 +407,7 @@ const Content = styled(Flex)` } `; -const Wrapper = styled(Flex)` +const Wrapper = styled(Flex)<{ $isverified: boolean }>` justify-content: space-between; align-items: center; gap: 24px; @@ -417,6 +419,7 @@ const Wrapper = styled(Flex)` ${mediaQueries.laptopL} { flex-direction: row; } + opacity: ${({ $isverified }) => ($isverified ? '1' : '0.75')}; `; const InnerLink = styled.a` diff --git a/src/components/views/project/ProjectIndex.tsx b/src/components/views/project/ProjectIndex.tsx index 55acb70dbc..965b6c69fb 100644 --- a/src/components/views/project/ProjectIndex.tsx +++ b/src/components/views/project/ProjectIndex.tsx @@ -221,7 +221,11 @@ const ProjectIndex: FC = () => { {projectData && !isDraft && ( - + )} diff --git a/src/components/views/project/ProjectTabs.tsx b/src/components/views/project/ProjectTabs.tsx index 744aaf581b..9aeaf8686f 100644 --- a/src/components/views/project/ProjectTabs.tsx +++ b/src/components/views/project/ProjectTabs.tsx @@ -17,6 +17,7 @@ import { Shadow } from '@/components/styled-components/Shadow'; interface IProjectTabs { activeTab: number; slug: string; + verified?: boolean; } const badgeCount = (count?: number) => { @@ -24,7 +25,7 @@ const badgeCount = (count?: number) => { }; const ProjectTabs = (props: IProjectTabs) => { - const { activeTab, slug } = props; + const { activeTab, slug, verified } = props; const { projectData, totalDonationsCount, boostersData } = useProjectContext(); const { totalProjectUpdates } = projectData || {}; @@ -61,8 +62,22 @@ const ProjectTabs = (props: IProjectTabs) => { i.query ? `?tab=${i.query}` : '' }`} scroll={false} + onClick={e => { + if ( + !verified && + i.query === EProjectPageTabs.UPDATES + ) { + e.preventDefault(); // Prevent the link from navigating from unverified users + } + }} > - + {formatMessage({ id: i.title })} {badgeCount(i.badge) && ( ` display: flex; padding: 9px 24px; border-radius: 48px; @@ -117,6 +136,7 @@ const Tab = styled(P)` color: ${brandColors.pinky[500]}; background: ${neutralColors.gray[200]}; } + opacity: ${({ $unverified }) => ($unverified ? '0.5' : '1')}; `; const Wrapper = styled.div` diff --git a/src/components/views/userProfile/UserProfile.view.tsx b/src/components/views/userProfile/UserProfile.view.tsx index 9235c4b532..2fbd335488 100644 --- a/src/components/views/userProfile/UserProfile.view.tsx +++ b/src/components/views/userProfile/UserProfile.view.tsx @@ -47,6 +47,8 @@ import { useGeneralWallet } from '@/providers/generalWalletProvider'; export interface IUserProfileView {} const UserProfileView: FC = () => { + const router = useRouter(); + const [showModal, setShowModal] = useState(false); // follow this state to refresh user content on screen const [showUploadProfileModal, setShowUploadProfileModal] = useState(false); const [showIncompleteWarning, setShowIncompleteWarning] = useState(false); @@ -57,12 +59,16 @@ const UserProfileView: FC = () => { const [pfpData, setPfpData] = useState(); const { walletChainType, chain } = useGeneralWallet(); const { user, myAccount } = useProfileContext(); - const router = useRouter(); const pfpToken = useGiverPFPToken(user?.walletAddress, user?.avatar); const showCompleteProfile = user && !isUserRegistered(user) && showIncompleteWarning && myAccount; + // Update the modal state if the query changes + useEffect(() => { + setShowModal(!!router.query.opencheck); + }, [router.query.opencheck]); + useEffect(() => { if (user && !isUserRegistered(user) && myAccount) { setShowIncompleteWarning(true); diff --git a/src/components/views/userProfile/VerifyEmailBanner.tsx b/src/components/views/userProfile/VerifyEmailBanner.tsx index ea992647a8..0a3dfe9741 100644 --- a/src/components/views/userProfile/VerifyEmailBanner.tsx +++ b/src/components/views/userProfile/VerifyEmailBanner.tsx @@ -16,7 +16,14 @@ const VerifyEmailBanner = () => {