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

handle backend errors #4580

Merged
merged 2 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
50 changes: 47 additions & 3 deletions packages/ui/src/app/App.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type Args = {
isRPCNodeConnected: boolean
hasRegisteredEmail: boolean
hasBeenAskedForEmail: boolean
subscribeEmailError: boolean
confirmEmailError: boolean
onBuyMembership: jest.Mock
onTransfer: jest.Mock
onSubscribeEmail: jest.Mock
Expand Down Expand Up @@ -82,6 +84,8 @@ export default {
isRPCNodeConnected: true,
hasRegisteredEmail: true,
hasBeenAskedForEmail: true,
subscribeEmailError: false,
confirmEmailError: false,
},

parameters: {
Expand Down Expand Up @@ -141,12 +145,14 @@ export default {
{
mutation: RegisterBackendMemberDocument,
onSend: (...sendArgs: any[]) => args.onSubscribeEmail(...sendArgs),
data: { signup: '' },
data: !args.subscribeEmailError ? { signup: '' } : undefined,
error: args.subscribeEmailError ? new Error('error') : undefined,
},
{
mutation: ConfirmBackendEmailDocument,
onSend: (...sendArgs: any[]) => args.onConfirmEmail(...sendArgs),
data: { confirmEmail: '' },
data: !args.confirmEmailError ? { confirmEmail: '' } : undefined,
error: args.confirmEmailError ? new Error('error') : undefined,
},
],
},
Expand Down Expand Up @@ -531,10 +537,34 @@ export const EmailSubscriptionModalSubscribe: Story = {
},
}

export const EmailSubscriptionModalSubscribeError: Story = {
args: {
isLoggedIn: true,
hasMemberships: true,
hasAccounts: true,
hasFunds: true,
hasWallet: true,
isRPCNodeConnected: true,
hasRegisteredEmail: false,
hasBeenAskedForEmail: false,
subscribeEmailError: true,
},
play: async ({ canvasElement }) => {
const modal = withinModal(canvasElement)
const button = modal.getByText(/^Sign and Authorize Email/i)
expect(button.closest('button')).toBeDisabled()
const testEmail = '[email protected]'
await userEvent.type(modal.getByPlaceholderText('Add email for notifications here'), testEmail)
await waitFor(() => expect(button.closest('button')).toBeEnabled())
await userEvent.click(button)
await waitFor(() => expect(modal.getAllByText(/Unexpected error/i)))
},
}

