Skip to content

Commit

Permalink
fix: new application redirect non logged users
Browse files Browse the repository at this point in the history
- fix: include all query params when redirecting to login
- refactor: new reservation login redirect to SSR only
  • Loading branch information
joonatank committed Mar 6, 2025
1 parent 6796dc4 commit 13f2354
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 205 deletions.
53 changes: 20 additions & 33 deletions apps/ui/components/LoginFragment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,40 @@ import React from "react";
import { useTranslation } from "next-i18next";
import { signIn } from "common/src/browserHelpers";
import { useSession } from "@/hooks/auth";
import { SubmitButton } from "@/styles/util";
import { Button } from "hds-react";

type Props = {
apiBaseUrl: string;
text?: string;
componentIfAuthenticated?: React.ReactNode;
componentIfAuthenticated: JSX.Element;
isActionDisabled?: boolean;
actionCallback?: () => void;
returnUrl?: string;
};

function LoginFragment({
export function LoginFragment({
apiBaseUrl,
text,
componentIfAuthenticated,
isActionDisabled,
actionCallback,
returnUrl,
}: Props): JSX.Element | null {
}: Props): JSX.Element {
// TODO pass the isAuthenticated from SSR and remove the hook
const { isAuthenticated } = useSession();
const { t } = useTranslation();

return !isAuthenticated ? (
<div>
<SubmitButton
onClick={() => {
if (actionCallback) {
actionCallback();
}
if (returnUrl) {
signIn(apiBaseUrl, returnUrl);
return;
}
signIn(apiBaseUrl);
}}
aria-label={t("reservationCalendar:loginAndReserve")}
className="login-fragment__button--login"
disabled={isActionDisabled}
>
{t("reservationCalendar:loginAndReserve")}
</SubmitButton>
{text}
</div>
) : (
<div>{componentIfAuthenticated}</div>
const handleClick = () => {
signIn(apiBaseUrl, returnUrl);
};

if (isAuthenticated) {
return componentIfAuthenticated;
}

return (
<Button
onClick={handleClick}
className="login-fragment__button--login"
disabled={isActionDisabled}
>
{t("reservationCalendar:loginAndReserve")}
</Button>
);
}

export default LoginFragment;
34 changes: 23 additions & 11 deletions apps/ui/components/recurring/StartApplicationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { useDisplayError } from "@/hooks/useDisplayError";
import { useReservationUnitList } from "@/hooks";
import { useSearchParams } from "next/navigation";
import { pageSideMargins } from "common/styles/layout";
import { LoginFragment } from "../LoginFragment";
import { getPostLoginUrl } from "@/modules/util";

const BackgroundContainer = styled.div`
position: fixed;
Expand Down Expand Up @@ -60,9 +62,11 @@ type Props = {
applicationRound: {
reservationUnits: NodeList;
};
apiBaseUrl: string;
};

export function StartApplicationBar({
apiBaseUrl,
applicationRound,
}: Props): JSX.Element | null {
const { t } = useTranslation();
Expand Down Expand Up @@ -138,17 +142,25 @@ export function StartApplicationBar({
? t("shoppingCart:deleteSelectionsShort")
: t("shoppingCart:deleteSelections")}
</WhiteButton>
<WhiteButton
id="startApplicationButton"
variant={ButtonVariant.Supplementary}
size={ButtonSize.Small}
onClick={onNext}
disabled={isSaving}
iconEnd={<IconArrowRight />}
colorVariant="light"
>
{t("shoppingCart:nextShort")}
</WhiteButton>
<LoginFragment
returnUrl={getPostLoginUrl()}
// No action callback (we only need the return url)
// actionCallback={() => storeReservationForLogin()}
apiBaseUrl={apiBaseUrl}
componentIfAuthenticated={
<WhiteButton
id="startApplicationButton"
variant={ButtonVariant.Supplementary}
size={ButtonSize.Small}
onClick={onNext}
disabled={isSaving}
iconEnd={<IconArrowRight />}
colorVariant="light"
>
{t("shoppingCart:nextShort")}
</WhiteButton>
}
/>
</InnerContainer>
</BackgroundContainer>
);
Expand Down
14 changes: 14 additions & 0 deletions apps/ui/hooks/useRemoveStoredReservation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ReservationProps } from "@/context/DataContext";
import { useEffect } from "react";
import { useLocalStorage } from "react-use";

/// Local storage usage has been removed
/// leaving this hook for now to cleanup any existing users
export function useRemoveStoredReservation() {
const [storedReservation, , removeStoredReservation] =
useLocalStorage<ReservationProps>("reservation");

useEffect(() => {
if (storedReservation) removeStoredReservation();
}, [storedReservation, removeStoredReservation]);
}
14 changes: 10 additions & 4 deletions apps/ui/modules/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
timeToMinutes,
type LocalizationLanguages,
} from "common/src/helpers";
import { ReadonlyURLSearchParams } from "next/navigation";

export { formatDuration } from "common/src/common/util";
export { fromAPIDate, fromUIDate };
Expand Down Expand Up @@ -120,14 +121,19 @@ export const getAddressAlt = (ru: {
export const isTouchDevice = (): boolean =>
isBrowser && window?.matchMedia("(any-hover: none)").matches;

export function getPostLoginUrl() {
export function getPostLoginUrl(
params: Readonly<URLSearchParams> = new ReadonlyURLSearchParams()
): string | undefined {
if (!isBrowser) {
return undefined;
}
const { origin, pathname, searchParams } = new URL(window.location.href);
const params = new URLSearchParams(searchParams);
params.set("isPostLogin", "true");
return `${origin}${pathname}?${params.toString()}`;
const p = new URLSearchParams(searchParams);
for (const [key, value] of params) {
p.append(key, value);
}
p.set("isPostLogin", "true");
return `${origin}${pathname}?${p.toString()}`;
}

// date format should always be in finnish, but the weekday and time separator should be localized
Expand Down
65 changes: 60 additions & 5 deletions apps/ui/pages/recurring/[id]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import {
ApplicationRoundDocument,
type ApplicationRoundQuery,
type ApplicationRoundQueryVariables,
type CreateApplicationMutationVariables,
type CreateApplicationMutation,
CreateApplicationDocument,
type ApplicationCreateMutationInput,
CurrentUserDocument,
type CurrentUserQuery,
} from "@gql/gql-types";
import {
base64encode,
Expand All @@ -28,14 +34,17 @@ import { useSearchQuery } from "@/hooks/useSearchQuery";
import { SortingComponent } from "@/components/SortingComponent";
import { useSearchParams } from "next/navigation";
import { Breadcrumb } from "@/components/common/Breadcrumb";
import { seasonalPrefix } from "@/modules/urls";
import { getApplicationPath, seasonalPrefix } from "@/modules/urls";
import { getApplicationRoundName } from "@/modules/applicationRound";
import { gql } from "@apollo/client";

function SeasonalSearch({
apiBaseUrl,
applicationRound,
options,
}: Readonly<Pick<NarrowedProps, "applicationRound" | "options">>): JSX.Element {
}: Readonly<
Pick<NarrowedProps, "applicationRound" | "options" | "apiBaseUrl">
>): JSX.Element {
const { t, i18n } = useTranslation();
const searchValues = useSearchParams();

Expand Down Expand Up @@ -102,7 +111,10 @@ function SeasonalSearch({
fetchMore={(cursor) => fetchMore(cursor)}
sortingComponent={<SortingComponent />}
/>
<StartApplicationBar applicationRound={applicationRound} />
<StartApplicationBar
applicationRound={applicationRound}
apiBaseUrl={apiBaseUrl}
/>
</>
);
}
Expand All @@ -111,10 +123,11 @@ type Props = Awaited<ReturnType<typeof getServerSideProps>>["props"];
type NarrowedProps = Exclude<Props, { notFound: boolean }>;

export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { locale, params } = ctx;
const { locale, params, query } = ctx;
const commonProps = getCommonServerSideProps();
const apolloClient = createApolloClient(commonProps.apiBaseUrl, ctx);
const pk = toNumber(ignoreMaybeArray(params?.id));
const isPostLogin = query.isPostLogin === "true";

const notFound = {
notFound: true,
Expand All @@ -141,7 +154,49 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
return notFound;
}

const opts = await getSearchOptions(apolloClient, "seasonal", locale ?? "");
const { data: userData } = await apolloClient.query<CurrentUserQuery>({
query: CurrentUserDocument,
});

if (isPostLogin && userData.currentUser != null) {
const input: ApplicationCreateMutationInput = {
applicationRound: applicationRound.pk ?? 0,
};

// don't catch errors here -> results in 500 page
// we can't display proper error message to user (no page for it)
// and we can't handle them
const mutRes = await apolloClient.mutate<
CreateApplicationMutation,
CreateApplicationMutationVariables
>({
mutation: CreateApplicationDocument,
variables: {
input,
},
});

if (mutRes.data?.createApplication?.pk) {
const { pk } = mutRes.data.createApplication;
const selected = query.selectedReservationUnits ?? [];
const forwardParams = new URLSearchParams();
for (const s of selected) {
forwardParams.append("selectedReservationUnits", s);
}
const url = `${getApplicationPath(pk, "page1")}?${forwardParams.toString()}`;
return {
redirect: {
permanent: false,
destination: url,
},
props: {
notFound: true, // for prop narrowing
},
};
}
}

const opts = await getSearchOptions(apolloClient, "seasonal", locale ?? "fi");
return {
props: {
...commonProps,
Expand Down
32 changes: 8 additions & 24 deletions apps/ui/pages/reservation-unit/[...params].tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, { useCallback, useMemo, useState } from "react";
import styled from "styled-components";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { useRouter } from "next/router";
import { useLocalStorage } from "react-use";
import { Stepper } from "hds-react";
import { FormProvider, useForm } from "react-hook-form";
import type { GetServerSidePropsContext } from "next";
Expand All @@ -21,6 +20,7 @@ import {
} from "@gql/gql-types";
import { type Inputs } from "common/src/reservation-form/types";
import { createApolloClient } from "@/modules/apolloClient";
import { default as NextError } from "next/error";
import {
getReservationPath,
getReservationUnitPath,
Expand All @@ -29,7 +29,6 @@ import {
import { Sanitize } from "common/src/components/Sanitize";
import { isReservationUnitFreeOfCharge } from "@/modules/reservationUnit";
import { getCheckoutUrl } from "@/modules/reservation";
import { ReservationProps } from "@/context/DataContext";
import { ReservationInfoCard } from "@/components/reservation/ReservationInfoCard";
import { Step0 } from "@/components/reservation/Step0";
import { Step1 } from "@/components/reservation/Step1";
Expand All @@ -51,6 +50,7 @@ import { Flex } from "common/styles/util";
import { Breadcrumb } from "@/components/common/Breadcrumb";
import { ReservationPageWrapper } from "@/components/reservations/styles";
import { useDisplayError } from "@/hooks/useDisplayError";
import { useRemoveStoredReservation } from "@/hooks/useRemoveStoredReservation";

const StyledReservationInfoCard = styled(ReservationInfoCard)`
grid-column: 1 / -1;
Expand Down Expand Up @@ -86,18 +86,6 @@ const TitleSection = styled(Flex)`
}
`;

/// We want to get rid of the local storage
/// but there is still code that requires it to be used.
/// Other pages (ex. login + book) get confused if we don't clear it here.
const useRemoveStoredReservation = () => {
const [storedReservation, , removeStoredReservation] =
useLocalStorage<ReservationProps>("reservation");

useEffect(() => {
if (storedReservation) removeStoredReservation();
}, [storedReservation, removeStoredReservation]);
};

// NOTE back / forward buttons (browser) do NOT work properly
// router.beforePopState could be used to handle them but it's super hackish
// the correct solution is to create separate pages (files) for each step (then next-router does this for free)
Expand All @@ -122,15 +110,6 @@ function NewReservation(props: PropsNarrowed): JSX.Element | null {
const reservation = resData?.reservation ?? props.reservation;
const reservationUnit = reservation?.reservationUnits?.find(() => true);

// it should be Created only here (SSR should have redirected)
if (reservation.state !== ReservationStateChoice.Created) {
// eslint-disable-next-line no-console
console.warn(
"should NOT be here when reservation state is ",
reservation.state
);
}

useRemoveStoredReservation();

const [step, setStep] = useState(0);
Expand Down Expand Up @@ -352,6 +331,11 @@ function NewReservation(props: PropsNarrowed): JSX.Element | null {
}
};

// it should be Created only here (SSR should have redirected)
if (reservation.state !== ReservationStateChoice.Created) {
return <NextError statusCode={404} />;
}

return (
<FormProvider {...form}>
<ReservationPageWrapper>
Expand Down
Loading

0 comments on commit 13f2354

Please sign in to comment.