From ea17eeccd2e9948bafc83106168bcec2cc439784 Mon Sep 17 00:00:00 2001 From: Subhranshu Das Date: Wed, 11 May 2022 10:05:38 +0530 Subject: [PATCH 1/6] feat: EPNS optin integration --- public/locales/en.json | 13 + src/components/Calendar/Dropdown.js | 2 +- src/components/EPNS/EPNSLink.js | 21 ++ src/components/EPNS/EPNSNotificationModal.js | 227 ++++++++++++++++ src/components/EPNS/EPNSOnSubscribeModal.js | 176 +++++++++++++ src/components/EPNS/EPNSUtil.js | 242 ++++++++++++++++++ src/components/EPNS/SwitchNetwork.js | 39 +++ src/components/EPNS/index.js | 4 + src/components/EPNS/useWeb3.js | 24 ++ .../ExpiryNotifyDropdown.js | 25 +- 10 files changed, 770 insertions(+), 3 deletions(-) create mode 100644 src/components/EPNS/EPNSLink.js create mode 100644 src/components/EPNS/EPNSNotificationModal.js create mode 100644 src/components/EPNS/EPNSOnSubscribeModal.js create mode 100644 src/components/EPNS/EPNSUtil.js create mode 100644 src/components/EPNS/SwitchNetwork.js create mode 100644 src/components/EPNS/index.js create mode 100644 src/components/EPNS/useWeb3.js diff --git a/public/locales/en.json b/public/locales/en.json index c0e4cd7f7..d180798df 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -382,5 +382,18 @@ "title": "ENS constitution book now available", "description": "A printed copy of the ENS constitution and its signers is now available in hardcover and ultra-limited edition of 50" } + }, + "epns": { + "link": "Wallet Notif (EPNS)", + "modal": { + "header": "You can choose to Opt-In/Opt-Out EPNS Notifications from ENS!", + "optInButton": "OPT-IN", + "optOutButton": "OPT-OUT", + "platforms": "View Supported Platforms", + "alreadyOpted": "You are Opted-In!", + "apiError": "Mayday! Mayday! Can the devs do something? They have been notified, please try again a little later!", + "unsupportedNetwork": "Wallet notifications are only supported on the Main net!", + "switchNetwork": "Switch Network" + } } } \ No newline at end of file diff --git a/src/components/Calendar/Dropdown.js b/src/components/Calendar/Dropdown.js index ce6f3f689..00a64db2c 100644 --- a/src/components/Calendar/Dropdown.js +++ b/src/components/Calendar/Dropdown.js @@ -24,7 +24,7 @@ function Dropdown(props, ref) { const allChildren = prependChildren.concat(children).concat(appendChildren) return ( -
+
{allChildren}
) diff --git a/src/components/EPNS/EPNSLink.js b/src/components/EPNS/EPNSLink.js new file mode 100644 index 000000000..1f3cb42b4 --- /dev/null +++ b/src/components/EPNS/EPNSLink.js @@ -0,0 +1,21 @@ +import React from 'react' + +const EPNSLink = ({ children, onClick = null }) => { + async function handleClick(e) { + e.preventDefault() + + if (onClick) { + onClick(e) + } + } + + return ( + <> + + {children} + + + ) +} + +export default EPNSLink diff --git a/src/components/EPNS/EPNSNotificationModal.js b/src/components/EPNS/EPNSNotificationModal.js new file mode 100644 index 000000000..2d4f9ef01 --- /dev/null +++ b/src/components/EPNS/EPNSNotificationModal.js @@ -0,0 +1,227 @@ +import styled from '@emotion/styled' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { + default as Button, + getButtonDefaultStyles, + getButtonStyles +} from '../Forms/Button' +import Loader from '../Loader' +import { useWeb3Data } from './useWeb3' +import { selectConfig, isUserSubscribed, optIn, optOut } from './EPNSUtil' +import OnSubscribedModal from './EPNSOnSubscribeModal' +import NetworkSwitch from './SwitchNetwork' + +const LoadingComponent = styled(Loader)` + display: inline-block; + margin: 0 10px; + vertical-align: middle; +` + +const FormComponent = styled('form')` + display: grid; + grid-gap: 10px; + font-weight: 200; +` + +const FormLabel = styled('label')` + display: block; + margin-top: 20px; +` + +const FormWarning = styled('div')` + color: #f5a623; + margin-bottom: 10px; +` + +const FormError = styled(FormLabel)` + color: red; + font-size: 0.8rem; +` + +const Header = styled('h3')` + font-size: 1rem; + font-weight: 400; + margin-top: 0; +` + +const Row = styled('div')` + padding: 10px 0 30px; +` + +const MessageBlock = styled('div')` + display: flex; + flex-direction: column; + margin-top: 30px; +` +const Actions = styled('div')` + display: flex; + justify-content: right; +` + +const buttonStyles = ` + margin: 5px; + position: relative; +` + +const CancelComponent = styled(Button)` + ${getButtonDefaultStyles()} + ${getButtonStyles({ type: 'hollow' })} + ${buttonStyles} + ` + +const EPNSNotificationModal = ({ address, onCancel }) => { + const { t } = useTranslation() + + const { signer, networkId } = useWeb3Data() + const [epnsConfig, setEpnsConfig] = useState({}) + const [isChannelSubscribed, setIsChannelSubscribed] = useState() + const [isLoading, setIsLoading] = useState() + const [isError, setError] = useState() + const [networkSupported, setNetworkSupported] = useState() // epns api don't support ropsten + const [isModalOpen, setIsModalOpen] = useState() + + const isWeb3Ready = signer && address && networkId + const disableButton = !networkSupported || isError + const buttonType = disableButton ? 'disabled' : 'primary' + + const handleSubmit = e => { + e.preventDefault() + } + + async function callOptIn() { + setIsLoading(true) + + await optIn(signer, epnsConfig.CHANNEL_ADDRESS, networkId, address, { + baseApiUrl: epnsConfig.API_BASE_URL, + onSuccess: () => { + setIsChannelSubscribed(true) + setIsLoading(false) + setIsModalOpen(true) + }, + onError: err => { + setError(true) + setIsLoading(false) + } + }) + } + + async function callOptOut() { + setIsLoading(true) + + await optOut(signer, epnsConfig.CHANNEL_ADDRESS, networkId, address, { + baseApiUrl: epnsConfig.API_BASE_URL, + onSuccess: () => { + setIsChannelSubscribed(false) + setIsLoading(false) + }, + onError: err => { + setError(true) + setIsLoading(false) + } + }) + } + + const toggleSubscription = () => { + if (!isChannelSubscribed) { + callOptIn() + } else { + callOptOut() + } + } + + const toggleModal = () => { + setIsModalOpen(value => !value) + } + + async function fetchAndUpdateSubscription() { + try { + setIsLoading(true) + const status = await isUserSubscribed( + address, + epnsConfig.CHANNEL_ADDRESS, + epnsConfig.API_BASE_URL + ) + setIsChannelSubscribed(status) + } catch (err) { + setError(true) + } finally { + setIsLoading(false) + } + } + + useEffect(() => { + // determine the config + if (networkId) { + setEpnsConfig(selectConfig(networkId)) + } + }, [networkId]) + + useEffect(() => { + // fetch the channel details if we have config + const isSupprtedNW = Object.keys(epnsConfig).length > 0 + if (isSupprtedNW) { + fetchAndUpdateSubscription() + } + + setNetworkSupported(isSupprtedNW) + }, [epnsConfig]) + + return ( + + +
{t('epns.modal.header')}
+
+ + + + + + {isWeb3Ready ? ( + + {isLoading ? ( + + ) : isChannelSubscribed ? ( + + ) : ( + + )} + + {!networkSupported ? ( + + {t('epns.modal.unsupportedNetwork')} + {t('epns.modal.switchNetwork')} + + ) : null} + {isError ? {t('epns.modal.apiError')} : null} + + ) : ( + + )} + + + {t('c.cancel')} + + + {/* OnSubscribeModal */} + {isModalOpen ? : null} +
+ ) +} + +export default EPNSNotificationModal diff --git a/src/components/EPNS/EPNSOnSubscribeModal.js b/src/components/EPNS/EPNSOnSubscribeModal.js new file mode 100644 index 000000000..b804fdab6 --- /dev/null +++ b/src/components/EPNS/EPNSOnSubscribeModal.js @@ -0,0 +1,176 @@ +import React from 'react' +// import * as ReactUse from "react-use"; +import styled from '@emotion/styled' +import { LINKS, CLOSE_ICON } from './EPNSUtil' +import { useOnClickOutside } from 'components/hooks' + +const OnSubscribedModal = ({ onClose }) => { + const modalRef = React.useRef(null) + // dummy function to help navigate to another page + const goto = url => { + window.open(url, '_blank') + } + + // ReactUse.useClickAway(modalRef, onClose); + useOnClickOutside([modalRef], () => onClose()) + + return ( + + + + + + Recieve + Notifications + +

