-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into MPDX-8347-donation-rounding
- Loading branch information
Showing
129 changed files
with
45,776 additions
and
44,145 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import Head from 'next/head'; | ||
import { useRouter } from 'next/router'; | ||
import React, { ReactElement, useEffect, useRef } from 'react'; | ||
import { useSnackbar } from 'notistack'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { SetupPage } from 'src/components/Setup/SetupPage'; | ||
import useGetAppSettings from 'src/hooks/useGetAppSettings'; | ||
import { useRequiredSession } from 'src/hooks/useRequiredSession'; | ||
import { loadSession } from './api/utils/pagePropsHelpers'; | ||
|
||
interface FetchAcceptInviteProps { | ||
apiToken: string; | ||
url: string; | ||
inviteType: string; | ||
id: string; | ||
code: string; | ||
} | ||
|
||
export const fetchAcceptInvite = ({ | ||
apiToken, | ||
url, | ||
inviteType, | ||
id, | ||
code, | ||
}: FetchAcceptInviteProps): Promise<Response> => { | ||
return fetch(process.env.REST_API_URL + url, { | ||
method: 'PUT', | ||
headers: { | ||
authorization: `Bearer ${apiToken}`, | ||
'content-type': 'application/vnd.api+json', | ||
}, | ||
body: JSON.stringify({ | ||
data: { | ||
type: inviteType, | ||
id, | ||
attributes: { code }, | ||
}, | ||
}), | ||
}); | ||
}; | ||
|
||
const AcceptInvitePage = (): ReactElement => { | ||
const { t } = useTranslation(); | ||
const { appName } = useGetAppSettings(); | ||
const { enqueueSnackbar } = useSnackbar(); | ||
const router = useRouter(); | ||
const query = router.query; | ||
const session = useRequiredSession(); | ||
|
||
// Ref to track if the API call has already been triggered | ||
const hasFetchedRef = useRef(false); | ||
// Since we don't have the user's accountListId, use an invalid accountListId so the page redirects on it's own. | ||
const dashboardLink = '/accountLists/_/'; | ||
|
||
useEffect(() => { | ||
if (!router.isReady || hasFetchedRef.current) { | ||
return; | ||
} | ||
const orgInviteId = | ||
typeof query.orgInviteId === 'string' ? query.orgInviteId : undefined; | ||
const orgId = typeof query.orgId === 'string' ? query.orgId : undefined; | ||
const accountInviteId = | ||
typeof query.accountInviteId === 'string' | ||
? query.accountInviteId | ||
: undefined; | ||
const inviteCode = | ||
typeof query.inviteCode === 'string' ? query.inviteCode : undefined; | ||
|
||
const inviterAccountListId = router.query.accountListId || undefined; | ||
const acceptInvite = async (id: string, code: string, url: string) => { | ||
const inviteType = url.includes('organizations') | ||
? 'organization_invites' | ||
: 'account_list_invites'; | ||
|
||
try { | ||
const apiToken = session.apiToken; | ||
|
||
const response = await fetchAcceptInvite({ | ||
apiToken, | ||
url, | ||
inviteType, | ||
id, | ||
code, | ||
}); | ||
|
||
if (!response.ok) { | ||
throw new Error('Network response was not ok'); | ||
} | ||
if (url.includes('organizations')) { | ||
enqueueSnackbar(t('Accepted invite successfully.'), { | ||
variant: 'success', | ||
}); | ||
|
||
router.push( | ||
dashboardLink + 'settings/integrations?selectedTab=organization', | ||
); | ||
} else { | ||
enqueueSnackbar(t('Accepted invite successfully.'), { | ||
variant: 'success', | ||
}); | ||
router.push(dashboardLink); | ||
} | ||
} catch (err) { | ||
const inviter = url.includes('organizations') | ||
? t('organization admin') | ||
: t('account holder'); | ||
enqueueSnackbar( | ||
t( | ||
'Unable to accept invite. Try asking the {{inviter}} to resend the invite.', | ||
{ inviter }, | ||
), | ||
{ | ||
variant: 'error', | ||
}, | ||
); | ||
} | ||
}; | ||
|
||
if (accountInviteId && inviteCode && inviterAccountListId) { | ||
const url = `account_lists/${inviterAccountListId}/invites/${accountInviteId}/accept`; | ||
acceptInvite(accountInviteId, inviteCode, url); | ||
hasFetchedRef.current = true; | ||
} else if (orgInviteId && inviteCode && orgId) { | ||
const url = `organizations/${orgId}/invites/${orgInviteId}/accept`; | ||
acceptInvite(orgInviteId, inviteCode, url); | ||
hasFetchedRef.current = true; | ||
} else { | ||
enqueueSnackbar( | ||
t( | ||
'Invalid invite URL. Try asking the the inviter to resend the invite.', | ||
), | ||
{ | ||
variant: 'error', | ||
}, | ||
); | ||
router.push(dashboardLink); | ||
} | ||
}, [router.query]); | ||
|
||
return ( | ||
<> | ||
<Head> | ||
<title> | ||
{appName} | {t('Accept Invite')} | ||
</title> | ||
</Head> | ||
<SetupPage title={t('Accepting Invite')}> | ||
<p>{t('You will be redirected soon...')}</p> | ||
</SetupPage> | ||
</> | ||
); | ||
}; | ||
|
||
export const getServerSideProps = loadSession; | ||
|
||
export default AcceptInvitePage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import React from 'react'; | ||
import { render, waitFor } from '@testing-library/react'; | ||
import { SnackbarProvider } from 'notistack'; | ||
import { I18nextProvider } from 'react-i18next'; | ||
import TestRouter from '__tests__/util/TestRouter'; | ||
import { AppSettingsProvider } from 'src/components/common/AppSettings/AppSettingsProvider'; | ||
import i18n from 'src/lib/i18n'; | ||
import AcceptInvitePage from './acceptInvite.page'; | ||
import 'node-fetch'; | ||
|
||
jest.mock('node-fetch', () => jest.fn()); | ||
jest.mock('src/hooks/useAccountListId'); | ||
const mockPush = jest.fn(); | ||
const dashboardLink = '/accountLists/_/'; | ||
|
||
const mockEnqueue = jest.fn(); | ||
jest.mock('notistack', () => ({ | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
...jest.requireActual('notistack'), | ||
useSnackbar: () => { | ||
return { | ||
enqueueSnackbar: mockEnqueue, | ||
}; | ||
}, | ||
})); | ||
|
||
describe('AcceptInvitePage', () => { | ||
beforeEach(() => { | ||
global.fetch = jest.fn(); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
(fetch as jest.Mock).mockClear(); | ||
}); | ||
|
||
it('renders the page and accepts an invite', async () => { | ||
const router = { | ||
isReady: true, | ||
query: { | ||
accountListId: 'test-account-list-id', | ||
inviteCode: 'test-invite-code', | ||
accountInviteId: 'test-invite-id', | ||
}, | ||
push: mockPush, | ||
}; | ||
(fetch as jest.Mock).mockResolvedValueOnce({ | ||
ok: true, | ||
json: jest.fn().mockResolvedValueOnce({}), | ||
}); | ||
|
||
const { getByText } = render( | ||
<TestRouter router={router}> | ||
<AppSettingsProvider> | ||
<I18nextProvider i18n={i18n}> | ||
<SnackbarProvider> | ||
<AcceptInvitePage /> | ||
</SnackbarProvider> | ||
</I18nextProvider> | ||
</AppSettingsProvider> | ||
</TestRouter>, | ||
); | ||
|
||
expect(getByText(/You will be redirected soon.../i)).toBeInTheDocument(); | ||
|
||
await waitFor(() => { | ||
expect(mockEnqueue).toHaveBeenCalledWith( | ||
'Accepted invite successfully.', | ||
{ variant: 'success' }, | ||
); | ||
expect(mockPush).toHaveBeenCalledWith(dashboardLink); | ||
}); | ||
}); | ||
|
||
it('redirects to preferences settings for organization invites', async () => { | ||
const router = { | ||
isReady: true, | ||
query: { | ||
accountListId: 'test-account-list-id', | ||
inviteCode: 'test-invite-code', | ||
orgInviteId: 'test-org-invite-id', | ||
orgId: 'test-org-id', | ||
}, | ||
push: mockPush, | ||
}; | ||
|
||
(fetch as jest.Mock).mockResolvedValueOnce({ | ||
ok: true, | ||
json: jest.fn().mockResolvedValueOnce({}), | ||
}); | ||
|
||
const { getByText } = render( | ||
<TestRouter router={router}> | ||
<AppSettingsProvider> | ||
<I18nextProvider i18n={i18n}> | ||
<SnackbarProvider> | ||
<AcceptInvitePage /> | ||
</SnackbarProvider> | ||
</I18nextProvider> | ||
</AppSettingsProvider> | ||
</TestRouter>, | ||
); | ||
|
||
expect(getByText(/You will be redirected soon/i)).toBeInTheDocument(); | ||
|
||
await waitFor(() => { | ||
expect(mockEnqueue).toHaveBeenCalledWith( | ||
'Accepted invite successfully.', | ||
{ variant: 'success' }, | ||
); | ||
expect(mockPush).toHaveBeenCalledWith( | ||
`${dashboardLink}settings/integrations?selectedTab=organization`, | ||
); | ||
}); | ||
}); | ||
|
||
it('handles invite acceptance error', async () => { | ||
const router = { | ||
isReady: true, | ||
query: { | ||
accountListId: 'test-account-list-id', | ||
inviteCode: 'test-invite-code', | ||
accountInviteId: 'another-invite-id', | ||
}, | ||
push: mockPush, | ||
}; | ||
(fetch as jest.Mock).mockResolvedValueOnce({ | ||
ok: false, | ||
json: jest.fn().mockResolvedValueOnce({}), | ||
}); | ||
|
||
render( | ||
<TestRouter router={router}> | ||
<AppSettingsProvider> | ||
<I18nextProvider i18n={i18n}> | ||
<SnackbarProvider> | ||
<AcceptInvitePage /> | ||
</SnackbarProvider> | ||
</I18nextProvider> | ||
</AppSettingsProvider> | ||
</TestRouter>, | ||
); | ||
|
||
await waitFor(() => { | ||
expect(mockEnqueue).toHaveBeenCalledWith( | ||
'Unable to accept invite. Try asking the account holder to resend the invite.', | ||
{ variant: 'error' }, | ||
); | ||
}); | ||
}); | ||
|
||
it('handles invalid url props', async () => { | ||
const router = { | ||
isReady: true, | ||
query: { | ||
accountListId: '', | ||
inviteCode: 'test-invite-code', | ||
accountInviteId: 'another-invite-id', | ||
}, | ||
push: mockPush, | ||
}; | ||
|
||
render( | ||
<TestRouter router={router}> | ||
<AppSettingsProvider> | ||
<I18nextProvider i18n={i18n}> | ||
<SnackbarProvider> | ||
<AcceptInvitePage /> | ||
</SnackbarProvider> | ||
</I18nextProvider> | ||
</AppSettingsProvider> | ||
</TestRouter>, | ||
); | ||
|
||
await waitFor(() => { | ||
expect(mockEnqueue).toHaveBeenCalledWith( | ||
'Invalid invite URL. Try asking the the inviter to resend the invite.', | ||
{ variant: 'error' }, | ||
); | ||
}); | ||
expect(mockPush).toHaveBeenCalledWith(dashboardLink); | ||
}); | ||
}); |
Oops, something went wrong.