Skip to content

Commit

Permalink
Merge branch 'main' into MPDX-8347-donation-rounding
Browse files Browse the repository at this point in the history
  • Loading branch information
caleballdrin committed Oct 14, 2024
2 parents a8fa747 + 2b6212e commit f14bcad
Show file tree
Hide file tree
Showing 129 changed files with 45,776 additions and 44,145 deletions.
9 changes: 7 additions & 2 deletions onesky/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@ onesky
if (!fs.existsSync('public/locales/' + lang.code)) {
fs.promises.mkdir('public/locales/' + lang.code);
}
const sortedContent = Object.fromEntries(
Object.entries(JSON.parse(langContent)).sort(([key1], [key2]) =>
key1.localeCompare(key2),
),
);
fs.promises.writeFile(
'public/locales/' + lang.code + '/translation.json',
langContent,
JSON.stringify(sortedContent, null, 2),
);
// For printing results in CLI log
console.log(langContent);
console.log(sortedContent);
})
.catch(function (langError) {
console.log(langError); // log error results
Expand Down
3 changes: 3 additions & 0 deletions pages/_document.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class MyDocument extends Document {
* defer. This is not ideal for first-load performance and should be
* switched to <Script> as soon as Helpjuice can fix their swifty.js
* script.
* Caleb Cox reached out to HelpJuice on August 30, 2024, and they
* a ticket for the issue on October 10, 2024.
* https://helpjuice.canny.io/feature-requests/p/swifty-swiftyjs-beacon-setup-fails-due-to-domcontentloaded-event-already-fired
*/}
{process.env.HELPJUICE_ORIGIN && (
<script
Expand Down
156 changes: 156 additions & 0 deletions pages/acceptInvite.page.tsx
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;
184 changes: 184 additions & 0 deletions pages/acceptInvite.test.tsx
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);
});
});
Loading

0 comments on commit f14bcad

Please sign in to comment.