Skip to content

Commit

Permalink
🧽 Notifications MVP last fixes (Joystream#4641)
Browse files Browse the repository at this point in the history
* Make the custom backend and faucet links optional

* Fix custom network form minor style issues

* Only show notification tab when the backend is set

* Fix setting page styles

* Do not show registration modal to registered members

* Refetch queries when the token changes

* Fix test

* Fix tests again
  • Loading branch information
thesan authored Nov 23, 2023
1 parent 5be8f61 commit 37c3922
Show file tree
Hide file tree
Showing 17 changed files with 180 additions and 149 deletions.
2 changes: 1 addition & 1 deletion packages/ui/src/app/hooks/usePageTabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface Options {
hasChanges?: boolean
}

export type TabsDefinition = [string, Path] | [string, Path, number] | [string, Path, Options]
export type TabsDefinition = readonly [string, Path] | [string, Path, number] | [string, Path, Options]

export const usePageTabs = (tabs: TabsDefinition[]) => {
const history = useHistory()
Expand Down
17 changes: 11 additions & 6 deletions packages/ui/src/app/pages/Settings/SettingsLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { ReactNode } from 'react'
import React, { ReactNode, useContext } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'

import { PageHeader } from '@/app/components/PageHeader'
import { PageLayout } from '@/app/components/PageLayout'
import { usePageTabs } from '@/app/hooks/usePageTabs'
import { BackendContext } from '@/app/providers/backend/context'
import { ButtonPrimary } from '@/common/components/buttons'
import { MainPanel } from '@/common/components/page/PageContent'
import { Tabs } from '@/common/components/Tabs'
Expand All @@ -17,15 +18,19 @@ export type SettingsLayoutProps = {
disabled: boolean
onClick: () => void
}
fullWidth?: boolean
children?: ReactNode
}

export const SettingsLayout = ({ saveButton, children }: SettingsLayoutProps) => {
export const SettingsLayout = ({ saveButton, fullWidth, children }: SettingsLayoutProps) => {
const { t } = useTranslation('settings')
const backendContext = useContext(BackendContext)
const notificationsTab = [t('notifications'), SettingsRoutes.notifications] as const
const tabs = usePageTabs([
[t('network'), SettingsRoutes.settings],
[t('notifications'), SettingsRoutes.notifications],
...(backendContext.backendClient ? [notificationsTab] : []),
])

return (
<PageLayout
header={
Expand All @@ -42,14 +47,14 @@ export const SettingsLayout = ({ saveButton, children }: SettingsLayoutProps) =>
/>
}
main={
<Container>
<Container fullWidth={fullWidth}>
<MainPanel>{children}</MainPanel>
</Container>
}
/>
)
}

export const Container = styled.div`
max-width: 690px;
export const Container = styled.div<{ fullWidth?: boolean }>`
max-width: ${({ fullWidth = false }) => (fullWidth ? 'auto' : '690px')};
`
193 changes: 108 additions & 85 deletions packages/ui/src/app/pages/Settings/SettingsNetworkTab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'
import { useForm, FormProvider } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'

import { useApi } from '@/api/hooks/useApi'
import { NetworkType, NetworkEndpoints } from '@/app/config'
Expand All @@ -14,6 +15,7 @@ import { PolkadotAppInfo } from '@/common/components/PolkadotAppInfo'
import { SimpleSelect } from '@/common/components/selects'
import { SettingsInformation, SettingsWarningInformation } from '@/common/components/SettingsInformation'
import { TextMedium } from '@/common/components/typography'
import { Colors } from '@/common/constants'
import { useLocalStorage } from '@/common/hooks/useLocalStorage'
import { useNetwork } from '@/common/hooks/useNetwork'
import { useNetworkEndpoints } from '@/common/hooks/useNetworkEndpoints'
Expand Down Expand Up @@ -57,75 +59,25 @@ export const SettingsNetworkTab = () => {
if (network === 'custom') {
form.setValue('settings.customRpcEndpoint', endpoints.nodeRpcEndpoint)
form.setValue('settings.customQueryEndpoint', endpoints.queryNodeEndpoint)
form.setValue('settings.customFaucetEndpoint', endpoints.membershipFaucetEndpoint)
form.setValue('settings.customBackendEndpoint', endpoints.backendEndpoint)
form.setValue('settings.customFaucetEndpoint', endpoints.membershipFaucetEndpoint ?? '')
form.setValue('settings.customBackendEndpoint', endpoints.backendEndpoint ?? '')
}
}, [network, endpoints])

const checkFaucetEndpoint = async () => {
// check faucet endpoint
try {
const faucetStatusEndpoint = customFaucetEndpoint.replace(new RegExp('register$'), 'status')
const response = await fetch(faucetStatusEndpoint)
const succeeded = response.status < 400
setIsValidFaucetEndpoint(succeeded)
return succeeded
} catch {
setIsValidFaucetEndpoint(false)
return false
}
}

const checkRpcEndpoint = async () => {
// check RPC endpoint
try {
return await new Promise<boolean>((resolve) => {
const ws = new WebSocket(customRpcEndpoint)
const willResolveTo = (succeeded: boolean, timeout?: any) => () => {
if (timeout) clearTimeout(timeout)

ws.close()
setIsValidRpcEndpoint(succeeded)
resolve(succeeded)
}

const timeout = setTimeout(willResolveTo(false), 3000)
ws.onopen = willResolveTo(true, timeout)
ws.onerror = willResolveTo(false, timeout)
})
} catch {
setIsValidRpcEndpoint(false)
return false
}
}

const checkGQLEndpoint = async (endpoint: string, setIsValidEndpoint: (v: boolean) => void) => {
// check GraphQL endpoint
try {
const response = await fetch(endpoint + '?query=%7B__typename%7D')
const succeeded = response.status < 400 && (await response.json()).data['__typename'] === 'Query'
setIsValidEndpoint(succeeded)
return succeeded
} catch {
setIsValidEndpoint(false)
return false
}
}

useEffect(() => {
if (
isValidFaucetEndpoint &&
isValidRpcEndpoint &&
isValidQueryEndpoint &&
isValidFaucetEndpoint &&
isValidBackendEndpoint &&
customSaveStatus === 'Done'
) {
storeCustomEndpoints({
nodeRpcEndpoint: customRpcEndpoint,
queryNodeEndpoint: customQueryEndpoint,
membershipFaucetEndpoint: customFaucetEndpoint,
queryNodeEndpointSubscription: customQueryEndpoint.replace(/^http?/, 'ws'),
backendEndpoint: customBackendEndpoint,
membershipFaucetEndpoint: customFaucetEndpoint || undefined,
backendEndpoint: customBackendEndpoint || undefined,
configEndpoint: undefined,
})
window.location.reload()
Expand All @@ -134,21 +86,21 @@ export const SettingsNetworkTab = () => {

const saveSettings = async () => {
if (
!isValidUrl(customFaucetEndpoint) ||
!isValidUrl(customRpcEndpoint, 'wss?') ||
!isValidUrl(customQueryEndpoint) ||
!isValidUrl(customBackendEndpoint)
!isValidRPCUrl(customRpcEndpoint) ||
!isValidQNUrl(customQueryEndpoint) ||
!isValidFaucetUrl(customFaucetEndpoint) ||
!isValidBackendUrl(customBackendEndpoint)
) {
return
}

setCustomSaveStatus('Saving')

await Promise.all([
checkFaucetEndpoint(),
checkRpcEndpoint(),
checkGQLEndpoint(customQueryEndpoint, setIsValidQueryEndpoint),
checkGQLEndpoint(customBackendEndpoint, setIsValidBackendEndpoint),
checkEndpoint(customRpcEndpoint, checkRpcEndpoint, setIsValidRpcEndpoint),
checkEndpoint(customQueryEndpoint, checkGQLEndpoint, setIsValidQueryEndpoint),
checkEndpoint(customFaucetEndpoint, checkFaucetEndpoint, setIsValidFaucetEndpoint),
checkEndpoint(customBackendEndpoint, checkGQLEndpoint, setIsValidBackendEndpoint),
])

setCustomSaveStatus('Done')
Expand All @@ -173,25 +125,12 @@ export const SettingsNetworkTab = () => {
</TextMedium>
</ColumnGapBlock>
</SettingsWarningInformation>
<InputComponent
label={t('customFaucet')}
validation={isValidUrl(customFaucetEndpoint) && isValidFaucetEndpoint ? undefined : 'invalid'}
message={cond(
[() => !isValidUrl(customFaucetEndpoint), 'This Faucet endpoint must start with http or https'],
[() => !isValidFaucetEndpoint, 'Connection Error']
)}
>
<InputText
id="field-custom-faucet"
placeholder="Paste faucet URL address"
name="settings.customFaucetEndpoint"
/>
</InputComponent>

<InputComponent
label={t('customRPCNode')}
validation={isValidUrl(customRpcEndpoint, 'wss?') && isValidRpcEndpoint ? undefined : 'invalid'}
validation={isValidRPCUrl(customRpcEndpoint) && isValidRpcEndpoint ? undefined : 'invalid'}
message={cond(
[() => !isValidUrl(customRpcEndpoint, 'wss?'), 'This RPC endpoint must start with ws or wss'],
[() => !isValidRPCUrl(customRpcEndpoint), 'This RPC endpoint must start with ws or wss'],
[
() => !isValidRpcEndpoint,
'Connection Error. Sometimes it fails due to network speed. Please try to check once more',
Expand All @@ -200,11 +139,12 @@ export const SettingsNetworkTab = () => {
>
<InputText id="field-custom-rpcnode" placeholder="Paste RPC node" name="settings.customRpcEndpoint" />
</InputComponent>

<InputComponent
label={t('customQueryNode')}
validation={isValidUrl(customQueryEndpoint) && isValidQueryEndpoint ? undefined : 'invalid'}
validation={isValidQNUrl(customQueryEndpoint) && isValidQueryEndpoint ? undefined : 'invalid'}
message={cond(
[() => !isValidUrl(customQueryEndpoint), 'This Query endpoint must start with http or https'],
[() => !isValidQNUrl(customQueryEndpoint), 'This Query endpoint must start with http or https'],
[() => !isValidQueryEndpoint, 'Connection Error']
)}
>
Expand All @@ -214,11 +154,30 @@ export const SettingsNetworkTab = () => {
name="settings.customQueryEndpoint"
/>
</InputComponent>

<InputComponent
label={t('customFaucet')}
validation={isValidFaucetUrl(customFaucetEndpoint) && isValidFaucetEndpoint ? undefined : 'invalid'}
message={cond(
[() => !isValidFaucetUrl(customFaucetEndpoint), 'This Faucet endpoint must start with http or https'],
[() => !isValidFaucetEndpoint, 'Connection Error']
)}
>
<InputText
id="field-custom-faucet"
placeholder="Paste faucet URL address"
name="settings.customFaucetEndpoint"
/>
</InputComponent>

<InputComponent
label={t('customBackend')}
validation={isValidUrl(customBackendEndpoint) && isValidBackendEndpoint ? undefined : 'invalid'}
validation={isValidBackendUrl(customBackendEndpoint) && isValidBackendEndpoint ? undefined : 'invalid'}
message={cond(
[() => !isValidUrl(customBackendEndpoint), 'This Backend endpoint must start with http or https'],
[
() => !isValidBackendUrl(customBackendEndpoint),
'This Backend endpoint must start with http or https',
],
[() => !isValidBackendEndpoint, 'Connection Error']
)}
>
Expand All @@ -228,9 +187,10 @@ export const SettingsNetworkTab = () => {
name="settings.customBackendEndpoint"
/>
</InputComponent>

<ButtonPrimary onClick={saveSettings} size="medium">
Save settings
{customSaveStatus === 'Saving' && <Loading />}
{customSaveStatus === 'Saving' && <CustomLoading />}
</ButtonPrimary>
</FormProvider>
)}
Expand All @@ -244,6 +204,7 @@ export const SettingsNetworkTab = () => {
networkAddress={endpoints.nodeRpcEndpoint}
queryNodeAddress={endpoints.queryNodeEndpoint}
faucetAddress={endpoints.membershipFaucetEndpoint}
backendAddress={endpoints.backendEndpoint}
/>
<PolkadotAppInfo rpcUrl={endpoints.nodeRpcEndpoint} />
<SettingsInformation icon={<WarnedIcon />} title={t('chainInfo')}>
Expand All @@ -261,4 +222,66 @@ export const SettingsNetworkTab = () => {
)
}

const isValidUrl = (url: string, prefix = 'https?') => RegExp(String.raw`${prefix}://\w+/?`, 'i').test(url)
type IsValidOptions = { prefix?: 'https?' | 'wss?'; isRequired?: boolean }
const isValid = (url: string, { prefix = 'https?', isRequired = true }: IsValidOptions = {}) =>
(isRequired === false && url === '') || RegExp(String.raw`${prefix}://\w+/?`, 'i').test(url)

const isValidRPCUrl = (url: string) => isValid(url, { prefix: 'wss?' })
const isValidQNUrl = (url: string) => isValid(url)
const isValidFaucetUrl = (url: string) => isValid(url, { isRequired: false })
const isValidBackendUrl = (url: string) => isValid(url, { isRequired: false })

const checkEndpoint = async (
endpoint: string,
check: (endpoint: string) => Promise<boolean>,
setEndpointIsValid: (isValid: boolean) => void
) => {
const isValid = endpoint ? await check(endpoint) : true
setEndpointIsValid(isValid)
return isValid
}

const checkRpcEndpoint = async (endpoint: string) => {
// check RPC endpoint
try {
return await new Promise<boolean>((resolve) => {
const ws = new WebSocket(endpoint)
const willResolveTo = (succeeded: boolean, timeout?: any) => () => {
if (timeout) clearTimeout(timeout)
ws.close()
resolve(succeeded)
}

const timeout = setTimeout(willResolveTo(false), 3000)
ws.onopen = willResolveTo(true, timeout)
ws.onerror = willResolveTo(false, timeout)
})
} catch {
return false
}
}

const checkGQLEndpoint = async (endpoint: string) => {
// check GraphQL endpoint
try {
const response = await fetch(endpoint + '?query=%7B__typename%7D')
return response.status < 400 && (await response.json()).data['__typename'] === 'Query'
} catch {
return false
}
}

const checkFaucetEndpoint = async (endpoint: string) => {
// check faucet endpoint
try {
const faucetStatusEndpoint = endpoint.replace(new RegExp('register$'), 'status')
const response = await fetch(faucetStatusEndpoint)
return response.status < 400
} catch {
return false
}
}

const CustomLoading = styled(Loading)`
fill: ${Colors.White};
`
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,14 @@ export default {
},
{
query: GetBackendMeDocument,
data: args.isAuthorized
? {
me: {
email: args.isEmailConfirmed ? email : null,
unverifiedEmail: args.isEmailConfirmed ? null : email,
receiveEmails: true,
name: 'test',
},
}
: undefined,
data: {
me: {
email: args.isEmailConfirmed ? email : null,
unverifiedEmail: args.isEmailConfirmed ? null : email,
receiveEmails: true,
name: 'test',
},
},
error: args.isAuthorized ? undefined : new Error('Unauthorized'),
},
],
Expand All @@ -110,14 +108,8 @@ export default {
},

backend: {
notificationsSettingsMap: args.isAuthorized
? {
[alice.id]: {
accessToken: 'token',
},
}
: {},
onSetMemberSettings: (...settingsArgs: any[]) => args.onSetMemberSettings(...settingsArgs),
authToken: SIGNIN_TOKEN,
},
}
},
Expand Down
Loading

0 comments on commit 37c3922

Please sign in to comment.