diff --git a/frontend/packages/client/.env.example b/frontend/packages/client/.env.example index 3335d86a1..731ea41c9 100644 --- a/frontend/packages/client/.env.example +++ b/frontend/packages/client/.env.example @@ -5,4 +5,4 @@ REACT_APP_BACK_END_SERVER_API=http://localhost:5001 REACT_APP_IPFS_GATEWAY=https://dappercollectives.mypinata.cloud/ipfs REACT_APP_TX_OPTIONS_ADDRS="0xc590d541b72f0ac1,0x72d401812f579e3e" REACT_APP_HOTJAR_SITE_ID=0 -REACT_APP_SENTRY_URL=REPLACE_SENTRY_URL \ No newline at end of file +REACT_APP_SENTRY_URL=REPLACE_SENTRY_URL diff --git a/frontend/packages/client/src/App.js b/frontend/packages/client/src/App.js index 099e2b062..5a77f2a7d 100644 --- a/frontend/packages/client/src/App.js +++ b/frontend/packages/client/src/App.js @@ -35,13 +35,13 @@ function App() { {/* using resetCSS to avoid conficts */} - - - + + + - - - + + + diff --git a/frontend/packages/client/src/api/notificationService.js b/frontend/packages/client/src/api/notificationService.js index d0ecbabf4..186f07fa5 100644 --- a/frontend/packages/client/src/api/notificationService.js +++ b/frontend/packages/client/src/api/notificationService.js @@ -5,6 +5,7 @@ import { LEANPLUM_EXPORT_KEY, LEANPLUM_PROD_KEY, } from 'api/constants'; +import { subscribeNotificationIntentions } from 'const'; import Leanplum from 'leanplum-sdk'; const COMMUNITY_UPDATES_CATEGORY_ID = 1; @@ -12,8 +13,25 @@ const options = { method: 'GET', headers: { accept: 'application/json' }, }; - -export const startLeanplum = () => { +/* @param: communitySubIntentions : [{communityId:"1", subscribeIntention:"subscribe"},{communityId:"2",subscribeIntention:"unsubscribe"}] + * @return: {communityId1:'True', communityId2:'False'} + */ +const getDesiredAttributes = (communitySubIntentions) => { + return communitySubIntentions + .map(({ communityId, subscribeIntention }) => ({ + key: `community${communityId}`, + value: + subscribeIntention === subscribeNotificationIntentions.subscribe + ? 'True' + : 'False', + })) + .reduce((acc, curr) => { + const { key, value } = curr; + acc[key] = value; + return acc; + }, {}); +}; +export const startLeanplumForUser = (walletId) => { const IS_LOCAL_DEV = process.env.REACT_APP_APP_ENV === 'development'; if (IS_LOCAL_DEV) { @@ -21,16 +39,16 @@ export const startLeanplum = () => { } else { Leanplum.setAppIdForProductionMode(LEANPLUM_APP_ID, LEANPLUM_PROD_KEY); } - - Leanplum.start(); -}; - -export const setUserId = async (walletId) => { - try { - Leanplum.setUserId(walletId); - } catch (e) { - throw new Error(e); - } + return new Promise((resolve, reject) => { + Leanplum.start((success) => { + Leanplum.setUserId(walletId); + if (success) { + resolve('leanplum user set'); + } else { + reject('leanplum user not set'); + } + }); + }); }; export const getUserSettings = async (walletId) => { @@ -47,15 +65,16 @@ export const getUserSettings = async (walletId) => { if (!data.userAttributes) { throw new Error('User Not Found'); } - const communitySubscriptions = []; const res = { email: data.userAttributes.email, }; - let isSubscribedToCommunityUpdates = true; - + let isSubscribedFromCommunityUpdates = true; for (const property in data.userAttributes) { - if (property.includes('community')) { + if ( + property.includes('community') && + typeof data.userAttributes[property] == 'string' + ) { const communityId = property.split('community')[1]; communitySubscriptions.push({ communityId, @@ -63,45 +82,32 @@ export const getUserSettings = async (walletId) => { }); } } - res.communitySubscription = communitySubscriptions; - if (data.unsubscribeCategories) { data.unsubscribeCategories.forEach((category) => { if (parseInt(category.id) === COMMUNITY_UPDATES_CATEGORY_ID) { - isSubscribedToCommunityUpdates = false; + isSubscribedFromCommunityUpdates = false; } }); } - - res.isSubscribedToCommunityUpdates = isSubscribedToCommunityUpdates; + res.isSubscribedFromCommunityUpdates = isSubscribedFromCommunityUpdates; return res; } catch (e) { throw new Error(e); } + // setTimeout(() => { + // throw new Error('get user setting error'); + // }, 500); }; export const setUserEmail = async (email) => { - try { - await Leanplum.setUserAttributes({ email }); - return true; - } catch (e) { - throw new Error(e); - } -}; - -export const unsubscribeCommunity = async (communityId) => { - try { - Leanplum.setUserAttributes({ [`community${communityId}`]: 'False' }); - return true; - } catch (e) { - throw new Error(e); - } + Leanplum.setUserAttributes({ email }); }; -export const subscribeCommunity = async (communityId) => { +export const updateCommunitySubscription = async (communitySubIntentions) => { + const desiredAttributes = getDesiredAttributes(communitySubIntentions); try { - Leanplum.setUserAttributes({ [`community${communityId}`]: 'True' }); + Leanplum.setUserAttributes(desiredAttributes); return true; } catch (e) { throw new Error(e); diff --git a/frontend/packages/client/src/components/Community/SubscribeCommunityButton.js b/frontend/packages/client/src/components/Community/SubscribeCommunityButton.js index 74d2b00a6..326eddfba 100644 --- a/frontend/packages/client/src/components/Community/SubscribeCommunityButton.js +++ b/frontend/packages/client/src/components/Community/SubscribeCommunityButton.js @@ -17,50 +17,81 @@ export default function SubscribeCommunityButton({ const { openModal, closeModal } = useModalContext(); const { notificationSettings, updateCommunitySubscription } = useNotificationServiceContext(); - const subscribedToCommunity = - notificationSettings?.communitySubscription.some( - (c) => c.communityId === communityId && c.subscribed - ); - const subscribedToEmails = - notificationSettings?.isSubscribedFromCommunityUpdates; + const { communitySubscription, isSubscribedFromCommunityUpdates, email } = + notificationSettings; + const subscribedToCommunity = communitySubscription.some( + (c) => c.communityId === communityId?.toString() && c.subscribed + ); + + const subscribedToEmails = isSubscribedFromCommunityUpdates; const isSubscribed = subscribedToCommunity && subscribedToEmails; + const { popToast } = useToast(); const { user } = useWebContext(); const history = useHistory(); + const openWalletErrorModal = () => { + openModal( + + } + onClose={closeModal} + />, + { isErrorModal: true } + ); + }; + const showUpdateSuccessToast = (subscribeIntention) => { + const emailNotificationsState = + subscribeIntention === subscribeNotificationIntentions.subscribe + ? 'on' + : 'off'; - const onOpenModal = () => { - if (!user?.addr) { - openModal( - - } - onClose={closeModal} - />, - { isErrorModal: true } - ); - } else if (isSubscribed) { - updateCommunitySubscription( + popToast({ + message: `Email notifications are turned ${emailNotificationsState}`, + messageType: 'success', + actionFn: () => history.push('/settings'), + actionText: 'Manage Settings', + }); + }; + const handleUpdateSubscription = async () => { + const subscribeIntention = isSubscribed + ? subscribeNotificationIntentions.unsubscribe + : subscribeNotificationIntentions.subscribe; + await updateCommunitySubscription([ + { communityId, - subscribeNotificationIntentions.unsubscribe - ); - const emailNotificationsState = subscribedToEmails ? 'on' : 'off'; - popToast({ - message: `Email notifications are turned ${emailNotificationsState}`, - messageType: 'info', - actionFn: () => history.push('/settings'), - actionText: 'Manage Settings', - }); + subscribeIntention, + }, + ]); + showUpdateSuccessToast(subscribeIntention); + }; + const handleSignUp = () => { + openModal( + , + { + classNameModalContent: 'rounded modal-content-sm', + showCloseButton: false, + } + ); + }; + const handleBellButtonClick = () => { + //if user is not connect to wallet open error modal + if (!user?.addr) { + openWalletErrorModal(); + return; + } + //if leanplum has user email handle the subscribe/unsubscribe and show toast + //if leanplumn doesn't have user email, show subscribe modal + if (email?.length > 0) { + handleUpdateSubscription(); } else { - openModal( - , - { - classNameModalContent: 'rounded modal-content-sm', - showCloseButton: false, - } - ); + handleSignUp(); } }; @@ -88,7 +119,7 @@ export default function SubscribeCommunityButton({ className={`column p-0 is-narrow-tablet is-full-mobile ${className}`} style={containerStyles} > - diff --git a/frontend/packages/client/src/components/Settings/NotificationSettingsSection/CommunitiesList.js b/frontend/packages/client/src/components/Settings/NotificationSettingsSection/CommunitiesList.js index a4fb159ad..ae5ae1e24 100644 --- a/frontend/packages/client/src/components/Settings/NotificationSettingsSection/CommunitiesList.js +++ b/frontend/packages/client/src/components/Settings/NotificationSettingsSection/CommunitiesList.js @@ -10,7 +10,7 @@ export default function CommunitiesList({ const subscribeIntention = subscribed ? subscribeNotificationIntentions.unsubscribe : subscribeNotificationIntentions.subscribe; - updateCommunitySubscription(communityId, subscribeIntention); + updateCommunitySubscription([{ communityId, subscribeIntention }]); }; return (
@@ -38,9 +38,13 @@ function CommunityListItem({ subscribed, handleUpdateCommunitySubscription, }) { - const { data: community, isLoading } = useCommunityDetails(communityId); + const { + data: community, + isLoading, + error, + } = useCommunityDetails(communityId); const { name, logo, slug } = community ?? {}; - if (isLoading) return null; + if (isLoading || error) return null; return (
  • diff --git a/frontend/packages/client/src/components/Settings/NotificationSettingsSection/EmailAddressInput.js b/frontend/packages/client/src/components/Settings/NotificationSettingsSection/EmailAddressInput.js index ff3636f36..e0b42fae6 100644 --- a/frontend/packages/client/src/components/Settings/NotificationSettingsSection/EmailAddressInput.js +++ b/frontend/packages/client/src/components/Settings/NotificationSettingsSection/EmailAddressInput.js @@ -5,10 +5,10 @@ import { EMAIL_REGEX } from 'const'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; -export default function EmailAddressInput({ email, setUserEmail }) { - const { register, handleSubmit, formState } = useForm({ +export default function EmailAddressInput({ defaultEmail, setUserEmail }) { + const { register, handleSubmit, formState, setValue } = useForm({ defaultValues: { - email: email, + email: defaultEmail, }, resolver: yupResolver( yup.object().shape({ @@ -20,8 +20,12 @@ export default function EmailAddressInput({ email, setUserEmail }) { }) ), }); - const onSubmit = ({ email }) => { - setUserEmail(email); + const onSubmit = async ({ email }) => { + try { + await setUserEmail(email); + } catch (e) { + setValue('email', defaultEmail); + } }; const { isSubmitting, errors, isDirty } = formState; diff --git a/frontend/packages/client/src/components/Settings/NotificationSettingsSection/index.js b/frontend/packages/client/src/components/Settings/NotificationSettingsSection/index.js index dd205b7a0..6aab651ba 100644 --- a/frontend/packages/client/src/components/Settings/NotificationSettingsSection/index.js +++ b/frontend/packages/client/src/components/Settings/NotificationSettingsSection/index.js @@ -1,4 +1,3 @@ -import { Fragment } from 'react'; import { useNotificationServiceContext } from 'contexts/NotificationService'; import CommunitiesList from './CommunitiesList'; import EmailAddressInput from './EmailAddressInput'; @@ -27,7 +26,7 @@ export default function NotificationSettingsSection() { )} {communitySubscription.length > 0 && ( <> - +
    { const onSubmit = async (formData) => { try { - onSubscribe(signupAll); + onSubscribe(formData.email, signupAll); onClose(); } catch (e) { setErrorMessage(e.message); @@ -83,6 +83,7 @@ const SignUpForm = ({ setErrorMessage, onSubscribe, onClose }) => { Close + + + } + onClose={closeModal} + /> + ); +} diff --git a/frontend/packages/client/src/components/modals/index.js b/frontend/packages/client/src/components/modals/index.js index 1016351c0..e035af262 100644 --- a/frontend/packages/client/src/components/modals/index.js +++ b/frontend/packages/client/src/components/modals/index.js @@ -4,3 +4,4 @@ export { default as VoteConfirmation } from './VoteConfirmation'; export { default as CastingVote } from './CastingVote'; export { default as VoteConfirmed } from './VoteConfirmed'; export { default as CancelProposal } from './CancelProposal'; +export { default as Retry } from './Retry'; diff --git a/frontend/packages/client/src/contexts/NotificationService.js b/frontend/packages/client/src/contexts/NotificationService.js index 4a1a102ff..e283c9f43 100644 --- a/frontend/packages/client/src/contexts/NotificationService.js +++ b/frontend/packages/client/src/contexts/NotificationService.js @@ -1,31 +1,24 @@ import { createContext, useContext, useEffect, useState } from 'react'; +import { ErrorModal, RetryModal } from 'components'; import { subscribeNotificationIntentions } from 'const'; +import { + getUserSettings as getUser, + setUserEmail as setEmail, + startLeanplumForUser, + subscribeToEmailNotifications, + unsubscribeFromEmailNotifications, + updateCommunitySubscription as updateCommunity, +} from 'api/notificationService'; +import { debounce } from 'lodash'; +import { useModalContext } from './NotificationModal'; import { useWebContext } from './Web3'; const NotificationServiceContext = createContext({}); const INIT_NOTIFICATION_SETTINGS = { - walletId: '', email: '', - communitySubscription: [{ communityId: '1', subscribed: true }], - isSubscribedFromCommunityUpdates: true, -}; - -const updateCommunitySubscriptionState = ( - communitySubscription, - communityId, - subscribedValue -) => { - const newCommunitySubscription = [...communitySubscription]; - const updateIndex = newCommunitySubscription.findIndex( - (communitySub) => communitySub.communityId === communityId - ); - if (updateIndex === -1) { - newCommunitySubscription.push({ communityId, subscribed: subscribedValue }); - } else { - newCommunitySubscription[updateIndex].subscribed = subscribedValue; - } - return newCommunitySubscription; + communitySubscription: [], + isSubscribedFromCommunityUpdates: false, }; export const useNotificationServiceContext = () => { @@ -46,33 +39,85 @@ const NotificationServiceProvider = ({ children }) => { const { user: { addr }, } = useWebContext(); + const { openModal, closeModal } = useModalContext(); useEffect(() => { if (addr) { - (async () => { - await setUserID(addr); - getUserSettings(); - })(); + initUser(); } else { setNotificationSettings(INIT_NOTIFICATION_SETTINGS); } }, [addr]); - const setUserID = async (walletId) => { + const initUser = debounce( + async () => { + try { + console.log('init user called'); + await startLeanplumForUser(addr); + await getUserSettings(); + } catch (e) { + openRetryModal(); + } + }, + //Leanplum API rate limit is 1QPS + 2000, + { leading: true, trailing: false } + ); + + const openRetryModal = () => { + openModal( + , + { + isErrorModal: true, + } + ); + }; + const handleNotificationServiceError = (fn) => { + return async (...args) => { + try { + await fn(...args); + } catch { + openModal( + + Close + + } + onClose={closeModal} + />, + { isErrorModal: true } + ); + throw new Error(); + } + }; + }; + const getUserSettings = async () => { try { - //here we call api to init the leanplum sdk + const { communitySubscription, isSubscribedFromCommunityUpdates, email } = + await getUser(addr); setNotificationSettings((prevState) => ({ ...prevState, - walletId, + communitySubscription, + isSubscribedFromCommunityUpdates, + email, })); } catch { - throw new Error('cannot set user id for leanplum'); + throw new Error('cannot get user settings'); } }; - - const setUserEmail = async (email) => { + const setUserEmail = handleNotificationServiceError(async (email) => { try { - //here we call api + await setEmail(email); setNotificationSettings((prevState) => ({ ...prevState, email, @@ -80,71 +125,41 @@ const NotificationServiceProvider = ({ children }) => { } catch { throw new Error('cannot set user email'); } - }; + }); - const getUserSettings = async () => { - try { - //here we call api - const { communitySubscription, isSubscribedFromCommunityUpdates } = - INIT_NOTIFICATION_SETTINGS; - setNotificationSettings((prevState) => ({ - ...prevState, - communitySubscription, - isSubscribedFromCommunityUpdates, - })); - } catch { - throw new Error('cannot get user settings'); + const updateCommunitySubscription = handleNotificationServiceError( + async (communitySubIntentions) => { + try { + await updateCommunity(communitySubIntentions); + await new Promise((r) => setTimeout(r, 500)); + await getUserSettings(); + } catch { + throw new Error('cannot update community subscription'); + } + // throw Error(); } - }; + ); - const updateCommunitySubscription = async ( - communityId, - subscribeIntention - ) => { - try { - if (subscribeIntention === subscribeNotificationIntentions.subscribe) { - //call api to subscribe community + const updateAllEmailNotificationSubscription = handleNotificationServiceError( + async (subscribeIntention) => { + if (subscribeIntention === subscribeNotificationIntentions.resubscribe) { + subscribeToEmailNotifications(addr); } else if ( subscribeIntention === subscribeNotificationIntentions.unsubscribe ) { - //call api to unsubscribe community + unsubscribeFromEmailNotifications(addr); } - setNotificationSettings((prevState) => { - const newCommunitySubscription = updateCommunitySubscriptionState( - prevState.communitySubscription, - communityId, - subscribeIntention === subscribeNotificationIntentions.subscribe - ); - return { - ...prevState, - communitySubscription: newCommunitySubscription, - }; - }); - } catch { - throw new Error('cannot subscribe community'); - } - }; - - const updateAllEmailNotificationSubscription = async (subscribeIntention) => { - if (subscribeIntention === subscribeNotificationIntentions.resubscribe) { - //call api to resubscribe all email notifications - } else if ( - subscribeIntention === subscribeNotificationIntentions.unsubscribe - ) { - //call api to unsubscribe all email notifications + setNotificationSettings((prevState) => ({ + ...prevState, + isSubscribedFromCommunityUpdates: + subscribeIntention === subscribeNotificationIntentions.resubscribe, + })); } - setNotificationSettings((prevState) => ({ - ...prevState, - isSubscribedFromCommunityUpdates: - subscribeIntention === subscribeNotificationIntentions.resubscribe, - })); - }; + ); const providerProps = { notificationSettings, - setUserID, setUserEmail, - getUserSettings, updateCommunitySubscription, updateAllEmailNotificationSubscription, }; diff --git a/frontend/packages/client/src/pages/Home.js b/frontend/packages/client/src/pages/Home.js index 6ffcfe452..590facfbf 100644 --- a/frontend/packages/client/src/pages/Home.js +++ b/frontend/packages/client/src/pages/Home.js @@ -38,6 +38,7 @@ export default function HomePage() { // missing fields isComingSoon: datum.isComingSoon || false, })); + const browserName = useBrowserName(); const [showToolTip, setValue] = useLocalStorage('dw-safary-tooltip', null); diff --git a/frontend/packages/client/src/pages/Settings.js b/frontend/packages/client/src/pages/Settings.js index 64fb0c2e5..38354f182 100644 --- a/frontend/packages/client/src/pages/Settings.js +++ b/frontend/packages/client/src/pages/Settings.js @@ -1,4 +1,4 @@ -import { useNotificationServiceContext } from 'contexts/NotificationService'; +import { useWebContext } from 'contexts/Web3'; import { HomeFooter } from 'components'; import { ConnectWalletPrompt, @@ -8,19 +8,20 @@ import { import SectionContainer from 'layout/SectionContainer'; export default function Settings() { - const { notificationSettings } = useNotificationServiceContext(); - const { walletId } = notificationSettings; + const { + user: { addr }, + } = useWebContext(); return (
    - {walletId && ( + {addr && (
    - +
    )} - {!walletId && } + {!addr && }