// ----------------------------------------------------------------------------
// Test Email Confirmation Modal
// ----------------------------------------------------------------------------
export const EmailConfirmationModal: Story = {
export const EmailConfirmationSuccess: Story = {
parameters: { router: { href: `/?${EMAIL_VERIFICATION_TOKEN_SEARCH_PARAM}=${MOCK_VERIFICATION_TOKEN}` } },
play: async ({ canvasElement, args: { onConfirmEmail } }) => {
const modal = withinModal(canvasElement)
Expand All @@ -546,3 +576,17 @@ export const EmailConfirmationModal: Story = {
expect(modal.getByText(/Your email has been confirmed/))
},
}

// ----------------------------------------------------------------------------
// Email Confirmation Error
// ----------------------------------------------------------------------------
export const EmailConfirmationError: Story = {
args: {
confirmEmailError: true,
},
parameters: { router: { href: `/?${EMAIL_VERIFICATION_TOKEN_SEARCH_PARAM}=${MOCK_VERIFICATION_TOKEN}` } },
play: async ({ canvasElement }) => {
const modal = withinModal(canvasElement)
await waitFor(() => expect(modal.getAllByText(/Unexpected error/i)))
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Args = {
isEmailConfirmed: boolean
activeMemberExistBackendLoading: boolean
activeMemberExistBackendError: boolean
updateMemberError: boolean
onMemberUpdate: CallableFunction
}
type Story = StoryObj<FC<Args>>
Expand All @@ -41,6 +42,7 @@ export default {
isEmailConfirmed: true,
activeMemberExistBackendLoading: false,
activeMemberExistBackendError: false,
updateMemberError: false,
},

parameters: {
Expand Down Expand Up @@ -82,6 +84,7 @@ export default {
{
mutation: UpdateBackendMemberDocument,
onSend: (...sendArgs: any[]) => args.onMemberUpdate(...sendArgs),
error: args.updateMemberError ? new Error('error') : undefined,
},
],
},
Expand Down Expand Up @@ -227,3 +230,28 @@ export const BackendLoading: Story = {
expect(screen.queryByText(/I want to be notified by email/)).toBeNull()
},
}

// ----------------------------------------------------------------------------
// Notifications settings: update error
// ----------------------------------------------------------------------------
export const UpdateError: Story = {
args: {
updateMemberError: true,
},

play: async ({ canvasElement }) => {
const screen = within(canvasElement)

expect(screen.getByText(/I want to be notified by email/)).toBeInTheDocument()

const saveChangesButton = getButtonByText(screen, /Save changes/i)
const _emailInput = screen.getByText(/Email/).parentElement?.querySelector('input')
expect(_emailInput).toBeDefined()
const emailInput = _emailInput as HTMLInputElement
await waitFor(() => expect(emailInput).toHaveValue(email))
userEvent.type(emailInput, 'm')
expect(saveChangesButton).toBeEnabled()
userEvent.click(saveChangesButton)
await waitFor(() => expect(screen.getByText(/Unexpected error/)).toBeInTheDocument())
},
}
30 changes: 20 additions & 10 deletions packages/ui/src/app/pages/Settings/SettingsNotificationsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import { SuccessModal } from '@/common/components/SuccessModal'
import { TextBig, TextMedium } from '@/common/components/typography'
import { Warning } from '@/common/components/Warning'
import { useModal } from '@/common/hooks/useModal'
import { error } from '@/common/logger'
import { useYupValidationResolver } from '@/common/utils/validation'
import { useNotificationSettings } from '@/memberships/hooks/useNotificationSettings'
import { BackendErrorModal } from '@/memberships/modals/BackendErrorModal'
import { EmailSubscriptionModalCall } from '@/memberships/modals/EmailSubscriptionModal'
import {
useGetBackendMeQuery,
Expand Down Expand Up @@ -67,7 +69,7 @@ export const SettingsNotificationsTab: FC = () => {
skip: !activeMemberSettings?.accessToken,
})

const [sendUpdateMemberMutation] = useUpdateBackendMemberMutation({
const [sendUpdateMemberMutation, { error: mutationError }] = useUpdateBackendMemberMutation({
client: backendClient,
})
const [newLinkGenerated, setNewLinkGenerated] = useState(false)
Expand Down Expand Up @@ -97,12 +99,17 @@ export const SettingsNotificationsTab: FC = () => {
}

const handleSaveChangesClick = handleSubmit(async (data) => {
await sendUpdateMemberMutation({
variables: {
email: dirtyFields.email ? data.email : undefined,
receiveEmails: dirtyFields.receiveEmailNotifications ? data.receiveEmailNotifications : undefined,
},
})
try {
await sendUpdateMemberMutation({
variables: {
email: dirtyFields.email ? data.email : undefined,
receiveEmails: dirtyFields.receiveEmailNotifications ? data.receiveEmailNotifications : undefined,
},
})
} catch (e) {
error(e)
}

setShowSettingsUpdatedModal(true)
})

Expand Down Expand Up @@ -176,9 +183,12 @@ You can customize what kind of notifications you receive anytime in settings."

return (
<>
{showSettingsUpdatedModal && (
<SuccessModal text="Settings saved successfully" onClose={() => setShowSettingsUpdatedModal(false)} />
)}
{showSettingsUpdatedModal &&
(mutationError ? (
<BackendErrorModal onClose={() => setShowSettingsUpdatedModal(false)} />
) : (
<SuccessModal text="Settings saved successfully" onClose={() => setShowSettingsUpdatedModal(false)} />
))}
<SettingsLayout
saveButton={{
isVisible: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { FC } from 'react'

import { Modal, ModalBody, ModalHeader } from '@/common/components/Modal'
import { Warning } from '@/common/components/Warning'

type BackendErrorModalProps = {
onClose: () => void
}

export const BackendErrorModal: FC<BackendErrorModalProps> = ({ onClose }) => {
return (
<Modal onClose={onClose} modalSize="m">
<ModalHeader onClick={onClose} title="Unexpected error" />
<ModalBody>
<Warning
content="There's been an unexpected error when communicating with notifications service. Please try again later."
isClosable={false}
/>
</ModalBody>
</Modal>
kdembler marked this conversation as resolved.
Show resolved Hide resolved
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './BackendErrorModal'
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { ModalWithDataCall } from '@/common/providers/modal/types'
import { useNotificationSettings } from '@/memberships/hooks/useNotificationSettings'
import { useConfirmBackendEmailMutation } from '@/memberships/queries/__generated__/backend.generated'

import { BackendErrorModal } from '../BackendErrorModal'

export type EmailConfirmationModalCall = ModalWithDataCall<
'EmailConfirmationModal',
{
Expand Down Expand Up @@ -49,14 +51,16 @@ export const EmailConfirmationModal = () => {
hideModal()
}

if (error) {
return <BackendErrorModal onClose={closeConfirmationModal} />
}

return (
<Modal onClose={closeConfirmationModal} modalSize="m">
<ModalHeader onClick={closeConfirmationModal} title="Email confirmation" />
<ModalBody>
{loading ? (
<Loading text="Confirming email..." withoutMargin />
) : error ? (
<TextMedium>Unexpected error occurred. Please try again later.</TextMedium>
) : (
<TextMedium>
Your email has been confirmed! You can always adjust your notification preferences in settings.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useEffect, useCallback } from 'react'

import { useMyAccounts } from '@/accounts/hooks/useMyAccounts'
import { FailureModal } from '@/common/components/FailureModal'
import { SuccessModal } from '@/common/components/SuccessModal'
import { WaitModal } from '@/common/components/WaitModal'
import { useMachine } from '@/common/hooks/useMachine'
Expand All @@ -13,6 +12,8 @@ import { EmailSubscriptionModalCall } from '@/memberships/modals/EmailSubscripti
import { getBackendAuthSignature } from '@/memberships/model/backendAuth'
import { useRegisterBackendMemberMutation } from '@/memberships/queries/__generated__/backend.generated'

import { BackendErrorModal } from '../BackendErrorModal'

import { EmailSubscriptionFormModal } from './EmaiSubscriptionFormModal'
import { EmailSubscriptionMachine } from './machine'
import { EmailSubscriptionForm } from './types'
Expand Down Expand Up @@ -100,13 +101,7 @@ export const EmailSubscriptionModal = () => {
}

if (state.matches('error')) {
return (
<FailureModal onClose={hideModal}>
There was a problem registering your email.
<br />
We could not register your email at the moment! Please, try again later!
</FailureModal>
)
return <BackendErrorModal onClose={hideModal} />
}

if (state.matches('signature')) {
Expand Down
5 changes: 4 additions & 1 deletion packages/ui/src/mocks/providers/gql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export { ApolloProvider } from '@apollo/client/react'

type OptionVariables = { where?: Record<string, any>; orderBy?: string | string[]; limit?: number; offset?: number }
type Options = { variables?: OptionVariables; skip?: boolean; onCompleted?: (data: any) => void }
type Result = { loading: boolean; data: any }
type Result = { loading: boolean; data: any; error?: any }
type Resolver = (options?: Options) => Result

type GqlQueriesMap = Map<DocumentNode, Resolver>
Expand Down Expand Up @@ -67,6 +67,9 @@ export const useMutation = (mutation: DocumentNode, options?: Options): [() => v
(...args: any[]) => {
spy?.(...args)
setMutationResult(result)
if (result.error) {
return Promise.reject(result.error)
}
return Promise.resolve(result.data)
},
[JSON.stringify(result)]
Expand Down