Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notifications frontend #904

Merged
merged 116 commits into from
Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
603384d
Add subsection (2nd level) routing to global preferences
2color Jul 11, 2019
3bd241d
Notifications: authentication flow
2color Jul 11, 2019
56249a7
Pass apps to notifications
2color Jul 11, 2019
a555477
Move api functions to separate file
2color Jul 11, 2019
53c0a6d
Move components and constants into separate file and add subscriptions
2color Jul 11, 2019
ce50333
Fix subsection parsing
2color Aug 7, 2019
83725be
Export extend error
2color Aug 7, 2019
d1a0424
Notifications auth flow and layout
2color Aug 7, 2019
1094ddd
Upgrade @aragon/ui
2color Aug 7, 2019
4a6cf4d
Fix linting errors
2color Aug 7, 2019
997132b
Remove unneeded comment
2color Aug 8, 2019
3ec3dcb
Pass apps to subscriptions component
2color Aug 8, 2019
46e759b
Improve state handling of verify flow
2color Aug 8, 2019
e3eb482
Update url of notification service to live one
2color Aug 8, 2019
0df8fb7
Add create subscriptions function
2color Aug 8, 2019
33ac665
Add subscription creation form
2color Aug 8, 2019
f472d3b
Vertically align notification login form
2color Aug 9, 2019
e94e361
Add logout function
2color Aug 9, 2019
3cc6d02
Style preverify and verify steps
2color Aug 9, 2019
62d1867
Handle and style email login/verification steps
2color Aug 12, 2019
8dcb3de
Use css for circle around checkmark
2color Aug 12, 2019
7fee586
Add delete account function
2color Aug 12, 2019
c03b565
Add parent component to manage notifications and delete account
2color Aug 12, 2019
cfa1ed1
Filter getSubscriptions for network
2color Aug 12, 2019
b83a16f
Create subscription journey
2color Aug 12, 2019
47aef63
Bump @aragon/ui for disabled dropdown state
2color Aug 15, 2019
790dd89
Remove dependency on email in hook
2color Aug 18, 2019
7ccf751
Typo correction
2color Aug 18, 2019
3dbb2bb
Use index when infering subsection in global preferences
2color Aug 18, 2019
08aa412
Verify token and add failed and authenticating states
2color Aug 18, 2019
1b51483
Make network type compatible with backend
2color Aug 18, 2019
1140652
Take single argument for consistency
2color Aug 18, 2019
9b01a77
Rename contractAddress to appContractAddress
2color Aug 18, 2019
15f73f2
Wrap in form to allow submission with enter key
2color Aug 18, 2019
a054ba2
Add email validation and style login error
2color Aug 18, 2019
9e76df6
Fix typo 🙈
2color Aug 18, 2019
76ee465
Use strong instead of span
2color Aug 19, 2019
ebf001d
Disable button while awaiting server response
2color Aug 19, 2019
ab682ea
Fix typo
2color Aug 19, 2019
c2afb65
Ensure correct order to avoid rendering problems
2color Aug 19, 2019
2585472
Use ButtonText for text link
2color Aug 21, 2019
b108200
Add comment about network type
2color Aug 21, 2019
6f99616
Use symbols for constants
2color Aug 21, 2019
e8d7085
Rename token to authToken for clarity
2color Aug 21, 2019
3df25fe
Bump @aragon/ui
2color Aug 21, 2019
f7201a2
Improve email validation
2color Aug 21, 2019
836df88
Add email validation icon
2color Aug 21, 2019
ed6c63b
Add email regex source
2color Aug 21, 2019
6577afa
Remove unused theme
2color Aug 21, 2019
607747c
Merge remote-tracking branch 'origin/newstyle' into newstyle-notifica…
2color Aug 21, 2019
b2e2a46
Submit an ABI subset with only the relevant event
2color Aug 21, 2019
d932092
Add deleteSubscriptions method
2color Aug 21, 2019
bdb2ff6
Use DataView for subscriptions table
2color Aug 21, 2019
6375f39
Fix prop type
2color Aug 21, 2019
474c41a
Render subscription filters
2color Aug 22, 2019
fd24a56
Do not reset app after creating a subscription
2color Aug 22, 2019
26afff7
Add subscription filters
2color Aug 22, 2019
2a4bc57
Render appName
2color Aug 26, 2019
e8b869f
Refine the subscription form
2color Aug 26, 2019
ebe2512
Style the table correctly
2color Aug 26, 2019
5c8808b
style and render clear filters conditionally
2color Aug 26, 2019
9eec3e0
Enable sign in button when email is valid
2color Aug 26, 2019
842a9b4
Disable the log in button initially
2color Aug 26, 2019
3ef0938
Use constants instead of magic numbers
2color Aug 26, 2019
f4a2077
Set unauthenticated state as the default too
2color Aug 26, 2019
2bd21d8
Update src/components/GlobalPreferences/Notifications/ManageNotificat…
2color Aug 26, 2019
2a1c1cc
Handle network error in verification
2color Aug 26, 2019
442467c
Handle verification errors more generally
2color Aug 26, 2019
ee7572e
Update login error
2color Aug 26, 2019
b0fdc3a
Show logout and reset email button in verification error states
2color Aug 26, 2019
e51e90b
Let there be consts
2color Aug 26, 2019
34ac76e
Remove completed todo
2color Aug 26, 2019
b459901
Use useCallback
2color Aug 26, 2019
55b77cb
Render loading ring when subscriptions are fetching
2color Aug 27, 2019
ba0a363
Add loading DAO state
2color Aug 27, 2019
c640eeb
Fix bugs in form when unsubscribing and subscribing to apps
2color Aug 27, 2019
23898b9
Improve subscription filters styling
2color Aug 27, 2019
2d25aa0
Use named function expression for subscriptions table
2color Aug 27, 2019
fa0e691
Style filter buttons vertically in small layouts
2color Aug 28, 2019
5ba5b31
Make function statement
2color Aug 28, 2019
a1bfb22
Render nothing while authenticating
2color Aug 28, 2019
352a7af
Set isFetching correctly and render table once fetched
2color Aug 28, 2019
1b0fbf6
Add cleanup function to useEffect hook
2color Aug 28, 2019
433ea60
Merge remote-tracking branch 'origin/newstyle' into newstyle-notifica…
2color Aug 28, 2019
122cab0
Update login copy
2color Aug 29, 2019
6554b8f
Update different verification states to align with designs
2color Aug 29, 2019
1ea0ccc
Navigate back when resetting account
2color Aug 29, 2019
e7e6c8f
Filter out non-relevant eventa
2color Aug 29, 2019
4034014
Prettify
2color Aug 29, 2019
4975906
Move errors to manage notifications and use InfoBox more generally
2color Aug 29, 2019
6dce2ee
Move subscriptions table to separate component
2color Aug 29, 2019
f2a2fb8
Merge remote-tracking branch 'origin/newstyle' into newstyle-notifica…
2color Aug 29, 2019
0f88a81
Global network error page with retry action
2color Aug 30, 2019
88f6300
Remove usused import
2color Aug 30, 2019
ff53022
Add confirmation modal and update images
2color Aug 30, 2019
295dbe8
Show image in form when subscribed to all events
2color Aug 30, 2019
b975615
Update delete account confirmation copy
2color Aug 30, 2019
93eed0c
notifications info-box: reduce margin between header and content
2color Sep 2, 2019
87ecabc
manage-notifications: make buttons consistently wide
2color Sep 2, 2019
484da85
Update expired magic link token message
2color Sep 2, 2019
a5f52ed
Refactor and simplify verification errors
2color Sep 2, 2019
6564e7c
Center the text to be aligned with the loading ring
2color Sep 2, 2019
cb159c1
subscriptions from: make submit button wide
2color Sep 2, 2019
0911d01
Revert "Render nothing while authenticating"
2color Sep 2, 2019
a4e109b
Style loading states
2color Sep 2, 2019
2bda588
Merge remote-tracking branch 'origin/newstyle' into newstyle-notifica…
2color Sep 2, 2019
a3205d1
Set consistent padding of 2/3 GU
2color Sep 2, 2019
dfe4899
Remove unused var
2color Sep 2, 2019
1992b1c
Style verify fetching state
2color Sep 2, 2019
0f788d0
Add automatic redirection upon successful verification
2color Sep 2, 2019
8eebf90
Unsubscribe from table modal
2color Sep 2, 2019
b8444bc
Render form loading only while apps are loading
2color Sep 2, 2019
987d05b
Fix bug with email input losing focus
2color Sep 2, 2019
1f1dec3
Merge branch 'master' into newstyle-notifications
2color Sep 3, 2019
6bda0ca
Improve mobile styles for feedback box
2color Sep 3, 2019
aa813ed
Make subscribe wide only in small viewports
2color Sep 3, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"dependencies": {
"@aragon/templates-tokens": "^1.2.1",
"@aragon/ui": "1.0.0-alpha.6",
"@aragon/ui": "1.0.0-alpha.10",
"@aragon/wrapper": "^5.0.0-rc.13",
"@babel/polyfill": "^7.0.0",
"@sentry/browser": "^5.5.0",
Expand Down
1 change: 1 addition & 0 deletions src/Wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ class Wrapper extends React.PureComponent {
<GlobalPreferences
locator={locator}
wrapper={wrapper}
apps={apps}
onClose={this.handleClosePreferences}
onHelpScoutOptedOutChange={onHelpScoutOptedOutChange}
helpScoutOptedOut={helpScoutOptedOut}
Expand Down
41 changes: 30 additions & 11 deletions src/components/GlobalPreferences/GlobalPreferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { useState, useEffect, useCallback } from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import {
Bar,
ButtonIcon,
GU,
IconClose,
Expand All @@ -16,7 +15,7 @@ import {
useViewport,
} from '@aragon/ui'
import { Transition, animated } from 'react-spring'
import { AragonType } from '../../prop-types'
import { AragonType, AppType } from '../../prop-types'
import { useEsc } from '../../hooks'
import Network from './Network/Network'
import Notifications from './Notifications/Notifications'
Expand All @@ -37,13 +36,15 @@ const VALUES = Array.from(SECTIONS.values())
const TIMEOUT_TOAST = 4000

function GlobalPreferences({
apps,
helpScoutOptedOut,
onClose,
onHelpScoutOptedOutChange,
toast,
wrapper,
locator,
sectionIndex,
subsection,
onNavigation,
}) {
const { dao } = locator
Expand Down Expand Up @@ -96,7 +97,15 @@ function GlobalPreferences({
<CustomLabels dao={dao} wrapper={wrapper} locator={locator} />
)}
{sectionIndex === 1 && <Network wrapper={wrapper} />}
{sectionIndex === 2 && <Notifications />}
{sectionIndex === 2 && (
<Notifications
apps={apps}
dao={dao}
subsection={subsection}
handleNavigation={onNavigation}
navigationIndex={2}
/>
)}
{sectionIndex === 3 && (
<HelpAndFeedback
optedOut={helpScoutOptedOut}
Expand All @@ -112,18 +121,21 @@ function GlobalPreferences({
}

GlobalPreferences.propTypes = {
apps: PropTypes.arrayOf(AppType).isRequired,
helpScoutOptedOut: PropTypes.bool,
onClose: PropTypes.func.isRequired,
onHelpScoutOptedOutChange: PropTypes.func.isRequired,
toast: PropTypes.func,
wrapper: AragonType,
locator: PropTypes.object,
sectionIndex: PropTypes.number,
subsection: PropTypes.string,
onNavigation: PropTypes.func.isRequired,
}

function useGlobalPreferences(locator = {}) {
const [sectionIndex, setSectionIndex] = useState(null)
const [subsection, setSubsection] = useState(null)
const handleNavigation = useCallback(
index => {
window.location.hash = `${getAppPath(
Expand All @@ -139,14 +151,18 @@ function useGlobalPreferences(locator = {}) {
setSectionIndex(null)
return
}
if (!SECTIONS.has(path)) {
setSectionIndex(null)
return
}
setSectionIndex(PATHS.findIndex(item => item === path))
}, [locator])
const index = PATHS.findIndex(item => path.startsWith(item))
setSectionIndex(index === -1 ? null : index)

// subsection is the part after the PATH, e.g. for `?p=/notifications/verify` - `/verify`
const subsection =
sectionIndex !== null ? path.substring(PATHS[sectionIndex].length) : null
2color marked this conversation as resolved.
Show resolved Hide resolved

return { sectionIndex, handleNavigation }
setSubsection(subsection)
// Does the current path start with any of the declared route paths
}, [locator, sectionIndex])

return { sectionIndex, subsection, handleNavigation }
sohkai marked this conversation as resolved.
Show resolved Hide resolved
}

function Close({ onClick }) {
Expand Down Expand Up @@ -196,7 +212,9 @@ Close.propTypes = {
}

function AnimatedGlobalPreferences(props) {
const { sectionIndex, handleNavigation } = useGlobalPreferences(props.locator)
const { sectionIndex, subsection, handleNavigation } = useGlobalPreferences(
props.locator
)
const { below } = useViewport()
const smallView = below('medium')
const theme = useTheme()
Expand Down Expand Up @@ -233,6 +251,7 @@ function AnimatedGlobalPreferences(props) {
<GlobalPreferences
{...props}
sectionIndex={sectionIndex}
subsection={subsection}
onNavigation={handleNavigation}
/>
</AnimatedWrap>
Expand Down
127 changes: 109 additions & 18 deletions src/components/GlobalPreferences/Notifications/Notifications.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,115 @@
import React from 'react'
import { Box } from '@aragon/ui'
import React, { useState, useEffect, useCallback } from 'react'
import PropTypes from 'prop-types'
import { AppType } from '../../../prop-types'
// import { Box } from '@aragon/ui'
import Subscriptions from './Subscriptions'
import NotificationsLogin from './NotificationsLogin'
import NotificationsVerify from './NotificationsVerify'

import {
AUTH_UNAUTHENTICATED,
AUTH_PREVERIFY,
AUTH_AUTHENTICATED,
NOTIFICATION_SERVICE_EMAIL_KEY,
NOTIFICATION_SERVICE_TOKEN_KEY,
VERIFY_SUBSECTION,
} from './constants'

// Hook responsible for deriving the authState from localStorage values and
// provide setters which update the localStorage
2color marked this conversation as resolved.
Show resolved Hide resolved
function useAuthState() {
const [authState, setAuthState] = useState(AUTH_UNAUTHENTICATED)
const [email, setEmail] = useState(
localStorage.getItem(NOTIFICATION_SERVICE_EMAIL_KEY)
)
const [token, setToken] = useState(
localStorage.getItem(NOTIFICATION_SERVICE_TOKEN_KEY)
)

useEffect(() => {
if (!email && !token) {
setAuthState(AUTH_UNAUTHENTICATED)
return
}

if (email && !token) {
setAuthState(AUTH_PREVERIFY)
return
}

if (email && token) {
Copy link
Contributor

@AquiGorka AquiGorka Aug 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we can assume these are both truthy no? No need for this check. Sorry, not true, email could be falsy. Now I'm curious if that state could happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't happen but for someone manipulating local storage. Do you think we should handle it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't worry about it happening, but because it could happen either have a last "default" return null or handling it specifically (last return is easiest, and if someone meddles with localStorage they should know better)

setAuthState(AUTH_AUTHENTICATED)
}
2color marked this conversation as resolved.
Show resolved Hide resolved
}, [email, token])
2color marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
token && localStorage.setItem(NOTIFICATION_SERVICE_TOKEN_KEY, token)
}, [token])

useEffect(() => {
email && localStorage.setItem(NOTIFICATION_SERVICE_EMAIL_KEY, email)
}, [email])

return {
authState,
email,
token,
handleTokenChange: setToken,
handleEmailChange: setEmail,
}
}

function Notifications({
apps,
subsection,
dao,
handleNavigation,
navigationIndex,
}) {
const navigateToNotifications = useCallback(
() => handleNavigation(navigationIndex),
[handleNavigation, navigationIndex]
)
const {
authState,
email,
token,
handleTokenChange,
handleEmailChange,
} = useAuthState()

if (subsection && subsection.startsWith(VERIFY_SUBSECTION)) {
return (
<NotificationsVerify
subsection={subsection}
onTokenChange={handleTokenChange}
onEmailChange={handleEmailChange}
navigateToNotifications={navigateToNotifications}
/>
)
}

if (authState === AUTH_AUTHENTICATED) {
// TODO: make sure token is valid
return <Subscriptions apps={apps} email={email} token={token} />
}

function Notifications() {
return (
<Box heading="Email notifications">
<label>
App notifications and reminders (upcoming votes, created incoming or
outgoing transactions, added or removed tokens, etc.).{' '}
<input type="checkbox" />
</label>
<label>
New releases and features announcements. <input type="checkbox" />
</label>
<label>
Calendar reminders of selected events or tasks.{' '}
<input type="checkbox" />
</label>
<button>Manage triggers</button>
</Box>
<NotificationsLogin
dao={dao}
email={email}
authState={authState}
onEmailChange={handleEmailChange}
/>
)
}

Notifications.propTypes = {
apps: PropTypes.arrayOf(AppType).isRequired,
subsection: PropTypes.string,
dao: PropTypes.string,
handleNavigation: PropTypes.func,
navigationIndex: PropTypes.number,
}

export default Notifications
116 changes: 116 additions & 0 deletions src/components/GlobalPreferences/Notifications/NotificationsLogin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import PropTypes from 'prop-types'
import { Box, Button, GU, TextInput, Text, Info, IconMail } from '@aragon/ui'
import { login } from './notification-service-api'
import { AUTH_PREVERIFY } from './constants'
import { getEthNetworkType } from '../../../local-settings'
2color marked this conversation as resolved.
Show resolved Hide resolved
import notificationSvg from './notifications.svg'
2color marked this conversation as resolved.
Show resolved Hide resolved
import checkmarkSvg from './check-mark.svg'

export default function NotificationsLogin({
dao,
authState,
email,
onEmailChange,
}) {
const [inputEmail, setInputEmail] = useState('')
const [apiError, setApiError] = useState(null)
const network = getEthNetworkType()

const handleEmailChange = e => {
setInputEmail(e.target.value)
2color marked this conversation as resolved.
Show resolved Hide resolved
}

const handleLogin = async e => {
try {
await login({ email: inputEmail, dao, network })
onEmailChange(inputEmail)
} catch (e) {
setApiError(e.message)
console.error('Failed to login', e)
}
}

if (authState === AUTH_PREVERIFY) {
return (
<Box heading="Email notifications">
<NotificationImage />
<div>
<Checkmark />
<Text>Awaiting verification. Please check your email!</Text>
2color marked this conversation as resolved.
Show resolved Hide resolved
<br />
<Text size="xsmall">
We’ve sent an email to {email}. Verify your email address so you can
manage your notifications subscriptions.
</Text>
</div>
</Box>
)
}
return (
<Box heading="Email notifications">
<NotificationImage />
{apiError && <Info mode="error">Error logging in:{apiError}</Info>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets just double check what should be the correct spacing here:
image
cc @dizzypaty
and we can help out the user with some other message, I was thrown off by "Failed to fetch".

<Label>Email address</Label>
2color marked this conversation as resolved.
Show resolved Hide resolved
<TextInput
css={`
width: 80%;
margin-right: 9px;
margin-bottom: 10px;
`}
sohkai marked this conversation as resolved.
Show resolved Hide resolved
type="email"
placeholder="[email protected]"
wide
value={inputEmail}
onChange={handleEmailChange}
/>
<Button onClick={handleLogin}>
2color marked this conversation as resolved.
Show resolved Hide resolved
<IconMail /> Sign in
</Button>
<Info>
Copy link
Contributor

@AquiGorka AquiGorka Aug 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dizzypaty would it make sense to split this in 2 info boxes? Feels a bit crowded it does communicate two different messages: what is this and how this works on diff devices with no password:

image

Receive email notifications for the new app events. For example,
2color marked this conversation as resolved.
Show resolved Hide resolved
whenever a new vote is created or when tokens added, you’ll get an email
2color marked this conversation as resolved.
Show resolved Hide resolved
informing you of the latest activity in your organization. How does it
2color marked this conversation as resolved.
Show resolved Hide resolved
work? You will be asked to enter with your email address whenever using
2color marked this conversation as resolved.
Show resolved Hide resolved
a different browser session or device to access your subsciptions. This
process doens’t require a password, just for you to confirm your email
2color marked this conversation as resolved.
Show resolved Hide resolved
address.
</Info>
</Box>
)
}

const NotificationImage = () => (
<img
src={notificationSvg}
alt="Notifications"
css={`
display: block;
margin: ${4 * GU}px auto;
height: 193px;
`}
/>
)

const Checkmark = () => (
2color marked this conversation as resolved.
Show resolved Hide resolved
<img
src={checkmarkSvg}
alt="check mark"
css={`
display: inline block;
margin: ${1 * GU}px auto;
`}
/>
)

const Label = styled.label`
display: block;
`

NotificationsLogin.propTypes = {
dao: PropTypes.string,
authState: PropTypes.string,
email: PropTypes.string,
onEmailChange: PropTypes.func,
}
Loading