From b54769cb822ed699fd4c2c87e93f4712a28ba224 Mon Sep 17 00:00:00 2001 From: Kolade Date: Sat, 21 Dec 2024 17:33:45 +0100 Subject: [PATCH] User Email/Telegram Integration (#1987) * update interface user account * update email and verify modal * DAPP-1957: Integrate UI for Dashboard Social Account Links (Email/Telegram/Discord) * update formdata * update button size * update icons * update user profile info * update user profile * update verify email * add telegram and discord modals * update dashboard to work with modals * update modals * update telegram flow * fix: added changes for telegram user validation * update url * update value and type * add discord verification * update dropdown * add social handles to rewards points * add rewards handle list item * update rewards email telegram * update index * update telegram and discord * add usersocial profile hook * adjust prod url * add baseurl and form error message * add enabled filter * remove console logs * hide discord on dev * remove discord * fix email schema * fix telegram schema * resolve comments * fix tooltip bug * fix url and add form settings * Fixed the display of alert message for email and telegram * telegram congif fixes for staging, localhost and alpha env --------- Co-authored-by: Kushdeep Singh Co-authored-by: Ashis Co-authored-by: abhishek-01k Co-authored-by: rohitmalhotra1420 --- src/blocks/button/Button.tsx | 4 + .../illustrations/components/Avatar.tsx | 54 ++++ .../components/DiscordProfile.tsx | 30 ++ .../illustrations/components/EmailProfile.tsx | 30 ++ .../illustrations/components/Telegram.tsx | 36 +++ .../components/TelegramProfile.tsx | 32 ++ src/blocks/illustrations/index.ts | 10 + src/blocks/tooltip/Tooltip.tsx | 33 ++- src/common/components/CopyButton.tsx | 17 +- .../UserProfileSettings/AddDiscord.tsx | 274 ++++++++++++++++++ .../UserProfileSettings/AddEmail.form.ts | 29 ++ .../UserProfileSettings/AddEmail.tsx | 247 ++++++++++++++++ .../UserProfileSettings/AddTelegram.form.ts | 18 ++ .../UserProfileSettings/AddTelegram.tsx | 274 ++++++++++++++++++ .../UserProfileSettings/UploadAvatarModal.tsx | 171 +++++++++++ .../UserProfileSettings.form.ts | 27 ++ .../UserProfileSettings.tsx | 188 ++++++++++++ .../UserProfileSettingsItem.tsx | 62 ++++ .../UserProfileSocialSettings.tsx | 134 +++++++++ src/components/userSettings/UserSettings.tsx | 130 +++++++-- src/config/Themization.js | 6 + src/config/config-alpha.js | 4 + src/config/config-dev.js | 4 + src/config/config-localhost.js | 4 + src/config/config-prod.js | 4 + src/config/config-staging.js | 4 + src/modules/dashboard/Dashboard.constants.ts | 2 +- src/modules/dashboard/Dashboard.tsx | 88 +++++- .../components/ClaimSocialHandles.tsx | 85 ++++++ .../components/ConnectSocialHandles.tsx | 125 ++++++++ .../dashboard/components/Socialhandles.tsx | 39 +++ .../dashboard/hooks/useSocialHandles.tsx | 110 +++++++ .../RewardsActivitiesBottomSection.tsx | 11 +- .../components/RewardsActivitiesList.tsx | 190 +++++++----- .../components/RewardsActivitiesSection.tsx | 17 +- .../rewards/components/SocialHandleItem.tsx | 118 ++++++++ .../rewards/utils/activityTypeArray.ts | 1 + .../userSettings/UserSettingsModule.tsx | 7 +- src/queries/baseURL.ts | 13 + src/queries/hooks/user/index.ts | 5 + src/queries/hooks/user/useGetSocialsStatus.ts | 10 + .../hooks/user/useGetUserProfileInfo.ts | 22 ++ .../user/useSendHandlesVerificationCode.ts | 9 + .../hooks/user/useUpdateUserProfileInfo.ts | 9 + .../user/useVerifyHandlesVerificationCode.ts | 9 + .../user/getSocialsStatusModelCreator.ts | 3 + .../user/getUserProfileInfoModelCreator.ts | 4 + src/queries/models/user/index.ts | 4 + ...sendHandlesVerificationCodeModelCreator.ts | 5 + ...rifyHandlesVerificationCodeModelCreator.ts | 5 + src/queries/queryKeys.ts | 5 + .../services/user/getUserProfileInfo.ts | 5 + .../services/user/getUserSocialsStatus.ts | 19 ++ src/queries/services/user/index.ts | 5 + .../user/sendHandlesVerificationCode.ts | 26 ++ .../services/user/updateUserProfileInfo.ts | 24 ++ .../user/verifyHandlesVerificationCode.ts | 26 ++ src/queries/types/rewards.ts | 1 + src/queries/types/user.ts | 33 +++ 59 files changed, 2721 insertions(+), 140 deletions(-) create mode 100644 src/blocks/illustrations/components/Avatar.tsx create mode 100644 src/blocks/illustrations/components/DiscordProfile.tsx create mode 100644 src/blocks/illustrations/components/EmailProfile.tsx create mode 100644 src/blocks/illustrations/components/Telegram.tsx create mode 100644 src/blocks/illustrations/components/TelegramProfile.tsx create mode 100644 src/components/UserProfileSettings/AddDiscord.tsx create mode 100644 src/components/UserProfileSettings/AddEmail.form.ts create mode 100644 src/components/UserProfileSettings/AddEmail.tsx create mode 100644 src/components/UserProfileSettings/AddTelegram.form.ts create mode 100644 src/components/UserProfileSettings/AddTelegram.tsx create mode 100644 src/components/UserProfileSettings/UploadAvatarModal.tsx create mode 100644 src/components/UserProfileSettings/UserProfileSettings.form.ts create mode 100644 src/components/UserProfileSettings/UserProfileSettings.tsx create mode 100644 src/components/UserProfileSettings/UserProfileSettingsItem.tsx create mode 100644 src/components/UserProfileSettings/UserProfileSocialSettings.tsx create mode 100644 src/modules/dashboard/components/ClaimSocialHandles.tsx create mode 100644 src/modules/dashboard/components/ConnectSocialHandles.tsx create mode 100644 src/modules/dashboard/components/Socialhandles.tsx create mode 100644 src/modules/dashboard/hooks/useSocialHandles.tsx create mode 100644 src/modules/rewards/components/SocialHandleItem.tsx create mode 100644 src/queries/hooks/user/useGetSocialsStatus.ts create mode 100644 src/queries/hooks/user/useGetUserProfileInfo.ts create mode 100644 src/queries/hooks/user/useSendHandlesVerificationCode.ts create mode 100644 src/queries/hooks/user/useUpdateUserProfileInfo.ts create mode 100644 src/queries/hooks/user/useVerifyHandlesVerificationCode.ts create mode 100644 src/queries/models/user/getSocialsStatusModelCreator.ts create mode 100644 src/queries/models/user/getUserProfileInfoModelCreator.ts create mode 100644 src/queries/models/user/sendHandlesVerificationCodeModelCreator.ts create mode 100644 src/queries/models/user/verifyHandlesVerificationCodeModelCreator.ts create mode 100644 src/queries/services/user/getUserProfileInfo.ts create mode 100644 src/queries/services/user/getUserSocialsStatus.ts create mode 100644 src/queries/services/user/sendHandlesVerificationCode.ts create mode 100644 src/queries/services/user/updateUserProfileInfo.ts create mode 100644 src/queries/services/user/verifyHandlesVerificationCode.ts diff --git a/src/blocks/button/Button.tsx b/src/blocks/button/Button.tsx index 9aec7345d8..dbf278d217 100644 --- a/src/blocks/button/Button.tsx +++ b/src/blocks/button/Button.tsx @@ -29,6 +29,8 @@ export type ButtonProps = { block?: boolean; /* Button loading state */ loading?: boolean; + /* indicate button type ina a form or similar usecases */ + type?: 'button' | 'submit'; } & TransformedHTMLAttributes; const StyledButton = styled.button` @@ -81,6 +83,7 @@ const Button = forwardRef( iconOnly, circular = false, children, + type, ...props }, ref @@ -94,6 +97,7 @@ const Button = forwardRef( role="button" ref={ref} size={size} + type={type} variant={variant} {...props} > diff --git a/src/blocks/illustrations/components/Avatar.tsx b/src/blocks/illustrations/components/Avatar.tsx new file mode 100644 index 0000000000..05cee6e9ff --- /dev/null +++ b/src/blocks/illustrations/components/Avatar.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react'; +import { IllustrationWrapper } from '../IllustrationWrapper'; +import { IllustrationProps } from '../Illustrations.types'; + +const Avatar: FC = (allProps) => { + const { svgProps: props, ...restProps } = allProps; + return ( + + + + + + + + + + } + {...restProps} + /> + ); +}; + +export default Avatar; diff --git a/src/blocks/illustrations/components/DiscordProfile.tsx b/src/blocks/illustrations/components/DiscordProfile.tsx new file mode 100644 index 0000000000..35a9944ae6 --- /dev/null +++ b/src/blocks/illustrations/components/DiscordProfile.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; +import { IllustrationWrapper } from '../IllustrationWrapper'; +import { IllustrationProps } from '../Illustrations.types'; + +const DiscordProfile: FC = (allProps) => { + const { svgProps: props, ...restProps } = allProps; + return ( + + + + } + {...restProps} + /> + ); +}; + +export default DiscordProfile; diff --git a/src/blocks/illustrations/components/EmailProfile.tsx b/src/blocks/illustrations/components/EmailProfile.tsx new file mode 100644 index 0000000000..1df8eba6db --- /dev/null +++ b/src/blocks/illustrations/components/EmailProfile.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; +import { IllustrationWrapper } from '../IllustrationWrapper'; +import { IllustrationProps } from '../Illustrations.types'; + +const EmailProfile: FC = (allProps) => { + const { svgProps: props, ...restProps } = allProps; + return ( + + + + } + {...restProps} + /> + ); +}; + +export default EmailProfile; diff --git a/src/blocks/illustrations/components/Telegram.tsx b/src/blocks/illustrations/components/Telegram.tsx new file mode 100644 index 0000000000..c7493a9c89 --- /dev/null +++ b/src/blocks/illustrations/components/Telegram.tsx @@ -0,0 +1,36 @@ +import { FC } from 'react'; +import { IllustrationWrapper } from '../IllustrationWrapper'; +import { IllustrationProps } from '../Illustrations.types'; + +const Telegram: FC = (allProps) => { + const { svgProps: props, ...restProps } = allProps; + return ( + + + + + } + {...restProps} + /> + ); +}; + +export default Telegram; diff --git a/src/blocks/illustrations/components/TelegramProfile.tsx b/src/blocks/illustrations/components/TelegramProfile.tsx new file mode 100644 index 0000000000..82ee5c7161 --- /dev/null +++ b/src/blocks/illustrations/components/TelegramProfile.tsx @@ -0,0 +1,32 @@ +import { FC } from 'react'; +import { IllustrationWrapper } from '../IllustrationWrapper'; +import { IllustrationProps } from '../Illustrations.types'; + +const TelegramProfile: FC = (allProps) => { + const { svgProps: props, ...restProps } = allProps; + return ( + + + + } + {...restProps} + /> + ); +}; + +export default TelegramProfile; diff --git a/src/blocks/illustrations/index.ts b/src/blocks/illustrations/index.ts index 5d9470392a..5c22e31c65 100644 --- a/src/blocks/illustrations/index.ts +++ b/src/blocks/illustrations/index.ts @@ -7,6 +7,8 @@ export { default as AlphaAccessNFT } from './components/AlphaAccessNFT'; export { default as Arbitrum } from './components/Arbitrum'; +export { default as Avatar } from './components/Avatar'; + export { default as Base } from './components/Base'; export { default as BlueBonusActivitySubscribers } from './components/BlueBonusActivitySubscribers'; @@ -29,8 +31,12 @@ export { default as CyberLogoRewards } from './components/CyberLogoRewards'; export { default as Discord } from './components/Discord'; +export { default as DiscordProfile } from './components/DiscordProfile'; + export { default as EarnOnPush } from './components/EarnOnPush'; +export { default as EmailProfile } from './components/EmailProfile'; + export { default as Ethereum } from './components/Ethereum'; export { default as FiveSubscribedDefiChannel } from './components/FiveSubscribedDefiChannel'; @@ -99,6 +105,10 @@ export { default as StakePushYellowMultiplier } from './components/StakePushYell export { default as SubscribePoints } from './components/SubscribePoints'; +export { default as Telegram } from './components/Telegram'; + +export { default as TelegramProfile } from './components/TelegramProfile'; + export { default as TripleRewardsCoin } from './components/TripleRewardsCoin'; export { default as Twitter } from './components/Twitter'; diff --git a/src/blocks/tooltip/Tooltip.tsx b/src/blocks/tooltip/Tooltip.tsx index 6d3e7553d3..75912f0093 100644 --- a/src/blocks/tooltip/Tooltip.tsx +++ b/src/blocks/tooltip/Tooltip.tsx @@ -20,6 +20,7 @@ const RadixTooltipContent = styled(RadixTooltip.Content).withConfig({ word-wrap: break-word; color: var(--text-primary-inverse); background-color: var(--surface-primary-inverse); + z-index: 9999999999; /* Tooltip non-responsive styles */ width: ${({ width }) => width}; @@ -96,21 +97,23 @@ const Tooltip: FC = ({ {children} - - {overlay ? ( - overlay - ) : ( - <> - {title && {title}} - {description && {description}} - - )} - + {(title || overlay || description) && ( + + {overlay ? ( + overlay + ) : ( + <> + {title && {title}} + {description && {description}} + + )} + + )} diff --git a/src/common/components/CopyButton.tsx b/src/common/components/CopyButton.tsx index 54b06d57f5..fae4a4d7a4 100644 --- a/src/common/components/CopyButton.tsx +++ b/src/common/components/CopyButton.tsx @@ -5,22 +5,31 @@ import { FC, useState } from 'react'; type CopyButtonProps = { tooltipTitle: string; content: string; + size?: number; }; -const CopyButton: FC = ({ tooltipTitle, content }) => { +const CopyButton: FC = ({ tooltipTitle, content, size }) => { const [hover, setHover] = useState(false); + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + copyToClipboard(content); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + return ( - + copyToClipboard(content)} + onClick={handleCopy} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} > diff --git a/src/components/UserProfileSettings/AddDiscord.tsx b/src/components/UserProfileSettings/AddDiscord.tsx new file mode 100644 index 0000000000..9ffba1779e --- /dev/null +++ b/src/components/UserProfileSettings/AddDiscord.tsx @@ -0,0 +1,274 @@ +import { FC, useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +import * as Yup from 'yup'; +import { useFormik } from 'formik'; + +import { useAccount } from 'hooks'; +import { useAppContext } from 'contexts/AppContext'; +import { walletToCAIP10 } from 'helpers/w2w'; +import { useSendHandlesVerificationCode } from 'queries'; +import { generateVerificationProof } from 'modules/rewards/utils/generateVerificationProof'; +import { appConfig } from 'config'; + +import { Box, Discord, Link, Modal, Text, TextInput } from 'blocks'; +import { CopyButton, ModalResponse } from 'common'; +import { shortenText } from 'helpers/UtilityHelper'; + +type AddDiscordProps = { + modalControl: ModalResponse; + refetchSocialHandleStatus: () => void; + setErrorMessage: (errorMessage: string) => void; + setSuccessMessage: (successMessage: string) => void; +}; + +enum Steps { + EnterDiscord = 1, + VerifyId = 2, +} + +const AddDiscord: FC = ({ + modalControl, + refetchSocialHandleStatus, + // setErrorMessage, + // setSuccessMessage, +}) => { + const { isOpen, onClose } = modalControl; + const { account, wallet } = useAccount(); + const { handleConnectWalletAndEnableProfile } = useAppContext(); + + const caip10WalletAddress = walletToCAIP10({ account }); + const [step, setStep] = useState(1); + const [discordCode, setDiscordCode] = useState(''); + const { userPushSDKInstance } = useSelector((state: any) => { + return state.user; + }); + + const { mutate: sendVerification, isPending: isSendingVerification } = useSendHandlesVerificationCode(); + + const discordValidationSchema = Yup.object({ + discord: Yup.string().required('Required'), + }); + + const discordFormik = useFormik({ + initialValues: { discord: '' }, + validationSchema: discordValidationSchema, + onSubmit: () => { + handleSendVerificationCode(); + }, + }); + + const getSDKInstance = useCallback(async () => { + return userPushSDKInstance?.signer ? userPushSDKInstance : await handleConnectWalletAndEnableProfile({ wallet }); + }, [userPushSDKInstance, handleConnectWalletAndEnableProfile, wallet]); + + const handleSendVerificationCode = async () => { + const sdkInstance = await getSDKInstance(); + const data = { + wallet: caip10WalletAddress, + value: { discord_username: discordFormik.values.discord }, + valueType: 'telegram', + }; + + const verificationProof = await generateVerificationProof(data, sdkInstance); + + if (!verificationProof) return; + + sendVerification( + { + caipAddress: caip10WalletAddress as string, + verificationProof: verificationProof as string, + value: { discord_username: discordFormik.values.discord }, + social_platform: 'discord', + }, + { + onSuccess: (response: any) => { + if (response?.success) { + setDiscordCode(response.verificationCode); + setStep(Steps.VerifyId); + } else { + discordFormik?.setFieldError('discord', 'Error sending code. Please try again'); + } + }, + onError: (error: Error) => { + console.log('Error sending code', error); + }, + } + ); + }; + + return ( + { + refetchSocialHandleStatus(); + onClose(); + }} + {...(step === Steps.VerifyId && { + onBack: () => setStep(Steps.EnterDiscord), + })} + acceptButtonProps={ + step === Steps.EnterDiscord + ? { + children: 'Next', + loading: isSendingVerification, + onClick: () => { + discordFormik?.handleSubmit(); + }, + } + : null + } + cancelButtonProps={null} + > + {step === Steps.EnterDiscord && ( + + + + + + + Enter your Discord ID + + + + Follow the steps to link your Discord to Push and receive notifications + + +
+ + + +
+
+ )} + + {step === Steps.VerifyId && discordCode && ( + + + + + + + Connect your Discord + + + + Follow the steps to link your Discord to Push and receive notifications + + + + + Step 1: Copy the verification code + + + + + {shortenText(`${caip10WalletAddress}-${discordCode}`, 10)} + + + + + + + + + Step 2: Visit the link and paste the code + + + + {/* generate call link shortly */} + + {appConfig?.discordExternalURL} + + + + + + Please ensure you’re logged into your Discord account. Click on Complete Verification below once complete. + + + )} +
+ ); +}; + +export default AddDiscord; diff --git a/src/components/UserProfileSettings/AddEmail.form.ts b/src/components/UserProfileSettings/AddEmail.form.ts new file mode 100644 index 0000000000..d552a2b9bd --- /dev/null +++ b/src/components/UserProfileSettings/AddEmail.form.ts @@ -0,0 +1,29 @@ +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import { getRequiredFieldMessage } from 'common'; + +// Validation schema for the email field +export const emailValidationSchema = Yup.object({ + email: Yup.string().email('Invalid email address').required(getRequiredFieldMessage('Email')), +}); + +// Validation schema for the verification code +export const codeValidationSchema = Yup.object({ + code: Yup.string().length(6, 'Code should be 6 digits').required('Required'), +}); + +// Formik setup for the email form +export const useEmailFormik = (handleSendVerificationCode: () => void) => + useFormik({ + initialValues: { email: '' }, + validationSchema: emailValidationSchema, + onSubmit: handleSendVerificationCode, + }); + +// Formik setup for the code form +export const useCodeFormik = (handleVerificationCode: () => void) => + useFormik({ + initialValues: { code: '' }, + validationSchema: codeValidationSchema, + onSubmit: handleVerificationCode, + }); diff --git a/src/components/UserProfileSettings/AddEmail.tsx b/src/components/UserProfileSettings/AddEmail.tsx new file mode 100644 index 0000000000..535bf11b3c --- /dev/null +++ b/src/components/UserProfileSettings/AddEmail.tsx @@ -0,0 +1,247 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import { css } from 'styled-components'; +import { useSelector } from 'react-redux'; + +import { useEmailFormik, useCodeFormik } from './AddEmail.form'; // Import Formik hooks +import { ModalResponse } from 'common'; +import { useAccount } from 'hooks'; +import { walletToCAIP10 } from 'helpers/w2w'; +import { useAppContext } from 'contexts/AppContext'; +import { generateVerificationProof } from 'modules/rewards/utils/generateVerificationProof'; +import { useSendHandlesVerificationCode, useVerifyHandlesVerificationCode } from 'queries'; + +import { Box, Modal, Spinner, Text, TextInput } from 'blocks'; + +type AddEmailProps = { + modalControl: ModalResponse; + refetchSocialHandleStatus: () => void; + setErrorMessage: (errorMessage: string) => void; + setSuccessMessage: (successMessage: string) => void; +}; + +enum Steps { + EnterEmail = 1, + VerifyCode = 2, +} + +const AddEmail: FC = ({ + modalControl, + refetchSocialHandleStatus, + setErrorMessage, + setSuccessMessage, +}) => { + const { isOpen, onClose } = modalControl; + const { account, wallet } = useAccount(); + const { handleConnectWalletAndEnableProfile } = useAppContext(); + const [isLoading, setIsLoading] = useState(false); + + const caip10WalletAddress = walletToCAIP10({ account }); + const [step, setStep] = useState(1); + const { userPushSDKInstance } = useSelector((state: any) => { + return state.user; + }); + + const { mutate: sendVerification, isPending: isSendingVerification } = useSendHandlesVerificationCode(); + const { mutate: verifyVerification } = useVerifyHandlesVerificationCode(); + + const getSDKInstance = useCallback(async () => { + return userPushSDKInstance?.signer ? userPushSDKInstance : await handleConnectWalletAndEnableProfile({ wallet }); + }, [userPushSDKInstance, handleConnectWalletAndEnableProfile, wallet]); + + const handleSendVerificationCode = async () => { + setIsLoading(true); + const sdkInstance = await getSDKInstance(); + const data = { + wallet: caip10WalletAddress, + value: emailFormik.values.email, + valueType: 'email', + }; + + const verificationProof = await generateVerificationProof(data, sdkInstance); + + if (!verificationProof) return; + + sendVerification( + { + caipAddress: caip10WalletAddress as string, + verificationProof: verificationProof as string, + value: emailFormik.values.email, + social_platform: 'email', + }, + { + onSuccess: (response: any) => { + if (response?.success) { + setStep(Steps.VerifyCode); + } else { + emailFormik?.setFieldError('email', 'Error sending code. Please try again'); + } + setIsLoading(false); + }, + onError: (error: Error) => { + console.log('Error sending code', error); + setIsLoading(false); + }, + } + ); + }; + + const handleVerificationCode = async () => { + const sdkInstance = await getSDKInstance(); + const data = { + wallet: caip10WalletAddress, + value: emailFormik.values.email, + valueType: 'email', + verificationCode: codeFormik.values.code, + }; + + const verificationProof = await generateVerificationProof(data, sdkInstance); + + if (!verificationProof) return; + + verifyVerification( + { + caipAddress: caip10WalletAddress as string, + verificationCode: codeFormik.values.code, + value: emailFormik.values.email, + social_platform: 'email', + }, + { + onSuccess: (response: any) => { + if (response?.success) { + onClose(); + refetchSocialHandleStatus(); + setSuccessMessage('Email Account was linked successfully'); + } else { + codeFormik?.setFieldError('code', 'Error verifying code. Please try again'); + } + }, + onError: (error: Error) => { + console.log('Error verifying code', error); + setErrorMessage('Error verifying code'); + }, + } + ); + }; + + // Formik hooks from form.ts + const emailFormik = useEmailFormik(handleSendVerificationCode); + const codeFormik = useCodeFormik(handleVerificationCode); + + // Watch for code length and submit automatically if it reaches 6 digits + useEffect(() => { + if (codeFormik.values.code.length === 6 && !codeFormik.errors.code) { + codeFormik.submitForm(); + } + }, [codeFormik.values.code, codeFormik.errors.code]); + + const resendVerificationCode = () => { + handleSendVerificationCode(); + }; + + return ( + { + emailFormik.handleSubmit(); + }, + } + : null + } + cancelButtonProps={null} + > + {step === Steps.EnterEmail && ( + + + Enter your email + + + + Confirm your email and verify to connect + + +
+ + + +
+
+ )} + + {step === Steps.VerifyCode && ( + + + Verify Email + + + + We sent you a 6 digit confirmation code to {emailFormik.values.email} Please enter it below to confirm your + email address. + + + + + + + + Didn't receive a code? + + Send code again + + {(isSendingVerification || isLoading) && } + + + )} +
+ ); +}; + +export { AddEmail }; diff --git a/src/components/UserProfileSettings/AddTelegram.form.ts b/src/components/UserProfileSettings/AddTelegram.form.ts new file mode 100644 index 0000000000..35a9aa1777 --- /dev/null +++ b/src/components/UserProfileSettings/AddTelegram.form.ts @@ -0,0 +1,18 @@ +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import { getRequiredFieldMessage } from 'common'; + +// Validation schema for the telegram field +const telegramValidationSchema = Yup.object({ + telegram: Yup.string().required(getRequiredFieldMessage('Telegram')), +}); + +// Formik setup for the code form +export const useTelegramFormik = (handleSendVerificationCode: () => void) => + useFormik({ + initialValues: { telegram: '' }, + validationSchema: telegramValidationSchema, + onSubmit: () => { + handleSendVerificationCode(); + }, + }); diff --git a/src/components/UserProfileSettings/AddTelegram.tsx b/src/components/UserProfileSettings/AddTelegram.tsx new file mode 100644 index 0000000000..9bee8d59fc --- /dev/null +++ b/src/components/UserProfileSettings/AddTelegram.tsx @@ -0,0 +1,274 @@ +import { FC, useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { CopyButton, ModalResponse } from 'common'; +import { useAccount } from 'hooks'; +import { useAppContext } from 'contexts/AppContext'; +import { walletToCAIP10 } from 'helpers/w2w'; +import { generateVerificationProof } from 'modules/rewards/utils/generateVerificationProof'; +import { useSendHandlesVerificationCode } from 'queries'; +import { appConfig } from 'config'; + +import { Box, Link, Modal, Telegram, Text, TextInput } from 'blocks'; +import { shortenText } from 'helpers/UtilityHelper'; +import { useTelegramFormik } from './AddTelegram.form'; +import { css } from 'styled-components'; + +type AddTelegramProps = { + modalControl: ModalResponse; + refetchSocialHandleStatus: () => void; + setErrorMessage: (errorMessage: string) => void; + setSuccessMessage: (successMessage: string) => void; +}; + +enum Steps { + EnterTelegram = 1, + VerifyId = 2, +} + +const AddTelegram: FC = ({ + modalControl, + refetchSocialHandleStatus, + // setErrorMessage, + setSuccessMessage, +}) => { + const { isOpen, onClose } = modalControl; + const { account, wallet } = useAccount(); + const { handleConnectWalletAndEnableProfile } = useAppContext(); + + const caip10WalletAddress = walletToCAIP10({ account }); + const [step, setStep] = useState(1); + const [telegramCode, setTelegramCode] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const { userPushSDKInstance } = useSelector((state: any) => { + return state.user; + }); + + const { mutate: sendVerification, isPending: isSendingVerification } = useSendHandlesVerificationCode(); + + const getSDKInstance = useCallback(async () => { + return userPushSDKInstance?.signer ? userPushSDKInstance : await handleConnectWalletAndEnableProfile({ wallet }); + }, [userPushSDKInstance, handleConnectWalletAndEnableProfile, wallet]); + + const handleSendVerificationCode = async () => { + setIsLoading(true); + const sdkInstance = await getSDKInstance(); + const data = { + wallet: caip10WalletAddress, + value: { telegram_username: telegramFormik.values.telegram }, + valueType: 'telegram', + }; + + const verificationProof = await generateVerificationProof(data, sdkInstance); + + if (!verificationProof) return; + + sendVerification( + { + caipAddress: caip10WalletAddress as string, + verificationProof: verificationProof as string, + value: { telegram_username: telegramFormik.values.telegram }, + social_platform: 'telegram', + }, + { + onSuccess: (response: any) => { + if (response?.success) { + setTelegramCode(response.verificationCode); + setStep(Steps.VerifyId); + } else { + telegramFormik?.setFieldError('telegram', 'Error sending code. Please try again'); + } + setIsLoading(false); + }, + onError: (error: Error) => { + console.log('Error sending code', error); + setIsLoading(false); + }, + } + ); + }; + + // Formik hooks from form.ts + const telegramFormik = useTelegramFormik(handleSendVerificationCode); + + return ( + { + setSuccessMessage('') + refetchSocialHandleStatus(); + onClose(); + }} + {...(step === Steps.VerifyId && { + onBack: () => setStep(Steps.EnterTelegram), + })} + acceptButtonProps={ + step === Steps.EnterTelegram + ? { + children: 'Next', + loading: isSendingVerification || isLoading, + onClick: () => { + telegramFormik?.handleSubmit(); + }, + } + : null + } + cancelButtonProps={null} + > + {step === Steps.EnterTelegram && ( + + + + + + + Enter your Telegram ID + + + + Proceed to the next step after entering your Telegram chat ID + + +
+ + + +
+
+ )} + + {step === Steps.VerifyId && telegramCode && ( + + + + + + + Connect your Telegram + + + + Follow the steps to link your Telegram to Push and receive notifications + + + + + Step 1: Copy the verification code + + + + + {shortenText(`/verify ${caip10WalletAddress}-${telegramCode}`, 10)} + + + {isOpen && ( + + )} + + + + + + Step 2: Visit the link and paste the code + + + + + {appConfig?.telegramExternalURL} + + + + + + Please ensure you’re logged into your Telegram account. Click on Complete Verification below once complete. + + + )} +
+ ); +}; + +export default AddTelegram; diff --git a/src/components/UserProfileSettings/UploadAvatarModal.tsx b/src/components/UserProfileSettings/UploadAvatarModal.tsx new file mode 100644 index 0000000000..febe4bcc3b --- /dev/null +++ b/src/components/UserProfileSettings/UploadAvatarModal.tsx @@ -0,0 +1,171 @@ +import { FC, useRef, useState } from 'react'; +import { css } from 'styled-components'; +import { Box, Button, FileUpload, Modal, Text } from 'blocks'; +import ImageClipper from 'primaries/ImageClipper'; +import { ModalResponse } from 'common'; + +type UploadAvatarModalProps = { + formValues: { picture: string | null }; + setFieldValue: (field: string, value: any) => void; + modalControl: ModalResponse; +}; + +const UploadAvatarModal: FC = ({ formValues, setFieldValue, modalControl }) => { + const { isOpen, onClose } = modalControl; + const childRef = useRef(null); + const [croppedImage, setCroppedImage] = useState(formValues.picture as string); + + // Handle file selection from input + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.currentTarget.files?.[0]; + setCroppedImage(undefined); + if (file) { + await processFile(file); + } + }; + + // Handle file drop + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setCroppedImage(undefined); + const file = e.dataTransfer.files?.[0]; + if (file) { + await processFile(file); + } + }; + + // Process selected file + const processFile = async (file: File) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onloadend = () => { + setFieldValue('picture', reader.result as string); // Set as picture + }; + }; + + return ( + + {/* Upload Area */} + + + Upload a PNG, JPG up to 1MB. Crop the image to resize to 128px. + + + + + {croppedImage ? ( + + Cropped Image + + ) : ( + { + setCroppedImage(croppedImg); + setFieldValue('picture', croppedImg); // Set picture value + }} + ref={childRef} + /> + )} + + + + Drag and Drop or + + + + + + + + {/* Action Button */} + + {croppedImage ? ( + + ) : ( + + )} + + + ); +}; + +export default UploadAvatarModal; diff --git a/src/components/UserProfileSettings/UserProfileSettings.form.ts b/src/components/UserProfileSettings/UserProfileSettings.form.ts new file mode 100644 index 0000000000..52b7ac75e5 --- /dev/null +++ b/src/components/UserProfileSettings/UserProfileSettings.form.ts @@ -0,0 +1,27 @@ +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import { getRequiredFieldMessage } from 'common'; + +// Define Formik initial values type +type FormValues = { + displayName: string; + picture: string | null; + desc: string | null; +}; + +// Validation schema for the email field +const validationSchema = Yup.object({ + displayName: Yup.string() + .max(50, 'Display Name cannot exceed 50 characters') + .required(getRequiredFieldMessage('Display Name')), + desc: Yup.string().max(150, 'Bio cannot exceed 150 characters').nullable(), +}); + +// Formik setup for the email form +export const useUserFormik = (handleSubmit: () => void) => { + return useFormik({ + initialValues: { displayName: '', picture: null, desc: '' }, // Ensure picture starts as null + validationSchema, + onSubmit: handleSubmit, + }); +}; diff --git a/src/components/UserProfileSettings/UserProfileSettings.tsx b/src/components/UserProfileSettings/UserProfileSettings.tsx new file mode 100644 index 0000000000..b4f721cf1f --- /dev/null +++ b/src/components/UserProfileSettings/UserProfileSettings.tsx @@ -0,0 +1,188 @@ +// React and other libraries +import { FC, useEffect } from 'react'; +import { useSelector } from 'react-redux'; + +// hooks +import { useAccount } from 'hooks'; +import { useAppContext } from 'contexts/AppContext'; +import { useUserFormik } from './UserProfileSettings.form'; + +//Components +import { Box, Button, CameraFilled, TextInput } from 'blocks'; +import { useDisclosure } from 'common'; +import UploadAvatarModal from './UploadAvatarModal'; +import { css } from 'styled-components'; +import { useGetUserProfileInfo, useUpdateUserProfileInfo } from 'queries'; + +type UserProfileSettingsType = { + errorMessage: string; + setErrorMessage: (errorMessage: string) => void; + successMessage: string; + setSuccessMessage: (successMessage: string) => void; +}; + +const UserProfileSettings: FC = ({ setErrorMessage, setSuccessMessage }) => { + const modalControl = useDisclosure(); + const { wallet } = useAccount(); + const { handleConnectWalletAndEnableProfile } = useAppContext(); + const { data: userProfileInfo, refetch: refetchUserInfo } = useGetUserProfileInfo(); + const { mutate: updateUserInfo, isPending: updatingInfo } = useUpdateUserProfileInfo(); + + const { userPushSDKInstance } = useSelector((state: any) => { + return state.user; + }); + + const handleSubmit = async () => { + const sdkInstance = !userPushSDKInstance?.signer + ? (await handleConnectWalletAndEnableProfile({ wallet })) ?? undefined + : userPushSDKInstance; + + updateUserInfo( + { + userPushSDKInstance: sdkInstance, + name: userFormik?.values.displayName, + desc: userFormik?.values.desc, + picture: userFormik?.values.picture, + }, + { + onSuccess: (response: any) => { + console.log(response); + setSuccessMessage('User Details Updated Successfully'); + refetchUserInfo(); + }, + onError: (error: Error) => { + console.log('Error updating user profile info', error); + setErrorMessage('Error while updating User Info!'); + }, + } + ); + }; + + const userFormik = useUserFormik(handleSubmit); + + // Populate initial form values when userProfileInfo is fetched + useEffect(() => { + if (userProfileInfo) { + userFormik.setValues({ + displayName: userProfileInfo.name || '', + picture: userProfileInfo?.picture || null, + desc: userProfileInfo?.desc || null, + }); + } + }, [userProfileInfo]); + + return ( + +
+ + {userFormik.values.picture ? ( + + + + ) : ( + + + + )} + + + + + {modalControl.isOpen && ( + + )} + + + 0 && !userFormik.values.displayName + ? true // Required error on submit + : userFormik.values.displayName.length > 50 // Length error during typing + } + errorMessage={ + userFormik.submitCount > 0 && !userFormik.values.displayName + ? 'Display Name is required' + : userFormik.values.displayName.length > 50 + ? 'Display Name cannot exceed 50 characters' + : '' + } + totalCount={50} + /> + + + + + + + + +
+ ); +}; + +export default UserProfileSettings; diff --git a/src/components/UserProfileSettings/UserProfileSettingsItem.tsx b/src/components/UserProfileSettings/UserProfileSettingsItem.tsx new file mode 100644 index 0000000000..62fb9587fc --- /dev/null +++ b/src/components/UserProfileSettings/UserProfileSettingsItem.tsx @@ -0,0 +1,62 @@ +import { Box, Text, Button, Skeleton } from 'blocks'; + +const UserProfileSettingsItem = ({ item, isPending }: any) => { + return ( + + + + {item?.icon()} + + + + + {item.itemTitle} + + + {item.itemDescription} + + + + + {item.userStatus === null ? ( + + ) : ( + + + + )} + + ); +}; + +export default UserProfileSettingsItem; diff --git a/src/components/UserProfileSettings/UserProfileSocialSettings.tsx b/src/components/UserProfileSettings/UserProfileSocialSettings.tsx new file mode 100644 index 0000000000..a78f97e5f9 --- /dev/null +++ b/src/components/UserProfileSettings/UserProfileSocialSettings.tsx @@ -0,0 +1,134 @@ +// React and other libraries +import { FC, useEffect, useState } from 'react'; +import { css } from 'styled-components'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; + +// Helpers +import UnlockProfileWrapper, { UNLOCK_PROFILE_TYPE } from 'components/chat/unlockProfile/UnlockProfileWrapper'; +import APP_PATHS from 'config/AppPaths'; + +//Components +import { Box, Text } from 'blocks'; +import { AddEmail } from './AddEmail'; +import AddTelegram from './AddTelegram'; +import AddDiscord from './AddDiscord'; +import UserProfileSettingsItem from './UserProfileSettingsItem'; +import { useSocialHandles } from 'modules/dashboard/hooks/useSocialHandles'; + +type UserProfileSocialSettingsType = { + errorMessage?: string; + setErrorMessage: (errorMessage: string) => void; + successMessage?: string; + setSuccessMessage: (successMessage: string) => void; +}; + +const UserProfileSocialSettings: FC = ({ setErrorMessage, setSuccessMessage }) => { + const navigate = useNavigate(); + const [isAuthModalVisible, setIsAuthModalVisible] = useState(true); + + const { userPushSDKInstance } = useSelector((state: any) => { + return state.user; + }); + + const { + socialHandlesList, + modalControl, + telegramModalControl, + discordModalControl, + isPending, + fetchStatus, + channelAddress, + } = useSocialHandles(setErrorMessage, true, userPushSDKInstance); + + useEffect(() => { + if (!userPushSDKInstance || !channelAddress) return; + fetchStatus(); + }, [userPushSDKInstance, channelAddress]); + + useEffect(() => { + setIsAuthModalVisible(userPushSDKInstance && userPushSDKInstance?.readmode()); + }, [userPushSDKInstance]); + + const handleCloseAuthModal = () => { + setIsAuthModalVisible(false); + navigate(APP_PATHS.WelcomeDashboard); + }; + + return ( + + + + Get Notified Anywhere + + + + Connects apps and receive notifications directly in your Email, Telegram and Discord + + + + + {socialHandlesList?.map((item) => ( + + ))} + + + {isAuthModalVisible && ( + + + + )} + + {modalControl.isOpen && ( + + )} + + {telegramModalControl.isOpen && ( + + )} + + {discordModalControl.isOpen && ( + + )} + + ); +}; + +export default UserProfileSocialSettings; diff --git a/src/components/userSettings/UserSettings.tsx b/src/components/userSettings/UserSettings.tsx index c65f4dba5a..f5bbd4c509 100644 --- a/src/components/userSettings/UserSettings.tsx +++ b/src/components/userSettings/UserSettings.tsx @@ -12,11 +12,14 @@ import { useAccount } from 'hooks'; import { Button } from 'primaries/SharedStyling'; import { ImageV2 } from 'components/reusables/SharedStylingV2'; import { updateBulkSubscriptions, updateBulkUserSettings } from 'redux/slices/channelSlice'; +import UserProfileSettings from 'components/UserProfileSettings/UserProfileSettings'; // Internal Configs import { device } from 'config/Globals'; import ChannelListSettings from 'components/channel/ChannelListSettings'; import PushSnapSettings from 'components/PushSnap/PushSnapSettings'; +import { Alert, Box } from 'blocks'; +import UserProfileSocialSettings from 'components/UserProfileSettings/UserProfileSocialSettings'; interface ChannelListItem { channel: string; @@ -37,6 +40,10 @@ function UserSettings() { const [channelList, setChannelList] = useState([]); const [isLoading, setIsLoading] = useState(true); + // for alerts + const [errorMessage, setErrorMessage] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + const navigate = useNavigate(); const dispatch = useDispatch(); @@ -95,11 +102,16 @@ function UserSettings() { const selectOptions = [ { value: 0, + label: 'My Profile', + title: 'Your Profile', + }, + { + value: 1, label: 'Notification Settings', title: 'Notification Settings', }, { - value: 1, + value: 2, label: 'Push Snap', title: '', }, @@ -109,6 +121,7 @@ function UserSettings() { Settings Customize your Push profile or manage your notification preferences + {selectOptions.map((selectOptions) => ( @@ -121,16 +134,57 @@ function UserSettings() { ))} - - - {selectOptions[selectedOption]?.title && ( - {selectOptions[selectedOption]?.title} - )} - - {selectedOption === 0 && } - {selectedOption === 1 && } - - + + + {successMessage && ( + + + + )} + + {errorMessage && ( + + + + )} + + + {selectOptions[selectedOption]?.title && ( + {selectOptions[selectedOption]?.title} + )} + + {selectedOption === 0 && ( + + )} + {selectedOption === 1 && } + {selectedOption === 2 && } + + + + {selectedOption == 0 && ( + + + + + + )} + ); @@ -142,6 +196,8 @@ export default UserSettings; const Container = styled.div` padding: 32px 24px; flex: 1; + height: 100%; + overflow: hidden; @media ${device.tablet} { padding: 24px 12px; @@ -182,6 +238,10 @@ const Wrapper = styled.div` flex-direction: row; justify-content: space-between; + height: 100%; + flex: 1; + min-height: 0; + @media ${device.tablet} { flex-direction: column; } @@ -220,10 +280,9 @@ const SelectListOption = styled(Button)<{ isSelected: boolean }>` `; const ChannelWrapper = styled.div` - border: 1px solid ${(props) => props.theme.default.borderColor}; + border: 1px solid ${(props) => props.theme.userSettingsBorder}; padding: 12px; - border-radius: 16px; - flex-grow: 1; + border-radius: 24px; @media ${device.tablet} { margin: 8px 0px; @@ -231,10 +290,45 @@ const ChannelWrapper = styled.div` } `; -const ChannelContainer = styled.div` - overflow: hidden; - overflow-y: scroll; - height: 55vh; +const ChannelBlock = styled.div` + display: flex; + flex-direction: column; + flex-grow: 1; + min-height: 0; + gap: 16px; + padding-right: 12px; + overflow-y: auto; + + &::-webkit-scrollbar-track { + background-color: transparent; + position: absolute; + right: 10px; + } + + &::-webkit-scrollbar { + background-color: transparent; + width: 4px; + position: absolute; + right: 10px; + } + + &::-webkit-scrollbar-thumb { + background-color: #d53a94; + border-radius: 99px; + width: 4px; + position: absolute; + right: 10px; + } + + // Adding margin-bottom to the last child + & > *:last-child { + margin-bottom: 100px; + } +`; + +const ChannelContainer = styled.div<{ selectedOption: number }>` + overflow-y: auto; + height: ${(props) => (props.selectedOption === 0 ? 'auto' : '55vh')}; padding: 12px; &::-webkit-scrollbar-track { diff --git a/src/config/Themization.js b/src/config/Themization.js index 8a1933a90e..7194737191 100644 --- a/src/config/Themization.js +++ b/src/config/Themization.js @@ -311,6 +311,9 @@ const themeLight = { //Connect Wallet and Push user Flow Modal userSecText: '#8C93A0', disabledBtnColor: '#E5E5E5', + + //userSettings + userSettingsBorder: '#EAEBF2', }; const themeDark = { @@ -627,6 +630,9 @@ const themeDark = { //Connect Wallet and Push user Flow Modal userSecText: '#484D58', disabledBtnColor: '#484D58', + + //userSettings + userSettingsBorder: '#313338', }; export { themeDark, themeLight }; diff --git a/src/config/config-alpha.js b/src/config/config-alpha.js index 0b8eec2949..d89bb6cf42 100644 --- a/src/config/config-alpha.js +++ b/src/config/config-alpha.js @@ -80,6 +80,10 @@ export const config = { extension: 'https://chrome.google.com/webstore/detail/epns-protocol-beta/lbdcbpaldalgiieffakjhiccoeebchmg', howto: 'https://push.org/docs', }, + + // social media integration + telegramExternalURL: 'https://web.telegram.org/k/#@PushCommDevBot', + discordExternalURL: null, }; /** diff --git a/src/config/config-dev.js b/src/config/config-dev.js index 185a95759c..e3bfdb022c 100644 --- a/src/config/config-dev.js +++ b/src/config/config-dev.js @@ -81,6 +81,10 @@ export const config = { extension: 'https://chrome.google.com/webstore/detail/epns-staging-protocol-alp/bjiennpmhdcandkpigcploafccldlakj', howto: 'https://push.org/docs', }, + + // social media integration + telegramExternalURL: 'https://web.telegram.org/k/#@PushCommDevBot', + discordExternalURL: null, }; /** diff --git a/src/config/config-localhost.js b/src/config/config-localhost.js index 686b871736..0f610200e2 100644 --- a/src/config/config-localhost.js +++ b/src/config/config-localhost.js @@ -68,6 +68,10 @@ export const config = { extension: 'https://chrome.google.com/webstore/detail/epns-staging-protocol-alp/bjiennpmhdcandkpigcploafccldlakj', howto: 'https://push.org/docs', }, + + // social media integration + telegramExternalURL: 'https://web.telegram.org/k/#@PushCommDevBot', + discordExternalURL: null, }; /** diff --git a/src/config/config-prod.js b/src/config/config-prod.js index 1ff2de8ea9..ab0d981f89 100644 --- a/src/config/config-prod.js +++ b/src/config/config-prod.js @@ -81,6 +81,10 @@ export const config = { extension: 'https://chrome.google.com/webstore/detail/epns-protocol-beta/lbdcbpaldalgiieffakjhiccoeebchmg', howto: 'https://push.org/docs', }, + + // social media integration + telegramExternalURL: 'https://web.telegram.org/k/#@PushCommBot', + discordExternalURL: null, }; /** diff --git a/src/config/config-staging.js b/src/config/config-staging.js index 6d8e3e6daa..6c769f72a4 100644 --- a/src/config/config-staging.js +++ b/src/config/config-staging.js @@ -83,6 +83,10 @@ export const config = { extension: 'https://chrome.google.com/webstore/detail/epns-staging-protocol-alp/bjiennpmhdcandkpigcploafccldlakj', howto: 'https://push.org/docs', }, + + // social media integration + telegramExternalURL: 'https://web.telegram.org/k/#@PushCommDevBot', + discordExternalURL: null, }; /** diff --git a/src/modules/dashboard/Dashboard.constants.ts b/src/modules/dashboard/Dashboard.constants.ts index 4a9342b255..1e50bdacc2 100644 --- a/src/modules/dashboard/Dashboard.constants.ts +++ b/src/modules/dashboard/Dashboard.constants.ts @@ -1,6 +1,6 @@ import { PushAlpha, PushBot, PushDev } from 'blocks'; -import { ChatType, EnvKeys, SourceKeys } from './Dashboard.types'; +import { ChatType, EnvKeys, SocialHandlesItemType, SourceKeys } from './Dashboard.types'; export const recommendedChatList: ChatType[] = [ { diff --git a/src/modules/dashboard/Dashboard.tsx b/src/modules/dashboard/Dashboard.tsx index d1db76e55b..9272e422e9 100644 --- a/src/modules/dashboard/Dashboard.tsx +++ b/src/modules/dashboard/Dashboard.tsx @@ -1,19 +1,55 @@ // React and other libraries import { FC, useState } from 'react'; +// Hooks +import { useAccount } from 'hooks'; +import { walletToCAIP10 } from 'helpers/w2w'; +import { RewardActivityStatus, useGetRewardsActivity, useGetUserRewardsDetails } from 'queries'; + // Components -import { Box } from 'blocks'; +import { Alert, Box, Button, Link, Skeleton } from 'blocks'; import { AnalyticsOverview } from './components/AnalyticsOverview'; import { ChannelVariantsSection } from './components/ChannelVariantsSection'; import { DashboardHeader } from './components/DashboardHeader'; import { DashboardSubHeader } from './components/DashboardSubHeader'; import { FeaturedChannels } from './components/FeaturedChannels'; import { StakingPools } from './components/StakingPools'; +import { SocialHandles } from './components/Socialhandles'; export type DashboardProps = {}; const Dashboard: FC = () => { + const { isWalletConnected, account } = useAccount(); + + // Getting user Id by wallet address + const caip10WalletAddress = walletToCAIP10({ account }); + const { data: userDetails } = useGetUserRewardsDetails({ + caip10WalletAddress: caip10WalletAddress, + }); + const [showSubHeader, setSubHeaderVisibility] = useState(true); + // for alerts + const [errorMessage, setErrorMessage] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + + const activityType = ['notifications_integration_email_telegram_discord']; + const { + data: emailTelegramIntegrationStatus, + isLoading: isActivitiesLoading, + // refetch: refetchActivity, + } = useGetRewardsActivity( + { userId: userDetails?.userId as string, activityTypes: activityType }, + { enabled: !!userDetails?.userId && activityType.length > 0 } + ); + + // Type Guard to check if an object is RewardActivityStatus + const isRewardActivityStatus = (obj: any): obj is RewardActivityStatus => { + return obj && typeof obj.status === 'string'; + }; + + const hasUserClaimedEmailTelegramIntegration = + isRewardActivityStatus(emailTelegramIntegrationStatus?.notifications_integration_email_telegram_discord) && + emailTelegramIntegrationStatus.notifications_integration_email_telegram_discord.status === 'COMPLETED'; return ( = () => { flexDirection="column" gap="spacing-md" > + {successMessage && ( + + + + )} + + {errorMessage && ( + + + + )} + + {isWalletConnected && ( + + {hasUserClaimedEmailTelegramIntegration ? ( + + ) : ( + + + + )} + + } + /> + )} = ({ claimButton }) => { + return ( + + + + + Get notified anywhere and earn points + + }>NEW + + + Connect apps and receive notifications directly in your Email, Telegram and Discord + + + + + + + + 1.5x + + + + + + 25,000 + + + {claimButton} + + + ); +}; + +export { ClaimSocialHandles }; diff --git a/src/modules/dashboard/components/ConnectSocialHandles.tsx b/src/modules/dashboard/components/ConnectSocialHandles.tsx new file mode 100644 index 0000000000..df2e551ac1 --- /dev/null +++ b/src/modules/dashboard/components/ConnectSocialHandles.tsx @@ -0,0 +1,125 @@ +import { FC } from 'react'; + +import { Box, Button, Text, Tick, Skeleton } from 'blocks'; +import { AddEmail } from 'components/UserProfileSettings/AddEmail'; +import AddDiscord from 'components/UserProfileSettings/AddDiscord'; +import AddTelegram from 'components/UserProfileSettings/AddTelegram'; +import { useSocialHandles } from '../hooks/useSocialHandles'; + +export type ConnectSocialHandlesProps = { + setErrorMessage: (errorMessage: string) => void; + setSuccessMessage: (successMessage: string) => void; +}; + +const ConnectSocialHandles: FC = ({ setErrorMessage, setSuccessMessage }) => { + const { socialHandlesList, modalControl, telegramModalControl, discordModalControl, isPending, fetchStatus } = + useSocialHandles(setErrorMessage, false); + + return ( + <> + + {socialHandlesList?.map((item) => ( + + + + {item?.icon()} + + + {item.itemTitle} + + + + + {item.itemDescription} + + + + + + {item.userStatus ? ( + + ) : ( + + )} + + + + ))} + + {modalControl.isOpen && ( + + )} + + {telegramModalControl.isOpen && ( + + )} + + {discordModalControl.isOpen && ( + + )} + + + ); +}; + +export { ConnectSocialHandles }; diff --git a/src/modules/dashboard/components/Socialhandles.tsx b/src/modules/dashboard/components/Socialhandles.tsx new file mode 100644 index 0000000000..79e72a9b03 --- /dev/null +++ b/src/modules/dashboard/components/Socialhandles.tsx @@ -0,0 +1,39 @@ +import { FC, ReactNode } from 'react'; + +import { BlocksSpaceType, Box, ResponsiveProp } from 'blocks'; +import { ClaimSocialHandles } from './ClaimSocialHandles'; +import { ConnectSocialHandles } from './ConnectSocialHandles'; + +export type SocialHandlesProps = { + errorMessage?: string; + setErrorMessage: (errorMessage: string) => void; + successMessage?: string; + setSuccessMessage: (successMessage: string) => void; + padding?: ResponsiveProp; + claimButton?: ReactNode; +}; + +const SocialHandles: FC = ({ setErrorMessage, setSuccessMessage, padding, claimButton }) => { + return ( + + {/* Render Claim based on Social Handles if wallet is connected */} + + + {/* Render option to connect Social Handles */} + + + ); +}; + +export { SocialHandles }; diff --git a/src/modules/dashboard/hooks/useSocialHandles.tsx b/src/modules/dashboard/hooks/useSocialHandles.tsx new file mode 100644 index 0000000000..510d6dd744 --- /dev/null +++ b/src/modules/dashboard/hooks/useSocialHandles.tsx @@ -0,0 +1,110 @@ +import React, { useState, useEffect } from 'react'; + +import { useDisclosure } from 'common'; +import { useGetSocialsStatus } from 'queries'; +import { useAccount } from 'hooks'; + +import { walletToCAIP10 } from 'helpers/w2w'; +import { appConfig } from 'config'; +import { generateVerificationProof } from 'modules/rewards/utils/generateVerificationProof'; + +import { PushAPI } from '@pushprotocol/restapi'; + +import { DiscordProfile, EmailProfile, TelegramProfile } from 'blocks'; + +export type SocialHandleStatusType = { + email: string | null; + discord_username: string | null; + telegram_username: string | null; +}; + +type SocialHandlesItemType = { + icon: () => React.JSX.Element; + itemTitle: string; + itemDescription: string; + onClick: () => void; + userStatus: string | null; +}; + +export const useSocialHandles = ( + setErrorMessage: (error: string) => void, + requiresVerificationProof: boolean, + userPushSDKInstance?: PushAPI +) => { + const modalControl = useDisclosure(); + const telegramModalControl = useDisclosure(); + const discordModalControl = useDisclosure(); + const { account } = useAccount(); + const [socialHandleStatus, setSocialHandleStatus] = useState(null); + + const channelAddress = walletToCAIP10({ account }); + const { mutate: fetchSocialStatus, isPending } = useGetSocialsStatus(); + + const fetchStatus = async () => { + if (!channelAddress) return; + + let verificationProof; + + if (requiresVerificationProof) { + const data = { + wallet: channelAddress, + }; + verificationProof = await generateVerificationProof(data, userPushSDKInstance); // Pass your SDK instance if required + if (!verificationProof) return; + } + + fetchSocialStatus( + { channelAddress, ...(requiresVerificationProof && { verificationProof: verificationProof as string }) }, + { + onError: (error) => { + setErrorMessage('Failed to fetch social status.'); + console.error('Error fetching social status:', error); + }, + onSuccess: (data) => { + setSocialHandleStatus(data); + }, + } + ); + }; + + useEffect(() => { + if (channelAddress && !requiresVerificationProof) { + fetchStatus(); + } + }, [channelAddress]); + + const socialHandlesList: SocialHandlesItemType[] = [ + { + icon: () => , + itemTitle: 'Email', + itemDescription: 'Receive notifications in your email inbox', + onClick: () => modalControl.open(), + userStatus: socialHandleStatus?.email || null, + }, + appConfig?.telegramExternalURL && { + icon: () => , + itemTitle: 'Telegram', + itemDescription: 'Receive notifications as Telegram messages', + onClick: () => telegramModalControl.open(), + userStatus: socialHandleStatus?.telegram_username || null, + }, + appConfig?.discordExternalURL && { + icon: () => , + itemTitle: 'Discord', + itemDescription: 'Receive notifications as Discord messages', + onClick: () => discordModalControl.open(), + userStatus: socialHandleStatus?.discord_username || null, + }, + ].filter(Boolean); + + return { + socialHandlesList, + socialHandleStatus, + modalControl, + telegramModalControl, + discordModalControl, + isPending, + fetchStatus, + channelAddress, + }; +}; diff --git a/src/modules/rewards/components/RewardsActivitiesBottomSection.tsx b/src/modules/rewards/components/RewardsActivitiesBottomSection.tsx index 84c3d1f750..54b96a0aae 100644 --- a/src/modules/rewards/components/RewardsActivitiesBottomSection.tsx +++ b/src/modules/rewards/components/RewardsActivitiesBottomSection.tsx @@ -17,16 +17,7 @@ const RewardsActivitiesBottomSection: FC = gap="spacing-lg" margin="spacing-none spacing-none spacing-md spacing-none" > - - - + = () => { ? Array(2).fill(0) : activityList.filter((activity) => activity.index.startsWith(`social-activity`) && activity?.status === 'ENABLED'); + const emailTelegramActivities = activityList.filter( + (activity) => activity.index.startsWith(`custom-delivery`) && activity?.status === 'ENABLED' + )[0]; + const platformRewardActivities = isLoading ? Array(7).fill(0) : activityList.filter((activity) => activity.index.startsWith(`reward-activity`) && activity?.status === 'ENABLED'); - const channelSubscriptionActivities = activityList.filter( - (activity) => activity.index.startsWith(`channel-subscription`) && activity?.status === 'ENABLED' + const channelSubscriptionActivities = activityList.filter((activity) => + activity.index.startsWith(`channel-subscription`) ); const { isLocked } = useRewardsContext(); // Combine all activities into a single array - const allActivities = [...socialActivities, ...platformRewardActivities, ...channelSubscriptionActivities]; + const allActivities = [ + ...socialActivities, + ...platformRewardActivities, + ...channelSubscriptionActivities, + emailTelegramActivities, + ]; // Extract the `activityType` from each activity and filter out any undefined values const activityTypes = allActivities - .map((activity) => activity.activityType) // Extract `activityType` + .map((activity) => activity?.activityType) // Extract `activityType` .filter(Boolean); // Remove undefined/null values const { @@ -70,84 +80,122 @@ const RewardsActivitiesList: FC = () => { - {/* These are the social activites Twitter and discord */} - {socialActivities.map((activity: Activity) => ( - - ))} - {(isLocked || !isWalletConnected) && ( - + - + + {/* These are the social activites Twitter and discord */} + {socialActivities.map((activity: Activity) => ( + - - Verify X and Discord to unlock more activities - - - )} - - {/* Activites related specific channel subscription */} - {channelSubscriptionActivities.map((activity: Activity) => ( - - ))} - - {/* These are other platform specifc reward activities */} - {platformRewardActivities.map((activity: Activity) => ( - + + Verify X and Discord to unlock more activities + + + )} + + - ))} + + + + {/* Activites related specific channel subscription */} + {channelSubscriptionActivities.map((activity: Activity) => ( + + ))} + + {/* These are other platform specifc reward activities */} + {platformRewardActivities.map((activity: Activity) => ( + + ))} + ); }; diff --git a/src/modules/rewards/components/RewardsActivitiesSection.tsx b/src/modules/rewards/components/RewardsActivitiesSection.tsx index 9ee6e43d7d..707b7f54b4 100644 --- a/src/modules/rewards/components/RewardsActivitiesSection.tsx +++ b/src/modules/rewards/components/RewardsActivitiesSection.tsx @@ -1,22 +1,7 @@ -import { Box, Text } from 'blocks'; import { RewardsActivitiesList } from './RewardsActivitiesList'; const RewardsActivitiesSection = () => { - return ( - - - Activities - - - - ); + return ; }; export { RewardsActivitiesSection }; diff --git a/src/modules/rewards/components/SocialHandleItem.tsx b/src/modules/rewards/components/SocialHandleItem.tsx new file mode 100644 index 0000000000..88eeaf945b --- /dev/null +++ b/src/modules/rewards/components/SocialHandleItem.tsx @@ -0,0 +1,118 @@ +import { FC, useEffect, useMemo, useState } from 'react'; + +import { Activity, StakeActivityResponse, UsersActivity } from 'queries'; +import { useAccount } from 'hooks'; +import useLockedStatus from '../hooks/useLockedStatus'; + +import { Alert, Box, Button, Skeleton } from 'blocks'; +import { ActivityButton } from './ActivityButton'; +import { SocialHandles } from 'modules/dashboard/components/Socialhandles'; + +export type SocialHandleItemFCType = { + userId: string; + activity: Activity; + isLoadingItem: boolean; + isLocked: boolean; + allUsersActivity: StakeActivityResponse; + isAllActivitiesLoading: boolean; + refetchActivity: () => void; +}; + +const SocialHandleItem: FC = ({ + userId, + activity, + isLoadingItem, + isLocked, + allUsersActivity, + isAllActivitiesLoading, + refetchActivity, +}) => { + const { isWalletConnected } = useAccount(); + const usersSingleActivity = allUsersActivity?.[activity?.activityType] as UsersActivity; + const isLoading = isAllActivitiesLoading; + + // for alerts + const [errorMessage, setErrorMessage] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + const { refetchRecentActivities, getLockStatus, statusRecentActivities } = useLockedStatus(); + + const isRewardsLocked = useMemo(() => { + return ( + (isLocked || !isWalletConnected) && + activity?.activityType !== 'follow_push_on_discord' && + activity?.activityType !== 'follow_push_on_twitter' + ); + }, [isLocked, isWalletConnected, activity?.activityType]); + + const updateActivities = () => { + refetchActivity(); + refetchRecentActivities(); + }; + + // if activityType is twitter or discord, then re-call check lock status fn + useEffect(() => { + if (activity?.activityType == 'follow_push_on_discord' || activity?.activityType == 'follow_push_on_twitter') { + getLockStatus(); + } + }, [usersSingleActivity?.status, activity?.activityType, statusRecentActivities]); + + return ( + + + {successMessage && ( + + + + )} + + {errorMessage && ( + + + + )} + + + Locked + + ) : ( + updateActivities()} + setErrorMessage={setErrorMessage} + usersSingleActivity={usersSingleActivity} + isLoadingActivity={isLoading} + label={'Claim'} + /> + ) + } + /> + + + ); +}; + +export default SocialHandleItem; diff --git a/src/modules/rewards/utils/activityTypeArray.ts b/src/modules/rewards/utils/activityTypeArray.ts index 16acfc5963..73ffc620c2 100644 --- a/src/modules/rewards/utils/activityTypeArray.ts +++ b/src/modules/rewards/utils/activityTypeArray.ts @@ -9,6 +9,7 @@ export const otherRewardActivities: ActvityType[] = [ 'active_push_chat_user', 'hold_push_alpha_access_nft', 'hold_push_rockstar_nft', + 'notifications_integration_email_telegram_discord', ]; export const channelSubscriptionActivities: ActvityType[] = [ diff --git a/src/modules/userSettings/UserSettingsModule.tsx b/src/modules/userSettings/UserSettingsModule.tsx index 4571d806b5..4159494578 100644 --- a/src/modules/userSettings/UserSettingsModule.tsx +++ b/src/modules/userSettings/UserSettingsModule.tsx @@ -7,7 +7,6 @@ import styled from 'styled-components'; // Internal Compoonents import UserSettings from 'components/userSettings/UserSettings'; - // Internal Configs import GLOBALS, { device, globalsMargin } from 'config/Globals'; @@ -18,7 +17,7 @@ const UserSettingsModule = () => { ); -} +}; // css styles const Container = styled.div` @@ -44,8 +43,8 @@ const Container = styled.div` @media ${device.laptop} { margin: ${GLOBALS.ADJUSTMENTS.MARGIN.MINI_MODULES.TABLET}; height: calc(100vh - ${GLOBALS.CONSTANTS.HEADER_HEIGHT}px - ${globalsMargin.MINI_MODULES.TABLET.TOP} - ${ - globalsMargin.MINI_MODULES.TABLET.BOTTOM - }); + globalsMargin.MINI_MODULES.TABLET.BOTTOM +}); } @media ${device.mobileL} { diff --git a/src/queries/baseURL.ts b/src/queries/baseURL.ts index 961f354b19..799d0a3d70 100644 --- a/src/queries/baseURL.ts +++ b/src/queries/baseURL.ts @@ -20,4 +20,17 @@ export const getRewardsBaseURL = () => { } }; +export const getCustomDeliveryURL = () => { + switch (appConfig.appEnv) { + case 'prod': + return `https://custom-delivery.push.org`; + case 'staging': + return `https://custom-delivery-dev.push.org`; + case 'dev': + return `https://custom-delivery-dev.push.org`; + default: + return `https://custom-delivery-dev.push.org`; + } +}; + export const analyticsBaseURL = 'https://backend.epns.io/apis/v1'; diff --git a/src/queries/hooks/user/index.ts b/src/queries/hooks/user/index.ts index 9df9805f44..8b2966f7e5 100644 --- a/src/queries/hooks/user/index.ts +++ b/src/queries/hooks/user/index.ts @@ -2,4 +2,9 @@ export * from './useGetUserSubscriptions'; export * from './useSubscribeChannel'; export * from './useUnsubscribeChannel'; export * from './useUpdateNotificationSettings'; +export * from './useGetUserProfileInfo'; +export * from './useUpdateUserProfileInfo'; +export * from './useSendHandlesVerificationCode'; +export * from './useVerifyHandlesVerificationCode'; +export * from './useGetSocialsStatus'; export * from './useGetUserProfileDetails'; diff --git a/src/queries/hooks/user/useGetSocialsStatus.ts b/src/queries/hooks/user/useGetSocialsStatus.ts new file mode 100644 index 0000000000..2090a9a18d --- /dev/null +++ b/src/queries/hooks/user/useGetSocialsStatus.ts @@ -0,0 +1,10 @@ +import { useMutation } from '@tanstack/react-query'; + +import { userSocialStatus } from '../../queryKeys'; +import { getUserSocialsStatus } from '../../services'; + +export const useGetSocialsStatus = () => + useMutation({ + mutationKey: [userSocialStatus], + mutationFn: getUserSocialsStatus, + }); diff --git a/src/queries/hooks/user/useGetUserProfileInfo.ts b/src/queries/hooks/user/useGetUserProfileInfo.ts new file mode 100644 index 0000000000..b006400904 --- /dev/null +++ b/src/queries/hooks/user/useGetUserProfileInfo.ts @@ -0,0 +1,22 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query'; +import { useSelector } from 'react-redux'; + +import { userProfileInfo } from '../../queryKeys'; +import { getUserProfileInfo } from '../../services'; + +//Types +import { UserStoreType } from 'types'; +import { UserProfileInfoResponse } from 'queries/types'; + +export const useGetUserProfileInfo = (config?: Partial>) => { + const { userPushSDKInstance } = useSelector((state: UserStoreType) => { + return state.user; + }); + + const query = useQuery({ + queryKey: [userProfileInfo, userPushSDKInstance?.account], + queryFn: () => getUserProfileInfo(userPushSDKInstance), + ...config, + }); + return query; +}; diff --git a/src/queries/hooks/user/useSendHandlesVerificationCode.ts b/src/queries/hooks/user/useSendHandlesVerificationCode.ts new file mode 100644 index 0000000000..0fc70dcaea --- /dev/null +++ b/src/queries/hooks/user/useSendHandlesVerificationCode.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query'; +import { sendVerificationCode } from 'queries/queryKeys'; +import { sendHandlesVerificationCode } from 'queries/services'; + +export const useSendHandlesVerificationCode = () => + useMutation({ + mutationKey: [sendVerificationCode], + mutationFn: sendHandlesVerificationCode, + }); diff --git a/src/queries/hooks/user/useUpdateUserProfileInfo.ts b/src/queries/hooks/user/useUpdateUserProfileInfo.ts new file mode 100644 index 0000000000..34ae314c51 --- /dev/null +++ b/src/queries/hooks/user/useUpdateUserProfileInfo.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query'; +import { updateUserProfileDetails } from 'queries/queryKeys'; +import { updateUserProfileInfo } from 'queries/services'; + +export const useUpdateUserProfileInfo = () => + useMutation({ + mutationKey: [updateUserProfileDetails], + mutationFn: updateUserProfileInfo, + }); diff --git a/src/queries/hooks/user/useVerifyHandlesVerificationCode.ts b/src/queries/hooks/user/useVerifyHandlesVerificationCode.ts new file mode 100644 index 0000000000..c764283845 --- /dev/null +++ b/src/queries/hooks/user/useVerifyHandlesVerificationCode.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query'; +import { verifyVerificationCode } from 'queries/queryKeys'; +import { verifyHandlesVerificationCode } from 'queries/services'; + +export const useVerifyHandlesVerificationCode = () => + useMutation({ + mutationKey: [verifyVerificationCode], + mutationFn: verifyHandlesVerificationCode, + }); diff --git a/src/queries/models/user/getSocialsStatusModelCreator.ts b/src/queries/models/user/getSocialsStatusModelCreator.ts new file mode 100644 index 0000000000..9c4806ce53 --- /dev/null +++ b/src/queries/models/user/getSocialsStatusModelCreator.ts @@ -0,0 +1,3 @@ +import { UserSocialStatusResponse } from 'queries/types'; + +export const getSocialsStatusModelCreator = (response: UserSocialStatusResponse): UserSocialStatusResponse => response; diff --git a/src/queries/models/user/getUserProfileInfoModelCreator.ts b/src/queries/models/user/getUserProfileInfoModelCreator.ts new file mode 100644 index 0000000000..a34881fef0 --- /dev/null +++ b/src/queries/models/user/getUserProfileInfoModelCreator.ts @@ -0,0 +1,4 @@ +import { UserProfileInfoResponse } from 'queries/types'; + +//any remodelling needed in the response can be done here +export const getUserProfileInfoModelCreator = (response: UserProfileInfoResponse): UserProfileInfoResponse => response; diff --git a/src/queries/models/user/index.ts b/src/queries/models/user/index.ts index ce1d94afca..49ad9da1f6 100644 --- a/src/queries/models/user/index.ts +++ b/src/queries/models/user/index.ts @@ -1,2 +1,6 @@ export * from './getUserSubscriptionsModelCreator'; +export * from './getUserProfileInfoModelCreator'; +export * from './sendHandlesVerificationCodeModelCreator'; +export * from './verifyHandlesVerificationCodeModelCreator'; +export * from './getSocialsStatusModelCreator'; export * from './getUserProfileDetailsModelCreator'; diff --git a/src/queries/models/user/sendHandlesVerificationCodeModelCreator.ts b/src/queries/models/user/sendHandlesVerificationCodeModelCreator.ts new file mode 100644 index 0000000000..2eb7adb782 --- /dev/null +++ b/src/queries/models/user/sendHandlesVerificationCodeModelCreator.ts @@ -0,0 +1,5 @@ +import { SendHandlesVerificationResponse } from 'queries/types'; + +export const sendHandlesVerificationCodeModelCreator = ( + response: SendHandlesVerificationResponse +): SendHandlesVerificationResponse => response; diff --git a/src/queries/models/user/verifyHandlesVerificationCodeModelCreator.ts b/src/queries/models/user/verifyHandlesVerificationCodeModelCreator.ts new file mode 100644 index 0000000000..276e96e7d4 --- /dev/null +++ b/src/queries/models/user/verifyHandlesVerificationCodeModelCreator.ts @@ -0,0 +1,5 @@ +import { VerifyHandlesVerificationResponse } from 'queries/types'; + +export const verifyHandlesVerificationCodeModelCreator = ( + response: VerifyHandlesVerificationResponse +): VerifyHandlesVerificationResponse => response; diff --git a/src/queries/queryKeys.ts b/src/queries/queryKeys.ts index b105f97615..6b531599c0 100644 --- a/src/queries/queryKeys.ts +++ b/src/queries/queryKeys.ts @@ -36,6 +36,8 @@ export const rewardsLeaderboard = 'rewardsLeaderboard'; export const sendNotification = 'sendNotification'; export const sentMessageCount = 'sentMessageCount'; export const sentNotificationCount = 'sentNotificationCount'; +export const sendVerificationCode = 'sendVerificationCode'; +export const verifyVerificationCode = 'verifyVerificationCode'; export const subscribe = 'subscribe'; export const subscriberCount = 'subscriberCount'; export const trendingChannels = 'trendingChannels'; @@ -43,8 +45,11 @@ export const uniV2StakeEpoch = 'uniV2StakeEpoch'; export const unsubscribe = 'unsubscribe'; export const updateChannelDetails = 'updateChannelDetails'; export const updatingNotificationSetting = 'updatingNotificationSetting'; +export const userProfileInfo = 'userProfileInfo'; +export const updateUserProfileDetails = 'updateUserProfileDetails'; export const userRewardsDetails = 'userRewardsDetails'; export const UserRewardsDetails = 'userRewardsDetails'; +export const userSocialStatus = 'userSocialStatus'; export const userSubscription = 'userSubscription'; export const userTwitterDetails = 'userTwitterDetails'; export const userProfileDetails = 'userProfileDetails'; diff --git a/src/queries/services/user/getUserProfileInfo.ts b/src/queries/services/user/getUserProfileInfo.ts new file mode 100644 index 0000000000..d2130c62a2 --- /dev/null +++ b/src/queries/services/user/getUserProfileInfo.ts @@ -0,0 +1,5 @@ +import { PushAPI } from '@pushprotocol/restapi'; +import { getUserProfileInfoModelCreator } from 'queries/models'; + +export const getUserProfileInfo = (userPushSDKInstance: PushAPI) => + userPushSDKInstance.profile.info().then(getUserProfileInfoModelCreator); diff --git a/src/queries/services/user/getUserSocialsStatus.ts b/src/queries/services/user/getUserSocialsStatus.ts new file mode 100644 index 0000000000..50914e7aac --- /dev/null +++ b/src/queries/services/user/getUserSocialsStatus.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; +import { getCustomDeliveryURL } from 'queries/baseURL'; +import { getSocialsStatusModelCreator } from 'queries/models'; + +type SocialStatusType = { + channelAddress: string; + verificationProof?: string; +}; + +export const getUserSocialsStatus = async (payload: SocialStatusType) => { + const response = await axios({ + method: 'POST', + url: `${getCustomDeliveryURL()}/apis/v1/users/${payload?.channelAddress}`, + data: { + verificationProof: payload?.verificationProof, + }, + }); + return getSocialsStatusModelCreator(response.data); +}; diff --git a/src/queries/services/user/index.ts b/src/queries/services/user/index.ts index da46eb1d04..75b30c5c83 100644 --- a/src/queries/services/user/index.ts +++ b/src/queries/services/user/index.ts @@ -2,4 +2,9 @@ export * from './getUserSubscriptions'; export * from './subscribeToChannel'; export * from './unsubscribeChannel'; export * from './updateNotificationSettings'; +export * from './getUserProfileInfo'; +export * from './updateUserProfileInfo'; +export * from './sendHandlesVerificationCode'; +export * from './verifyHandlesVerificationCode'; +export * from './getUserSocialsStatus'; export * from './getUserProfileDetails'; diff --git a/src/queries/services/user/sendHandlesVerificationCode.ts b/src/queries/services/user/sendHandlesVerificationCode.ts new file mode 100644 index 0000000000..925d3b356c --- /dev/null +++ b/src/queries/services/user/sendHandlesVerificationCode.ts @@ -0,0 +1,26 @@ +import axios from 'axios'; +import { getCustomDeliveryURL } from 'queries/baseURL'; + +import { sendHandlesVerificationCodeModelCreator } from 'queries/models'; + +type sendHandlesVerificationCodeType = { + caipAddress: string; + value: string | { telegram_username: string } | { discord_username: string }; + verificationProof: string; + social_platform: 'email' | 'discord' | 'telegram'; +}; + +export const sendHandlesVerificationCode = async (payload: sendHandlesVerificationCodeType) => { + const response = await axios({ + method: 'POST', + url: `${getCustomDeliveryURL()}/apis/v1/users/verify/init/${payload?.caipAddress}/${payload.social_platform}`, + data: { + value: payload?.value, + verificationProof: payload?.verificationProof, + }, + headers: { + 'Content-Type': 'application/json', + }, + }); + return sendHandlesVerificationCodeModelCreator(response.data); +}; diff --git a/src/queries/services/user/updateUserProfileInfo.ts b/src/queries/services/user/updateUserProfileInfo.ts new file mode 100644 index 0000000000..0bb47b9cfb --- /dev/null +++ b/src/queries/services/user/updateUserProfileInfo.ts @@ -0,0 +1,24 @@ +import { PushAPI } from '@pushprotocol/restapi'; +import { UpdateProfileInfoResponse } from 'queries/types'; + +// Assuming PushAPI.Profile is the type you want to use for userPushSDKInstance +type UpdateUserProfileParams = { + userPushSDKInstance: PushAPI; + name: string | null; + desc: string | null; + picture: string | null; +}; + +export const updateUserProfileInfo = async ({ + userPushSDKInstance, + name, + desc, + picture, +}: UpdateUserProfileParams): Promise => { + const res = await userPushSDKInstance.profile.update({ + name: name as string, + desc: desc ? desc : '', + picture: picture as string, + }); + return res; +}; diff --git a/src/queries/services/user/verifyHandlesVerificationCode.ts b/src/queries/services/user/verifyHandlesVerificationCode.ts new file mode 100644 index 0000000000..c6d7d128e8 --- /dev/null +++ b/src/queries/services/user/verifyHandlesVerificationCode.ts @@ -0,0 +1,26 @@ +import axios from 'axios'; +import { getCustomDeliveryURL } from 'queries/baseURL'; + +import { verifyHandlesVerificationCodeModelCreator } from 'queries/models'; + +type verifyHandlesVerificationCodeType = { + caipAddress: string; + value: string; + verificationCode: string; + social_platform: 'email' | 'discord' | 'telegram'; +}; + +export const verifyHandlesVerificationCode = async (payload: verifyHandlesVerificationCodeType) => { + const response = await axios({ + method: 'POST', + url: `${getCustomDeliveryURL()}/apis/v1/users/verify/${payload?.caipAddress}/${payload?.social_platform}`, + data: { + value: payload?.value, + verificationCode: payload?.verificationCode, + }, + headers: { + 'Content-Type': 'application/json', + }, + }); + return verifyHandlesVerificationCodeModelCreator(response.data); +}; diff --git a/src/queries/types/rewards.ts b/src/queries/types/rewards.ts index 2a8a9cb283..cb206316aa 100644 --- a/src/queries/types/rewards.ts +++ b/src/queries/types/rewards.ts @@ -9,6 +9,7 @@ export type RewardsAcitivitesResponse = { export type ActvityType = | 'follow_push_on_discord' | 'follow_push_on_twitter' + | 'notifications_integration_email_telegram_discord' | 'create_gated_group_push_chat' | 'subscribe_5_channels_push' | 'subscribe_20_channels_push' diff --git a/src/queries/types/user.ts b/src/queries/types/user.ts index 209d773ce9..9a393c2633 100644 --- a/src/queries/types/user.ts +++ b/src/queries/types/user.ts @@ -33,6 +33,38 @@ export type UnsubscribeChannelResponse = { message: string; }; +export type UserProfileInfoResponse = { + name: string | null; + desc: string | null; + picture: string | null; + profileVerificationProof?: string | null; +}; + +export type UpdateProfileInfoResponse = { + name: string | null; + desc: string | null; + picture: string | null; + profileVerificationProof?: string | null; + blockedUserList?: []; +}; + +export type SendHandlesVerificationResponse = { + email: string | null; + success: boolean; + VerificationCode?: string | null; +}; + +export type VerifyHandlesVerificationResponse = { + message: string; + success: boolean; +}; + +export type UserSocialStatusResponse = { + email: string | null; + telegram_username: string | null; + discord_username: string | null; + }; + export type UserProfileDetailsResponse = { blockedUsersList: Array; desc: string | null; @@ -40,3 +72,4 @@ export type UserProfileDetailsResponse = { picture: string; profileVerificationProof: string | null; }; +