From ee862e3197d054bf4a40f44eb0f3f0a5f57f9b7c Mon Sep 17 00:00:00 2001 From: Joseph Dvorak Date: Sun, 22 Sep 2024 11:28:21 -0400 Subject: [PATCH] CHAL-53 #done - Implement election reminders page (#36) * added election reminders page * added tests * added tests * added pledge-to-vote-iframe to coveragePathIgnorePatterns array --- jest-setup.js | 7 + jest.config.mjs | 3 + .../app/reminders/completed/page.test.tsx | 165 ++++++++++++++++++ .../unit/app/reminders/page.test.tsx | 36 ++++ .../guards/has-not-completed-action.test.tsx | 137 +++++++++++++++ ...if-supabase-user-completed-action.test.tsx | 98 +++++++++++ .../get-signed-in-request-with-user.test.ts | 109 ++++++++++++ .../completed-reminders.tsx | 59 +++++++ .../completed/completed-reminders/index.tsx | 1 + .../completed-reminders/styles.module.scss | 6 + src/app/reminders/completed/page.tsx | 13 ++ src/app/reminders/page.tsx | 25 +++ .../reminders/pledge-to-vote-iframe/index.tsx | 1 + .../pledge-to-vote-iframe.tsx | 88 ++++++++++ .../pledge-to-vote-iframe/styles.module.scss | 4 + src/app/reminders/styles.module.scss | 7 + .../guards/has-not-completed-action.tsx | 30 ++++ src/constants/signed-in-only-routes.ts | 2 +- src/services/server/container.ts | 5 + src/services/server/keys.ts | 5 +- src/services/server/middleware/middleware.ts | 26 ++- .../redirect-if-completed-action.ts | 14 ++ ...irect-if-supabase-user-completed-action.ts | 65 +++++++ src/styles/defaults/_body.scss | 2 +- src/styles/defaults/_html.scss | 3 + src/styles/defaults/_index.scss | 1 + .../test/get-signed-in-request-with-user.ts | 52 ++++++ 27 files changed, 959 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/unit/app/reminders/completed/page.test.tsx create mode 100644 src/__tests__/unit/app/reminders/page.test.tsx create mode 100644 src/__tests__/unit/components/guards/has-not-completed-action.test.tsx create mode 100644 src/__tests__/unit/services/server/redirect-if-completed-action/redirect-if-supabase-user-completed-action.test.tsx create mode 100644 src/__tests__/unit/utils/test/get-signed-in-request-with-user.test.ts create mode 100644 src/app/reminders/completed/completed-reminders/completed-reminders.tsx create mode 100644 src/app/reminders/completed/completed-reminders/index.tsx create mode 100644 src/app/reminders/completed/completed-reminders/styles.module.scss create mode 100644 src/app/reminders/completed/page.tsx create mode 100644 src/app/reminders/page.tsx create mode 100644 src/app/reminders/pledge-to-vote-iframe/index.tsx create mode 100644 src/app/reminders/pledge-to-vote-iframe/pledge-to-vote-iframe.tsx create mode 100644 src/app/reminders/pledge-to-vote-iframe/styles.module.scss create mode 100644 src/app/reminders/styles.module.scss create mode 100644 src/components/guards/has-not-completed-action.tsx create mode 100644 src/services/server/redirect-if-completed-action/redirect-if-completed-action.ts create mode 100644 src/services/server/redirect-if-completed-action/redirect-if-supabase-user-completed-action.ts create mode 100644 src/styles/defaults/_html.scss create mode 100644 src/utils/test/get-signed-in-request-with-user.ts diff --git a/jest-setup.js b/jest-setup.js index 07092719..bdfa2c3e 100644 --- a/jest-setup.js +++ b/jest-setup.js @@ -23,3 +23,10 @@ console.warn = (message, ...optionalParams) => { actualWarn(message, ...optionalParams); }; + +/* Mock the ResizeObserver */ +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); diff --git a/jest.config.mjs b/jest.config.mjs index 4e9df6d6..6f1d68f2 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -30,6 +30,9 @@ const config = { '/src/app/register/eligibility/page.tsx', '/src/app/register/names/page.tsx', '/src/app/register/other-details/page.tsx', + // pledge to vote iframe is heavily reliant on the ResizeObserver and + // content dimensions + '/src/app/reminders/pledge-to-vote-iframe/pledge-to-vote-iframe.tsx', ], //require 100% code coverage for the tests to pass coverageThreshold: { diff --git a/src/__tests__/unit/app/reminders/completed/page.test.tsx b/src/__tests__/unit/app/reminders/completed/page.test.tsx new file mode 100644 index 00000000..5aab1702 --- /dev/null +++ b/src/__tests__/unit/app/reminders/completed/page.test.tsx @@ -0,0 +1,165 @@ +import CompletedRemindersPage from '@/app/reminders/completed/page'; +import { render, screen, cleanup } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import { UserContext, type UserContextType } from '@/contexts/user-context'; +import { AlertsContextProvider } from '@/contexts/alerts-context'; +import { Builder } from 'builder-pattern'; +import navigation from 'next/navigation'; +import { UserType } from '@/model/enums/user-type'; +import type { User } from '@/model/types/user'; +import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; + +jest.mock('next/navigation', () => ({ + useRouter: jest.fn(), +})); + +describe('CompletedRemindersPage', () => { + let router: AppRouterInstance; + + beforeEach(() => { + router = Builder().push(jest.fn()).build(); + jest.spyOn(navigation, 'useRouter').mockImplementation(() => router); + }); + + afterEach(cleanup); + + it('renders.', () => { + const user = Builder() + .completedActions({ + electionReminders: true, + registerToVote: false, + sharedChallenge: false, + }) + .build(); + + const userContextValue = Builder() + .user(user) + .gotElectionReminders(() => Promise.resolve()) + .build(); + + render( + + + + + , + ); + }); + + it('displays an alert if its searchParams include hasError=true.', () => { + const user = Builder() + .completedActions({ + electionReminders: true, + registerToVote: false, + sharedChallenge: false, + }) + .build(); + + const userContextValue = Builder() + .user(user) + .gotElectionReminders(() => Promise.resolve()) + .build(); + + render( + + + + + , + ); + + const alert = screen.getByRole('alert'); + expect(alert.classList).not.toContain('hidden'); + expect(alert.textContent).toBe( + "Oops! We couldn't award you a badge. Please try again later.", + ); + }); + + it(`does not display an alert if its searchParams do not include + hasError=true.`, () => { + const user = Builder() + .completedActions({ + electionReminders: true, + registerToVote: false, + sharedChallenge: false, + }) + .build(); + + const userContextValue = Builder() + .user(user) + .gotElectionReminders(() => Promise.resolve()) + .build(); + + render( + + + + + , + ); + + const alert = screen.getByRole('alert'); + expect(alert.classList).toContain('hidden'); + }); + + it(`redirects the user to /progress if they click 'Continue' and are a + challenger.`, async () => { + const userContextValue = Builder() + .user( + Builder() + .type(UserType.Challenger) + .completedActions({ + electionReminders: true, + registerToVote: false, + sharedChallenge: false, + }) + .build(), + ) + .gotElectionReminders(() => Promise.resolve()) + .build(); + + const user = userEvent.setup(); + + render( + + + + + , + ); + + await user.click(screen.getByText(/continue/i)); + expect(router.push).toHaveBeenCalledWith('/progress'); + }); + + it(`redirects the user to /actions if they click 'Continue' and are not a + challenger.`, async () => { + const userContextValue = Builder() + .user( + Builder() + .type(UserType.Player) + .completedActions({ + electionReminders: true, + registerToVote: false, + sharedChallenge: false, + }) + .build(), + ) + .gotElectionReminders(() => Promise.resolve()) + .build(); + + const user = userEvent.setup(); + + render( + + + + + , + ); + + await user.click(screen.getByText(/continue/i)); + expect(router.push).toHaveBeenCalledWith('/actions'); + }); +}); diff --git a/src/__tests__/unit/app/reminders/page.test.tsx b/src/__tests__/unit/app/reminders/page.test.tsx new file mode 100644 index 00000000..9e66d3d3 --- /dev/null +++ b/src/__tests__/unit/app/reminders/page.test.tsx @@ -0,0 +1,36 @@ +import RemindersPage from '@/app/reminders/page'; +import { render, cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { UserContext, type UserContextType } from '@/contexts/user-context'; +import { AlertsContextProvider } from '@/contexts/alerts-context'; +import { Builder } from 'builder-pattern'; +import type { User } from '@/model/types/user'; + +jest.mock('next/navigation', () => require('next-router-mock')); + +describe('RemindersPage', () => { + afterEach(cleanup); + + it('renders.', () => { + const user = Builder() + .completedActions({ + electionReminders: false, + registerToVote: false, + sharedChallenge: false, + }) + .build(); + + const userContextValue = Builder() + .user(user) + .gotElectionReminders(() => Promise.resolve()) + .build(); + + render( + + + + + , + ); + }); +}); diff --git a/src/__tests__/unit/components/guards/has-not-completed-action.test.tsx b/src/__tests__/unit/components/guards/has-not-completed-action.test.tsx new file mode 100644 index 00000000..3eb594ec --- /dev/null +++ b/src/__tests__/unit/components/guards/has-not-completed-action.test.tsx @@ -0,0 +1,137 @@ +import { hasNotCompletedAction } from '@/components/guards/has-not-completed-action'; +import { render, screen, cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import navigation from 'next/navigation'; +import { Builder } from 'builder-pattern'; +import { UserContext, type UserContextType } from '@/contexts/user-context'; +import { Actions } from '@/model/enums/actions'; +import type { User } from '@/model/types/user'; +import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; + +jest.mock('next/navigation', () => ({ + useRouter: jest.fn(), +})); + +describe('hasNotCompletedAction', () => { + let router: AppRouterInstance; + + beforeEach(() => { + router = Builder().push(jest.fn()).build(); + jest.spyOn(navigation, 'useRouter').mockImplementation(() => router); + }); + + afterEach(cleanup); + + it('returns a component that can be rendered.', () => { + const user = Builder() + .completedActions({ + electionReminders: false, + registerToVote: false, + sharedChallenge: false, + }) + .build(); + + const userContextValue = Builder().user(user).build(); + + const TestComponent = hasNotCompletedAction( + function () { + return
; + }, + { action: Actions.ElectionReminders, redirectTo: '/' }, + ); + + render( + + + , + ); + expect(screen.queryByTestId('test')).toBeInTheDocument(); + }); + + it('returns a component that can accept props.', () => { + const user = Builder() + .completedActions({ + electionReminders: false, + registerToVote: false, + sharedChallenge: false, + }) + .build(); + + const userContextValue = Builder().user(user).build(); + + interface TestComponentProps { + message: string; + } + + const TestComponent = hasNotCompletedAction( + function ({ message }: TestComponentProps) { + return
{message}
; + }, + { action: Actions.ElectionReminders, redirectTo: '/' }, + ); + + const message = 'test'; + + render( + + + , + ); + expect(screen.queryByText(message)).toBeInTheDocument(); + }); + + it('blocks access to a page if the user has completed the provided action.', () => { + const user = Builder() + .completedActions({ + electionReminders: true, + registerToVote: false, + sharedChallenge: false, + }) + .build(); + + const userContextValue = Builder().user(user).build(); + + const redirectTo = '/'; + + const TestComponent = hasNotCompletedAction( + function () { + return null; + }, + { action: Actions.ElectionReminders, redirectTo }, + ); + + render( + + + , + ); + + expect(router.push).toHaveBeenCalledWith(redirectTo); + }); + + it('allows access to a page if the user has not completed the provided action.', () => { + const user = Builder() + .completedActions({ + electionReminders: true, + registerToVote: true, + sharedChallenge: false, + }) + .build(); + + const userContextValue = Builder().user(user).build(); + + const TestComponent = hasNotCompletedAction( + function () { + return null; + }, + { action: Actions.SharedChallenge, redirectTo: '/' }, + ); + + render( + + + , + ); + expect(router.push).not.toHaveBeenCalled(); + }); +}); diff --git a/src/__tests__/unit/services/server/redirect-if-completed-action/redirect-if-supabase-user-completed-action.test.tsx b/src/__tests__/unit/services/server/redirect-if-completed-action/redirect-if-supabase-user-completed-action.test.tsx new file mode 100644 index 00000000..8e65afdc --- /dev/null +++ b/src/__tests__/unit/services/server/redirect-if-completed-action/redirect-if-supabase-user-completed-action.test.tsx @@ -0,0 +1,98 @@ +import { redirectIfSupabaseUserCompletedAction } from '@/services/server/redirect-if-completed-action/redirect-if-supabase-user-completed-action'; +import { willBeRedirected } from '@/utils/shared/will-be-redirected'; +import { resetAuthAndDatabase } from '@/utils/test/reset-auth-and-database'; +import { NextRequest } from 'next/server'; +import { getSignedInRequestWithUser } from '@/utils/test/get-signed-in-request-with-user'; +import { SupabaseUserRecordBuilder } from '@/utils/test/supabase-user-record-builder'; +import { Actions } from '@/model/enums/actions'; +import { serverContainer } from '@/services/server/container'; +import { SERVER_SERVICE_KEYS } from '@/services/server/keys'; + +describe('redirectIfSupabaseUserCompletedAction', () => { + afterEach(() => { + return resetAuthAndDatabase(); + }); + + it(`returns a response that redirects a user if they have completed the + specified action.`, async () => { + const user = await new SupabaseUserRecordBuilder('user@example.com') + .completedActions({ + electionReminders: true, + registerToVote: false, + sharedChallenge: false, + }) + .build(); + + const request = await getSignedInRequestWithUser( + user, + 'https://challenge.8by8.us/reminders', + { + method: 'GET', + }, + ); + + const userRepo = serverContainer.get(SERVER_SERVICE_KEYS.UserRepository); + + const response = await redirectIfSupabaseUserCompletedAction( + userRepo, + request, + { + action: Actions.ElectionReminders, + redirectTo: '/', + }, + ); + + expect(willBeRedirected(response)).toBe(true); + }); + + it(`returns a response that does not redirect the user if they have not + completed the specified action.`, async () => { + const user = await new SupabaseUserRecordBuilder('user@example.com') + .completedActions({ + electionReminders: false, + registerToVote: false, + sharedChallenge: false, + }) + .build(); + + const request = await getSignedInRequestWithUser( + user, + 'https://challenge.8by8.us/reminders', + { + method: 'GET', + }, + ); + + const userRepo = serverContainer.get(SERVER_SERVICE_KEYS.UserRepository); + + const response = await redirectIfSupabaseUserCompletedAction( + userRepo, + request, + { + action: Actions.ElectionReminders, + redirectTo: '/', + }, + ); + + expect(willBeRedirected(response)).toBe(false); + }); + + it(`returns a response that does not redirect the user if they are signed out.`, async () => { + const request = new NextRequest('https://challenge.8by8.us/reminders', { + method: 'GET', + }); + + const userRepo = serverContainer.get(SERVER_SERVICE_KEYS.UserRepository); + + const response = await redirectIfSupabaseUserCompletedAction( + userRepo, + request, + { + action: Actions.ElectionReminders, + redirectTo: '/', + }, + ); + + expect(willBeRedirected(response)).toBe(false); + }); +}); diff --git a/src/__tests__/unit/utils/test/get-signed-in-request-with-user.test.ts b/src/__tests__/unit/utils/test/get-signed-in-request-with-user.test.ts new file mode 100644 index 00000000..30fd0e7f --- /dev/null +++ b/src/__tests__/unit/utils/test/get-signed-in-request-with-user.test.ts @@ -0,0 +1,109 @@ +import { getSignedInRequestWithUser } from '@/utils/test/get-signed-in-request-with-user'; +import { SupabaseUserRecordBuilder } from '@/utils/test/supabase-user-record-builder'; +import { resetAuthAndDatabase } from '@/utils/test/reset-auth-and-database'; +import * as supabaseSSR from '@supabase/ssr'; +import { AuthError } from '@supabase/supabase-js'; + +jest.mock('@supabase/ssr', () => ({ + __esModule: true, + ...jest.requireActual('@supabase/ssr'), +})); + +describe('getSignedInRequest', () => { + afterEach(() => { + return resetAuthAndDatabase(); + }); + + afterAll(() => { + jest.unmock('@supabase/ssr'); + }); + + it('returns a request with an authentication cookie.', async () => { + const user = await new SupabaseUserRecordBuilder( + 'user@example.com', + ).build(); + + const request = await getSignedInRequestWithUser( + user, + 'https://challenge.8by8.org/progress', + { + method: 'GET', + }, + ); + + const authCookieNamePattern = /sb-[^-]+-auth-token/; + + expect( + request.cookies + .getAll() + .find(cookie => authCookieNamePattern.test(cookie.name)), + ).toBeDefined(); + }); + + it('throws an error if supabase.auth.admin.generateLink() returns an error.', async () => { + const user = await new SupabaseUserRecordBuilder( + 'user@example.com', + ).build(); + + const supabaseSpy = jest + .spyOn(supabaseSSR, 'createServerClient') + .mockImplementationOnce(() => ({ + auth: { + admin: { + generateLink: () => { + return Promise.resolve({ + data: null, + error: new AuthError('Magic link not supported.', 502), + }); + }, + }, + }, + })); + + await expect( + getSignedInRequestWithUser(user, 'https://challenge.8by8.org/progress', { + method: 'GET', + }), + ).rejects.toThrow(new Error('Magic link not supported.')); + + supabaseSpy.mockRestore(); + }); + + it('throws an error if supabase.auth.verifyOtp throws an error.', async () => { + const user = await new SupabaseUserRecordBuilder( + 'user@example.com', + ).build(); + + const supabaseSpy = jest + .spyOn(supabaseSSR, 'createServerClient') + .mockImplementationOnce(() => ({ + auth: { + admin: { + generateLink: () => { + return Promise.resolve({ + data: { + properties: { + email_otp: '123456', + }, + }, + error: null, + }); + }, + }, + verifyOtp: () => { + return Promise.resolve({ + error: new AuthError('Failed to verify otp.'), + }); + }, + }, + })); + + await expect( + getSignedInRequestWithUser(user, 'https://challenge.8by8.org/progress', { + method: 'GET', + }), + ).rejects.toThrow(new Error('Failed to verify otp.')); + + supabaseSpy.mockRestore(); + }); +}); diff --git a/src/app/reminders/completed/completed-reminders/completed-reminders.tsx b/src/app/reminders/completed/completed-reminders/completed-reminders.tsx new file mode 100644 index 00000000..d6f8646f --- /dev/null +++ b/src/app/reminders/completed/completed-reminders/completed-reminders.tsx @@ -0,0 +1,59 @@ +'use client'; +import { isSignedIn } from '@/components/guards/is-signed-in'; +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useContextSafely } from '@/hooks/use-context-safely'; +import { AlertsContext } from '@/contexts/alerts-context'; +import { UserContext } from '@/contexts/user-context'; +import { PageContainer } from '@/components/utils/page-container'; +import { Button } from '@/components/utils/button'; +import { UserType } from '@/model/enums/user-type'; +import styles from './styles.module.scss'; + +interface CompletedRemindersProps { + hasError: boolean; +} + +export const CompletedReminders = isSignedIn(function CompletedReminders({ + hasError, +}: CompletedRemindersProps) { + const { showAlert } = useContextSafely(AlertsContext, 'CompletedReminders'); + const { user } = useContextSafely(UserContext, 'CompletedReminders'); + const router = useRouter(); + + useEffect(() => { + if (hasError) { + showAlert( + "Oops! We couldn't award you a badge. Please try again later.", + 'error', + ); + } + }, [hasError]); + + const onClick = () => { + if (user?.type === UserType.Challenger) { + router.push('/progress'); + } else { + router.push('/actions'); + } + }; + + return ( + +
+

+ Get Election +
+ Alerts +

+

Thank you for joining us!

+

+ Encourage your friends and family to do the same. +

+ +
+
+ ); +}); diff --git a/src/app/reminders/completed/completed-reminders/index.tsx b/src/app/reminders/completed/completed-reminders/index.tsx new file mode 100644 index 00000000..f916e7be --- /dev/null +++ b/src/app/reminders/completed/completed-reminders/index.tsx @@ -0,0 +1 @@ +export { CompletedReminders } from './completed-reminders'; diff --git a/src/app/reminders/completed/completed-reminders/styles.module.scss b/src/app/reminders/completed/completed-reminders/styles.module.scss new file mode 100644 index 00000000..79fac7d7 --- /dev/null +++ b/src/app/reminders/completed/completed-reminders/styles.module.scss @@ -0,0 +1,6 @@ +@use '../../../../styles/partials' as *; + +.page { + margin-left: $spacing-vertical-gutter; + margin-right: $spacing-vertical-gutter; +} diff --git a/src/app/reminders/completed/page.tsx b/src/app/reminders/completed/page.tsx new file mode 100644 index 00000000..344649d9 --- /dev/null +++ b/src/app/reminders/completed/page.tsx @@ -0,0 +1,13 @@ +import { CompletedReminders } from './completed-reminders'; + +interface CompletedRemindersPageProps { + searchParams: { + hasError?: string; + }; +} + +export default function CompletedRemindersPage({ + searchParams, +}: CompletedRemindersPageProps) { + return ; +} diff --git a/src/app/reminders/page.tsx b/src/app/reminders/page.tsx new file mode 100644 index 00000000..9cb42185 --- /dev/null +++ b/src/app/reminders/page.tsx @@ -0,0 +1,25 @@ +'use client'; +import { isSignedIn } from '@/components/guards/is-signed-in'; +import { PageContainer } from '@/components/utils/page-container'; +import { PledgeToVoteIFrame } from './pledge-to-vote-iframe/pledge-to-vote-iframe'; +import { hasNotCompletedAction } from '@/components/guards/has-not-completed-action'; +import { Actions } from '@/model/enums/actions'; +import styles from './styles.module.scss'; + +export default isSignedIn( + hasNotCompletedAction( + function Page() { + return ( + +

+ Get Election +
+ Alerts +

+ +
+ ); + }, + { action: Actions.ElectionReminders, redirectTo: '/reminders/completed' }, + ), +); diff --git a/src/app/reminders/pledge-to-vote-iframe/index.tsx b/src/app/reminders/pledge-to-vote-iframe/index.tsx new file mode 100644 index 00000000..5c317544 --- /dev/null +++ b/src/app/reminders/pledge-to-vote-iframe/index.tsx @@ -0,0 +1 @@ +export { PledgeToVoteIFrame } from './pledge-to-vote-iframe'; diff --git a/src/app/reminders/pledge-to-vote-iframe/pledge-to-vote-iframe.tsx b/src/app/reminders/pledge-to-vote-iframe/pledge-to-vote-iframe.tsx new file mode 100644 index 00000000..bbcbcb0f --- /dev/null +++ b/src/app/reminders/pledge-to-vote-iframe/pledge-to-vote-iframe.tsx @@ -0,0 +1,88 @@ +'use client'; +import { useEffect, useRef, useState } from 'react'; +import { LoadingWheel } from '@/components/utils/loading-wheel'; +import { useContextSafely } from '@/hooks/use-context-safely'; +import { UserContext } from '@/contexts/user-context'; +import styles from './styles.module.scss'; + +const iFrameSourceDoc = ` + + + +`; + +const formHeight = 1000; + +export function PledgeToVoteIFrame() { + const iFrameRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [initialLoadCompleted, setInitialLoadCompleted] = useState(false); + const { gotElectionReminders } = useContextSafely( + UserContext, + 'PledgeToVoteIFrame', + ); + + useEffect(() => { + const observer = new ResizeObserver(() => { + const contentHeight = + iFrameRef.current!.contentWindow!.document.body.scrollHeight; + iFrameRef.current!.height = `${contentHeight}`; + + if (contentHeight >= formHeight) { + setIsLoading(false); + setInitialLoadCompleted(true); + } else if (initialLoadCompleted) { + completeAction(); + } + }); + + async function completeAction() { + setIsLoading(true); + + try { + await gotElectionReminders(); + } catch (e) { + /* + Here, it is important to fetch the completed reminders page from the + server, not via client side navigation (i.e. with the useRouter hook). + + Fetching this page from the server reloads the current document, + so that when the user presses the back button and arrives at this page, + the iframe is reloaded. + */ + location.href = '/reminders/completed?hasError=true'; + } + } + + observer.observe(iFrameRef.current!.contentWindow!.document.body); + + return () => observer.disconnect(); + }, [initialLoadCompleted]); + + return ( + <> +