+ Recieve notifications from EPNS via the following platforms. +

+
+ + + {LINKS.map((oneLink, idx) => ( + goto(oneLink.link)} key={idx}> + + {oneLink.text} + + ))} + +
+
+ ) +} + +const ItemLink = styled.div` + width: 260px; + height: 62px; + padding-left: 22px; + + background: #fafafa; + border: 0.2px solid rgba(0, 0, 0, 0.16); + box-sizing: border-box; + border-radius: 5px; + font-size: 0.75em; + text-transform: uppercase; + display: flex; + align-items: center; + gap: 1.3125em; + + cursor: pointer; + transition: 300ms; + + &:hover { + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + } +` +const CustomHeaderTwo = styled.h2` + margin-top: 0; + margin-bottom: 1em; + color: rgb(0, 0, 0); + font-weight: 600; + font-size: 1.5625em; + letter-spacing: 0.1em; + text-transform: uppercase; + padding: 0px; + font-family: inherit; + text-align: inherit; +` + +const Item = styled.div` + display: flex; + flex-direction: column; + text-transform: capitalise; + + &.modal__heading { + margin-bottom: 3.3125rem; + } + + &.modal__content { + display: grid; + grid-template-columns: 50% 50%; + grid-row-gap: 3.3125em; + } +` + +const CustomSpan = styled.span` + flex: initial; + align-self: auto; + color: rgb(0, 0, 0); + background: transparent; + font-weight: 400; + font-size: inherit; + text-transform: inherit; + margin: 0px; + padding: 0px; + letter-spacing: inherit; + text-align: initial; + position: initial; + inset: auto; + z-index: auto; +` + +const StyledSpan = styled(CustomSpan)` + background: rgb(226, 8, 128); + color: #fff; + font-weight: 600; + padding: 3px 8px; +` + +const H3 = styled.h3` + color: rgb(0 0 0 / 0.5); + font-weight: 300; + font-size: 1em; + text-transform: uppercase; + margin: -15px 0px 20px 0px; + padding: 0px; + letter-spacing: 0.1em; + font-family: 'Source Sans Pro', Helvetica, sans-serif; + text-align: inherit; + max-width: initial; +` + +const Overlay = styled.div` + top: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.85); + height: 100%; + width: 100%; + z-index: 1000; + position: fixed; + display: flex; + justify-content: center; + align-items: center; + overflow-y: scroll; +` + +const Modal = styled.div` + padding: 3.875em; + background: white; + text-align: left; + border: 1px solid rgba(0, 0, 0, 0.16); + box-sizing: border-box; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + border-radius: 15px; + position: relative; + + & > img { + position: absolute; + right: 40px; + top: 40px; + cursor: pointer; + } + + @media (max-width: 1000px) { + width: max(70vw, 350px); + padding: 2em; + .modal__content { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + } + } +` + +export default OnSubscribedModal diff --git a/src/components/EPNS/EPNSUtil.js b/src/components/EPNS/EPNSUtil.js new file mode 100644 index 000000000..c8224cdb4 --- /dev/null +++ b/src/components/EPNS/EPNSUtil.js @@ -0,0 +1,242 @@ +const EPNS_CONFIG = { + // MAIN_NET - prod + 1: { + CHANNEL_ADDRESS: '0x983110309620D911731Ac0932219af06091b6744', // ENS address + API_BASE_URL: 'https://backend-prod.epns.io/apis', + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa' + } +} + +const signingConstants = { + // The several types of actions and their corresponding types + // which we can take, when it comes to signing messages + ACTION_TYPES: { + // the type to be used for the subscribe action to a channel + subscribe: { + Subscribe: [ + { name: 'channel', type: 'address' }, + { name: 'subscriber', type: 'address' }, + { name: 'action', type: 'string' } + ] + }, + // the type to be used for the unsubscribe action to a channel + unsubscribe: { + Unsubscribe: [ + { name: 'channel', type: 'address' }, + { name: 'unsubscriber', type: 'address' }, + { name: 'action', type: 'string' } + ] + } + } +} + +function getDomainInformation(chainId, verifyingContractAddress) { + return { + name: 'EPNS COMM V1', + chainId: chainId, + verifyingContract: + verifyingContractAddress || + EPNS_CONFIG[chainId].EPNS_COMMUNICATOR_CONTRACT + } +} + +function getSubscriptionMessage(channelAddress, userAddress, action) { + return { + channel: channelAddress, + [action === 'Unsubscribe' ? 'unsubscriber' : 'subscriber']: userAddress, + action: action + } +} + +async function fetchPOST(url, body = {}, options = {}) { + return fetch(url, { + method: 'POST', + headers: { + 'Content-type': 'application/json; charset=UTF-8' + }, + ...options, + body: body + }) + .then(async response => { + const isJson = response.headers + .get('content-type') + ?.includes('application/json') + const data = isJson ? await response.json() : null + + // check for error response + if (!response.ok) { + // get error message from body or default to response status + const error = (data && data.message) || response.status + return Promise.reject(error) + } + + return Promise.resolve(data) + }) + .catch(error => { + console.error('EPNS API Fetch error: ', error) + throw error + }) +} + +async function optIn( + signer, + channelAddress, + chainId, + userAddress, + { + baseApiUrl = EPNS_CONFIG[chainId].API_BASE_URL, + verifyingContractAddress = EPNS_CONFIG[chainId].EPNS_COMMUNICATOR_CONTRACT, + onSuccess = () => 'success', + onError = () => 'error' + } = {} +) { + try { + // get domain information + const domainInformation = getDomainInformation( + chainId, + verifyingContractAddress + ) + // get type information + const typeInformation = signingConstants.ACTION_TYPES['subscribe'] + // get message + const messageInformation = getSubscriptionMessage( + channelAddress, + userAddress, + 'Subscribe' + ) + // sign message + const signature = await signer._signTypedData( + domainInformation, + typeInformation, + messageInformation + ) + + const postBody = JSON.stringify({ + signature, + message: messageInformation, + op: 'write', + chainId, + contractAddress: verifyingContractAddress + }) + + await fetchPOST(`${baseApiUrl}/channels/subscribe_offchain`, postBody) + + onSuccess() + return { status: 'success', message: 'succesfully opted into channel' } + } catch (err) { + onError(err) + return { status: 'error', message: err.message } + } +} + +async function optOut( + signer, + channelAddress, + chainId, + userAddress, + { + baseApiUrl = EPNS_CONFIG[chainId].API_BASE_URL, + verifyingContractAddress = EPNS_CONFIG[chainId].EPNS_COMMUNICATOR_CONTRACT, + onSuccess = () => 'success', + onError = () => 'error' + } = {} +) { + try { + // get domain information + const domainInformation = getDomainInformation( + chainId, + verifyingContractAddress + ) + // get type information + const typeInformation = signingConstants.ACTION_TYPES['unsubscribe'] + + // get message + const messageInformation = getSubscriptionMessage( + channelAddress, + userAddress, + 'Unsubscribe' + ) + + // sign message + const signature = await signer._signTypedData( + domainInformation, + typeInformation, + messageInformation + ) + + const postBody = JSON.stringify({ + signature, + message: messageInformation, + op: 'write', + chainId, + contractAddress: verifyingContractAddress + }) + + await fetchPOST(`${baseApiUrl}/channels/unsubscribe_offchain`, postBody) + + onSuccess() + return { status: 'success', message: 'succesfully opted out of channel' } + } catch (err) { + onError(err) + return { status: 'error', message: err.message } + } +} + +function selectConfig(networkId) { + return EPNS_CONFIG[networkId] || {} +} + +/** + * Function to obtain all the addresses subscribed to a channel + * @param channelAddress the address of the channel + * @param userAddress + */ +async function getSubscribers(channelAddress, baseApiUrl) { + try { + const postBody = JSON.stringify({ channel: channelAddress, op: 'read' }) + const response = await fetchPOST( + `${baseApiUrl}/channels/get_subscribers`, + postBody + ) + + return response.subscribers || [] + } catch (err) { + throw err + } +} + +async function isUserSubscribed(userAddress, channelAddress, baseApiUrl) { + const channelSubscribers = await getSubscribers(channelAddress, baseApiUrl) + + return channelSubscribers + .map(a => a.toLowerCase()) + .includes(userAddress.toLowerCase()) +} + +const LINKS = [ + { + text: 'EPNS Browser Extension', + link: + 'https://chrome.google.com/webstore/detail/epns-protocol-alpha/lbdcbpaldalgiieffakjhiccoeebchmg', + img: 'https://backend-kovan.epns.io/assets/googlechromeicon.png' + }, + { + text: 'EPNS App (iOS)', + link: 'https://apps.apple.com/app/ethereum-push-service-epns/id1528614910', + img: 'https://backend-kovan.epns.io/assets/apple.png' + }, + { + text: 'EPNS App (Android)', + link: 'https://play.google.com/store/apps/details?id=io.epns.epns', + img: 'https://backend-kovan.epns.io/assets/playstorecolor@3x.png' + }, + { + text: 'Visit our dApp', + link: 'https://app.epns.io/', + img: 'https://backend-kovan.epns.io/assets/dappcolor@3x.png' + } +] + +const CLOSE_ICON = 'https://backend-kovan.epns.io/assets/cross.png' + +export { optIn, optOut, selectConfig, isUserSubscribed, LINKS, CLOSE_ICON } diff --git a/src/components/EPNS/SwitchNetwork.js b/src/components/EPNS/SwitchNetwork.js new file mode 100644 index 000000000..1f4d435bc --- /dev/null +++ b/src/components/EPNS/SwitchNetwork.js @@ -0,0 +1,39 @@ +import React from 'react' +import styled from '@emotion/styled' +import { default as Button } from '../Forms/Button' + +const buttonStyles = ` + padding: 7px 20px; + width: 200px; +` + +const SwitchButton = styled(Button)` + ${buttonStyles} +` + +const NetworkSwitch = ({ children }) => { + async function handleClick(e) { + e.preventDefault() + + try { + debugger + await window.ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x1' }] + }) + console.log('You have switched to the Ethereum Main network') + } catch (switchError) { + console.error('Cannot switch to the network', switchError) + } + } + + return ( + <> + + {children} + + + ) +} + +export default NetworkSwitch diff --git a/src/components/EPNS/index.js b/src/components/EPNS/index.js new file mode 100644 index 000000000..a77b683fe --- /dev/null +++ b/src/components/EPNS/index.js @@ -0,0 +1,4 @@ +import EPNSLink from './EPNSLink' +import EPNSNotificationModal from './EPNSNotificationModal' + +export { EPNSLink, EPNSNotificationModal } diff --git a/src/components/EPNS/useWeb3.js b/src/components/EPNS/useWeb3.js new file mode 100644 index 000000000..3001c3028 --- /dev/null +++ b/src/components/EPNS/useWeb3.js @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react' +import { getSigner, getNetworkId } from '@ensdomains/ui' + +export function useWeb3Data() { + const [signer, setSigner] = useState(null) + const [networkId, setNetworkId] = useState(null) + + useEffect(() => { + async function init() { + const _signer = await getSigner() + const _networkId = await getNetworkId() + + setSigner(_signer) + setNetworkId(_networkId) + } + + init() + }, []) + + return { + signer, + networkId + } +} diff --git a/src/components/ExpiryNotification/ExpiryNotifyDropdown.js b/src/components/ExpiryNotification/ExpiryNotifyDropdown.js index 710bd0a05..ae98b4e2b 100644 --- a/src/components/ExpiryNotification/ExpiryNotifyDropdown.js +++ b/src/components/ExpiryNotification/ExpiryNotifyDropdown.js @@ -7,16 +7,19 @@ import EmailNotifyLink from './EmailNotifyLink' import Modal from '../Modal/Modal' import ExpiryNotificationModal from './ExpiryNotificationModal' import { useOnClickOutside } from 'components/hooks' +import { EPNSLink, EPNSNotificationModal } from '../EPNS' const ExpiryNotifyDropdownContainer = styled('div')` position: relative; ` +const customDropdownStyles = { minWidth: 162 } export default function ExpiryNotifyDropdown({ address }) { const dropdownRef = createRef() const togglerRef = createRef() const [showDropdown, setShowDropdown] = useState(false) const [showModal, setShowModal] = useState(false) + const [optionSelected, setOptionSelected] = useState(false) const { t } = useTranslation() useOnClickOutside([dropdownRef, togglerRef], () => setShowDropdown(false)) @@ -26,12 +29,20 @@ export default function ExpiryNotifyDropdown({ address }) { } const handleEmailNotifyClick = () => { + setOptionSelected('email') setShowModal(true) setShowDropdown(false) } const handleCloseModal = () => { setShowModal(false) + setOptionSelected('') + } + + const handleEPNSNotifyClick = () => { + setOptionSelected('epns') + setShowModal(true) + setShowDropdown(false) } return ( @@ -40,7 +51,7 @@ export default function ExpiryNotifyDropdown({ address }) { {t('expiryNotification.reminder')} {showDropdown && ( - + {t('c.email')} + + + {t('epns.link')} + )} - {showModal && ( + {showModal && optionSelected === 'email' && ( )} + + {showModal && optionSelected === 'epns' && ( + + + + )} ) } From ab3084184ace81b73a75aa48dd58291f8d5de620 Mon Sep 17 00:00:00 2001 From: xander Date: Fri, 13 May 2022 20:23:15 +0100 Subject: [PATCH 2/6] - Add mobile responsiveness - Improve overall UI --- public/locales/en.json | 5 +- src/components/EPNS/EPNSNotificationModal.js | 90 ++++++++++--------- .../ExpiryNotification/EmailNotifyLink.js | 6 +- .../ExpiryNotifyDropdown.js | 3 +- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index d180798df..cc18299f9 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -386,10 +386,11 @@ "epns": { "link": "Wallet Notif (EPNS)", "modal": { - "header": "You can choose to Opt-In/Opt-Out EPNS Notifications from ENS!", + "header": "Subscribe to Notifications from ENS!", + "subheader": "Opt-in to the ENS channel on EPNS to get notified when your domain is near expiry.", "optInButton": "OPT-IN", "optOutButton": "OPT-OUT", - "platforms": "View Supported Platforms", + "platforms": "Notification Platforms", "alreadyOpted": "You are Opted-In!", "apiError": "Mayday! Mayday! Can the devs do something? They have been notified, please try again a little later!", "unsupportedNetwork": "Wallet notifications are only supported on the Main net!", diff --git a/src/components/EPNS/EPNSNotificationModal.js b/src/components/EPNS/EPNSNotificationModal.js index 2d4f9ef01..0c7032f42 100644 --- a/src/components/EPNS/EPNSNotificationModal.js +++ b/src/components/EPNS/EPNSNotificationModal.js @@ -1,4 +1,5 @@ import styled from '@emotion/styled' +import mq from 'mediaQuery' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -19,6 +20,13 @@ const LoadingComponent = styled(Loader)` vertical-align: middle; ` +const Header = styled('h3')` + font-size: 2rem; + font-weight: 700; + margin-top: 0; + margin-bottom: 0; +` + const FormComponent = styled('form')` display: grid; grid-gap: 10px; @@ -40,31 +48,32 @@ const FormError = styled(FormLabel)` font-size: 0.8rem; ` -const Header = styled('h3')` - font-size: 1rem; - font-weight: 400; - margin-top: 0; +const FormContent = styled('div')` + margin-top: 15px; + margin-bottom: 0; ` -const Row = styled('div')` - padding: 10px 0 30px; +const FormText = styled('div')` + font-size: 1rem; + margin-bottom: 45px; ` -const MessageBlock = styled('div')` +const FormActions = styled('div')` display: flex; - flex-direction: column; - margin-top: 30px; -` -const Actions = styled('div')` - display: flex; - justify-content: right; + flex-wrap: wrap; + justify-content: space-between; ` const buttonStyles = ` - margin: 5px; position: relative; ` +const SubActionWrapper = styled('div')` + display: flex; + gap: 15px; + flex-wrap: wrap; +` + const CancelComponent = styled(Button)` ${getButtonDefaultStyles()} ${getButtonStyles({ type: 'hollow' })} @@ -82,7 +91,6 @@ const EPNSNotificationModal = ({ address, onCancel }) => { const [networkSupported, setNetworkSupported] = useState() // epns api don't support ropsten const [isModalOpen, setIsModalOpen] = useState() - const isWeb3Ready = signer && address && networkId const disableButton = !networkSupported || isError const buttonType = disableButton ? 'disabled' : 'primary' @@ -90,9 +98,9 @@ const EPNSNotificationModal = ({ address, onCancel }) => { e.preventDefault() } + // opt into the ENS channel async function callOptIn() { setIsLoading(true) - await optIn(signer, epnsConfig.CHANNEL_ADDRESS, networkId, address, { baseApiUrl: epnsConfig.API_BASE_URL, onSuccess: () => { @@ -100,23 +108,23 @@ const EPNSNotificationModal = ({ address, onCancel }) => { setIsLoading(false) setIsModalOpen(true) }, - onError: err => { + onError: _ => { setError(true) setIsLoading(false) } }) } + // opt out of the ENS channel async function callOptOut() { setIsLoading(true) - await optOut(signer, epnsConfig.CHANNEL_ADDRESS, networkId, address, { baseApiUrl: epnsConfig.API_BASE_URL, onSuccess: () => { setIsChannelSubscribed(false) setIsLoading(false) }, - onError: err => { + onError: _ => { setError(true) setIsLoading(false) } @@ -135,6 +143,7 @@ const EPNSNotificationModal = ({ address, onCancel }) => { setIsModalOpen(value => !value) } + // fetch and update of a user is already subscribed to the ens channel async function fetchAndUpdateSubscription() { try { setIsLoading(true) @@ -164,7 +173,6 @@ const EPNSNotificationModal = ({ address, onCancel }) => { if (isSupprtedNW) { fetchAndUpdateSubscription() } - setNetworkSupported(isSupprtedNW) }, [epnsConfig]) @@ -174,15 +182,23 @@ const EPNSNotificationModal = ({ address, onCancel }) => {
{t('epns.modal.header')}
- - - + + {t('epns.modal.subheader')} + {!networkSupported ? ( + {t('epns.modal.unsupportedNetwork')} + ) : null} + {isError ? {t('epns.modal.apiError')} : null} + - {isWeb3Ready ? ( - - {isLoading ? ( + + {t('c.cancel')} + + + {!networkSupported ? ( + {t('epns.modal.switchNetwork')} + ) : isLoading ? ( ) : isChannelSubscribed ? ( )} - - {!networkSupported ? ( - - {t('epns.modal.unsupportedNetwork')} - {t('epns.modal.switchNetwork')} - - ) : null} - {isError ? {t('epns.modal.apiError')} : null} - - ) : ( - - )} - - - {t('c.cancel')} - + + {/* OnSubscribeModal */} {isModalOpen ? : null} diff --git a/src/components/ExpiryNotification/EmailNotifyLink.js b/src/components/ExpiryNotification/EmailNotifyLink.js index 8c32a65df..60f97a27c 100644 --- a/src/components/ExpiryNotification/EmailNotifyLink.js +++ b/src/components/ExpiryNotification/EmailNotifyLink.js @@ -1,9 +1,5 @@ import React, { useContext, useState } from 'react' -import GlobalState from '../../globalState' - -import Modal from '../Modal/Modal' - -import ExpiryNotificationModal from './ExpiryNotificationModal' +import styled from '@emotion/styled/macro' // If react-add-to-calendar-hoc is replaced, it may be useful // to switch to a button element for a11y purposes. diff --git a/src/components/ExpiryNotification/ExpiryNotifyDropdown.js b/src/components/ExpiryNotification/ExpiryNotifyDropdown.js index ae98b4e2b..3f0842477 100644 --- a/src/components/ExpiryNotification/ExpiryNotifyDropdown.js +++ b/src/components/ExpiryNotification/ExpiryNotifyDropdown.js @@ -12,7 +12,8 @@ import { EPNSLink, EPNSNotificationModal } from '../EPNS' const ExpiryNotifyDropdownContainer = styled('div')` position: relative; ` -const customDropdownStyles = { minWidth: 162 } + +const customDropdownStyles = { minWidth: 162, gap: '10px' } export default function ExpiryNotifyDropdown({ address }) { const dropdownRef = createRef() From 5977a4219285710e72e7f16bff1481a6e65fbdfe Mon Sep 17 00:00:00 2001 From: xander Date: Wed, 18 May 2022 09:37:38 +0100 Subject: [PATCH 3/6] fix ux bug with disabling button when there's an error --- src/components/EPNS/EPNSNotificationModal.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/EPNS/EPNSNotificationModal.js b/src/components/EPNS/EPNSNotificationModal.js index 0c7032f42..7084134f8 100644 --- a/src/components/EPNS/EPNSNotificationModal.js +++ b/src/components/EPNS/EPNSNotificationModal.js @@ -15,7 +15,8 @@ import OnSubscribedModal from './EPNSOnSubscribeModal' import NetworkSwitch from './SwitchNetwork' const LoadingComponent = styled(Loader)` - display: inline-block; + display: inline-flex; + align-items: center; margin: 0 10px; vertical-align: middle; ` @@ -91,7 +92,7 @@ const EPNSNotificationModal = ({ address, onCancel }) => { const [networkSupported, setNetworkSupported] = useState() // epns api don't support ropsten const [isModalOpen, setIsModalOpen] = useState() - const disableButton = !networkSupported || isError + const disableButton = !networkSupported const buttonType = disableButton ? 'disabled' : 'primary' const handleSubmit = e => { From f87c985aa81aef463cd44bbbd6d608dce74eb473 Mon Sep 17 00:00:00 2001 From: xander Date: Wed, 18 May 2022 09:55:38 +0100 Subject: [PATCH 4/6] add more mobile responsiveness to buttons --- src/components/EPNS/EPNSNotificationModal.js | 35 +++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/components/EPNS/EPNSNotificationModal.js b/src/components/EPNS/EPNSNotificationModal.js index 7084134f8..e5acdda3d 100644 --- a/src/components/EPNS/EPNSNotificationModal.js +++ b/src/components/EPNS/EPNSNotificationModal.js @@ -63,6 +63,12 @@ const FormActions = styled('div')` display: flex; flex-wrap: wrap; justify-content: space-between; + gap: 15px; + width: 100%; + + ${mq.small` + gap: 0px; + `} ` const buttonStyles = ` @@ -73,9 +79,21 @@ const SubActionWrapper = styled('div')` display: flex; gap: 15px; flex-wrap: wrap; + width: 100%; + + ${mq.small` + width: fit-content; + `} ` -const CancelComponent = styled(Button)` +const ResponsiveButton = styled(Button)` + width: 100%; + ${mq.small` + width: auto; + `} +` + +const CancelComponent = styled(ResponsiveButton)` ${getButtonDefaultStyles()} ${getButtonStyles({ type: 'hollow' })} ${buttonStyles} @@ -89,7 +107,7 @@ const EPNSNotificationModal = ({ address, onCancel }) => { const [isChannelSubscribed, setIsChannelSubscribed] = useState() const [isLoading, setIsLoading] = useState() const [isError, setError] = useState() - const [networkSupported, setNetworkSupported] = useState() // epns api don't support ropsten + const [networkSupported, setNetworkSupported] = useState() // epns api only support kovan const [isModalOpen, setIsModalOpen] = useState() const disableButton = !networkSupported @@ -143,7 +161,6 @@ const EPNSNotificationModal = ({ address, onCancel }) => { const toggleModal = () => { setIsModalOpen(value => !value) } - // fetch and update of a user is already subscribed to the ens channel async function fetchAndUpdateSubscription() { try { @@ -194,29 +211,29 @@ const EPNSNotificationModal = ({ address, onCancel }) => { {t('c.cancel')} - + {!networkSupported ? ( {t('epns.modal.switchNetwork')} ) : isLoading ? ( ) : isChannelSubscribed ? ( - + ) : ( - + )} From ba160833596caf62229992829c5a7ba18fcc031b Mon Sep 17 00:00:00 2001 From: xander Date: Thu, 19 May 2022 16:11:49 +0100 Subject: [PATCH 5/6] Change text to provide more context --- public/locales/en.json | 5 ++--- src/components/EPNS/EPNSNotificationModal.js | 4 ---- src/components/EPNS/EPNSOnSubscribeModal.js | 8 ++++---- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index cc18299f9..0ebd1add0 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -386,11 +386,10 @@ "epns": { "link": "Wallet Notif (EPNS)", "modal": { - "header": "Subscribe to Notifications from ENS!", - "subheader": "Opt-in to the ENS channel on EPNS to get notified when your domain is near expiry.", + "subheader": "You can choose to opt-in or opt-out of ENS channel on EPNS to get notified when your domain is near expiry.", "optInButton": "OPT-IN", "optOutButton": "OPT-OUT", - "platforms": "Notification Platforms", + "platforms": "View Supported Platforms", "alreadyOpted": "You are Opted-In!", "apiError": "Mayday! Mayday! Can the devs do something? They have been notified, please try again a little later!", "unsupportedNetwork": "Wallet notifications are only supported on the Main net!", diff --git a/src/components/EPNS/EPNSNotificationModal.js b/src/components/EPNS/EPNSNotificationModal.js index e5acdda3d..519166b07 100644 --- a/src/components/EPNS/EPNSNotificationModal.js +++ b/src/components/EPNS/EPNSNotificationModal.js @@ -196,10 +196,6 @@ const EPNSNotificationModal = ({ address, onCancel }) => { return ( - -
{t('epns.modal.header')}
-
- {t('epns.modal.subheader')} {!networkSupported ? ( diff --git a/src/components/EPNS/EPNSOnSubscribeModal.js b/src/components/EPNS/EPNSOnSubscribeModal.js index b804fdab6..d515ec8fb 100644 --- a/src/components/EPNS/EPNSOnSubscribeModal.js +++ b/src/components/EPNS/EPNSOnSubscribeModal.js @@ -165,10 +165,10 @@ const Modal = styled.div` width: max(70vw, 350px); padding: 2em; .modal__content { - display: flex; - flex-direction: column; - align-items: center; - gap: 20px; + display: flex !important; + flex-direction: column !important; + align-items: center !important; + gap: 10px !important; } } ` From b51471f32565fb0d089b6fc77f2b9ffb584a2739 Mon Sep 17 00:00:00 2001 From: Subhranshu Das Date: Sun, 22 May 2022 22:45:13 +0530 Subject: [PATCH 6/6] pr comments: disable link --- src/components/EPNS/EPNSNotificationModal.js | 12 +----- src/components/EPNS/SwitchNetwork.js | 39 ------------------- src/components/EPNS/index.js | 3 +- src/components/EPNS/useWeb3.js | 4 +- .../ExpiryNotifyDropdown.js | 13 ++++--- 5 files changed, 14 insertions(+), 57 deletions(-) delete mode 100644 src/components/EPNS/SwitchNetwork.js diff --git a/src/components/EPNS/EPNSNotificationModal.js b/src/components/EPNS/EPNSNotificationModal.js index 519166b07..1d2760c18 100644 --- a/src/components/EPNS/EPNSNotificationModal.js +++ b/src/components/EPNS/EPNSNotificationModal.js @@ -12,7 +12,6 @@ import Loader from '../Loader' import { useWeb3Data } from './useWeb3' import { selectConfig, isUserSubscribed, optIn, optOut } from './EPNSUtil' import OnSubscribedModal from './EPNSOnSubscribeModal' -import NetworkSwitch from './SwitchNetwork' const LoadingComponent = styled(Loader)` display: inline-flex; @@ -21,13 +20,6 @@ const LoadingComponent = styled(Loader)` vertical-align: middle; ` -const Header = styled('h3')` - font-size: 2rem; - font-weight: 700; - margin-top: 0; - margin-bottom: 0; -` - const FormComponent = styled('form')` display: grid; grid-gap: 10px; @@ -210,9 +202,7 @@ const EPNSNotificationModal = ({ address, onCancel }) => { {t('epns.modal.platforms')} - {!networkSupported ? ( - {t('epns.modal.switchNetwork')} - ) : isLoading ? ( + {isLoading ? ( ) : isChannelSubscribed ? ( { - async function handleClick(e) { - e.preventDefault() - - try { - debugger - await window.ethereum.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x1' }] - }) - console.log('You have switched to the Ethereum Main network') - } catch (switchError) { - console.error('Cannot switch to the network', switchError) - } - } - - return ( - <> - - {children} - - - ) -} - -export default NetworkSwitch diff --git a/src/components/EPNS/index.js b/src/components/EPNS/index.js index a77b683fe..d3111da37 100644 --- a/src/components/EPNS/index.js +++ b/src/components/EPNS/index.js @@ -1,4 +1,5 @@ import EPNSLink from './EPNSLink' import EPNSNotificationModal from './EPNSNotificationModal' +import { useWeb3Data } from './useWeb3' -export { EPNSLink, EPNSNotificationModal } +export { EPNSLink, EPNSNotificationModal, useWeb3Data } diff --git a/src/components/EPNS/useWeb3.js b/src/components/EPNS/useWeb3.js index 3001c3028..7b21665dd 100644 --- a/src/components/EPNS/useWeb3.js +++ b/src/components/EPNS/useWeb3.js @@ -4,6 +4,7 @@ import { getSigner, getNetworkId } from '@ensdomains/ui' export function useWeb3Data() { const [signer, setSigner] = useState(null) const [networkId, setNetworkId] = useState(null) + const isEpnsSupportedNetwork = [1, 42].includes(networkId) useEffect(() => { async function init() { @@ -19,6 +20,7 @@ export function useWeb3Data() { return { signer, - networkId + networkId, + isEpnsSupportedNetwork } } diff --git a/src/components/ExpiryNotification/ExpiryNotifyDropdown.js b/src/components/ExpiryNotification/ExpiryNotifyDropdown.js index 3f0842477..37e9cd6f6 100644 --- a/src/components/ExpiryNotification/ExpiryNotifyDropdown.js +++ b/src/components/ExpiryNotification/ExpiryNotifyDropdown.js @@ -7,7 +7,7 @@ import EmailNotifyLink from './EmailNotifyLink' import Modal from '../Modal/Modal' import ExpiryNotificationModal from './ExpiryNotificationModal' import { useOnClickOutside } from 'components/hooks' -import { EPNSLink, EPNSNotificationModal } from '../EPNS' +import { EPNSLink, EPNSNotificationModal, useWeb3Data } from '../EPNS' const ExpiryNotifyDropdownContainer = styled('div')` position: relative; @@ -22,6 +22,7 @@ export default function ExpiryNotifyDropdown({ address }) { const [showModal, setShowModal] = useState(false) const [optionSelected, setOptionSelected] = useState(false) const { t } = useTranslation() + const { isEpnsSupportedNetwork } = useWeb3Data() useOnClickOutside([dropdownRef, togglerRef], () => setShowDropdown(false)) @@ -61,9 +62,11 @@ export default function ExpiryNotifyDropdown({ address }) { {t('c.email')} - - {t('epns.link')} - + {isEpnsSupportedNetwork && ( + + {t('epns.link')} + + )}
)} {showModal && optionSelected === 'email' && ( @@ -74,7 +77,7 @@ export default function ExpiryNotifyDropdown({ address }) { )} - {showModal && optionSelected === 'epns' && ( + {isEpnsSupportedNetwork && showModal && optionSelected === 'epns' && (