Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 8 additions & 2 deletions src/app/[channel]/(main)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import { Suspense } from "react";
import { Loader } from "@/ui/atoms/Loader";
import { LoginForm } from "@/ui/components/LoginForm";

export default function LoginPage() {
interface LoginPageProps {
params: Promise<{ channel: string }>;
}

export default async function LoginPage({ params }: LoginPageProps) {
const { channel } = await params;

return (
<Suspense fallback={<Loader />}>
<section className="mx-auto max-w-7xl p-8">
<LoginForm />
<LoginForm channel={channel} />
</section>
</Suspense>
);
Expand Down
5 changes: 3 additions & 2 deletions src/app/[channel]/(main)/orders/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { executeGraphQL } from "@/lib/graphql";
import { LoginForm } from "@/ui/components/LoginForm";
import { OrderListItem } from "@/ui/components/OrderListItem";

export default async function OrderPage() {
export default async function OrderPage(props: { params: { channel: string } }) {
const { channel } = props.params;
const { me: user } = await executeGraphQL(CurrentUserOrderListDocument, {
cache: "no-cache",
});

if (!user) {
return <LoginForm />;
return <LoginForm channel={channel} />;
}

const orders = user.orders?.edges || [];
Expand Down
11 changes: 9 additions & 2 deletions src/checkout/hooks/useAlerts/useAlerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,15 @@ function useAlerts(globalScope?: any): any {

const showErrors = useCallback(
(errors: ApiErrors<any>, scope: CheckoutScope = globalScope) =>
getParsedApiErrors(errors).forEach((error) => showDefaultAlert({ ...error, scope } as AlertErrorData)),
[getParsedApiErrors, showDefaultAlert, globalScope],
getParsedApiErrors(errors).forEach(({ message, ...error }) => {
if (message) {
showAlert({ message });
return;
}

showDefaultAlert({ ...error, scope } as AlertErrorData);
}),
[getParsedApiErrors, showAlert, showDefaultAlert, globalScope],
);

const showCustomErrors = useCallback(
Expand Down
1 change: 0 additions & 1 deletion src/checkout/hooks/useAutoSaveAddressForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ export const useAutoSaveAddressForm = ({

const onBlur: BlurHandler = (event) => {
handleBlur(event);
void partialSubmit();
};

return { ...form, handleChange: onChange, handleBlur: onBlur, handleSubmit: partialSubmit };
Expand Down
3 changes: 3 additions & 0 deletions src/checkout/hooks/useCustomerAttach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const useCustomerAttach = () => {
shouldAbort: () =>
!!checkout?.user?.id || !authenticated || fetchingCustomerAttach || fetchingCheckout,
onSubmit: customerAttach,
onSuccess: () => {
void refetch();
},
parse: ({ languageCode, checkoutId }) => ({ languageCode, checkoutId }),
onError: ({ errors }) => {
if (
Expand Down
4 changes: 2 additions & 2 deletions src/checkout/hooks/useGetParsedErrors/useGetParsedErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ export const useGetParsedErrors = <TFormData extends FormDataBase, TErrorCodes e
const { getMessageByErrorCode } = useErrorMessages();

const getParsedApiError = useCallback(
({ code, field }: ApiError<TFormData, TErrorCodes>): ParsedApiError<TFormData> => {
({ code, field, message }: ApiError<TFormData, TErrorCodes>): ParsedApiError<TFormData> => {
const errorCode = camelCase(code) as ErrorCode;

return {
field,
code: errorCode,
message: getMessageByErrorCode(errorCode as GenericErrorCode),
message: message || getMessageByErrorCode(errorCode as GenericErrorCode) || "Something went wrong.",
};
},
[getMessageByErrorCode],
Expand Down
65 changes: 38 additions & 27 deletions src/checkout/sections/DeliveryMethods/DeliveryMethods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ import { useDeliveryMethodsForm } from "@/checkout/sections/DeliveryMethods/useD
import { FormProvider } from "@/checkout/hooks/useForm/FormProvider";
import { useCheckoutUpdateState } from "@/checkout/state/updateStateStore";
import { DeliveryMethodsSkeleton } from "@/checkout/sections/DeliveryMethods/DeliveryMethodsSkeleton";
import { useUser } from "@/checkout/hooks/useUser";

export const DeliveryMethods: FC<CommonSectionProps> = ({ collapsed }) => {
const { checkout } = useCheckout();
const { authenticated } = useUser();
const { shippingMethods, shippingAddress } = checkout;
const form = useDeliveryMethodsForm();
const { updateState } = useCheckoutUpdateState();
Expand All @@ -31,35 +28,49 @@ export const DeliveryMethods: FC<CommonSectionProps> = ({ collapsed }) => {
return null;
}

// Show skeleton while shipping address is being saved and methods are being fetched
if (updateState.checkoutShippingUpdate === "loading") {
return (
<FormProvider form={form}>
<Divider />
<DeliveryMethodsSkeleton />
</FormProvider>
);
}

// Show message when shipping address is required but not filled
if (!shippingAddress) {
return (
<FormProvider form={form}>
<Divider />
<div className="py-4" data-testid="deliveryMethods">
<Title className="mb-2">Delivery methods</Title>
<p>Please fill in shipping address to see available shipping methods</p>
</div>
</FormProvider>
);
}

return (
<FormProvider form={form}>
<Divider />
<div className="py-4" data-testid="deliveryMethods">
<Title className="mb-2">Delivery methods</Title>
{!authenticated && !shippingAddress && (
<p>Please fill in shipping address to see available shipping methods</p>
)}
{authenticated && !shippingAddress && updateState.checkoutShippingUpdate ? (
<DeliveryMethodsSkeleton />
) : (
<SelectBoxGroup label="delivery methods">
{shippingMethods?.map(
({ id, name, price, minimumDeliveryDays: min, maximumDeliveryDays: max }) => (
<SelectBox key={id} name="selectedMethodId" value={id}>
<div className="pointer-events-none flex min-h-12 grow flex-col justify-center">
<div className="flex flex-row items-center justify-between self-stretch">
<p>{name}</p>
<p>{getFormattedMoney(price)}</p>
</div>
<p className="font-xs" color="secondary">
{getSubtitle({ min, max })}
</p>
</div>
</SelectBox>
),
)}
</SelectBoxGroup>
)}
<SelectBoxGroup label="delivery methods">
{shippingMethods?.map(({ id, name, price, minimumDeliveryDays: min, maximumDeliveryDays: max }) => (
<SelectBox key={id} name="selectedMethodId" value={id}>
<div className="pointer-events-none flex min-h-12 grow flex-col justify-center">
<div className="flex flex-row items-center justify-between self-stretch">
<p>{name}</p>
<p>{getFormattedMoney(price)}</p>
</div>
<p className="font-xs" color="secondary">
{getSubtitle({ min, max })}
</p>
</div>
</SelectBox>
))}
</SelectBoxGroup>
</div>
</FormProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export function CheckoutForm() {
<PaymentElement className="payment-element" options={paymentElementOptions} />
<button
className="h-12 items-center rounded-md bg-neutral-900 px-6 py-3 text-base font-medium leading-6 text-white shadow hover:bg-neutral-800 disabled:cursor-not-allowed disabled:opacity-70 hover:disabled:bg-neutral-700 aria-disabled:cursor-not-allowed aria-disabled:opacity-70 hover:aria-disabled:bg-neutral-700"
disabled={isLoading || !stripe || !elements}
aria-disabled={isLoading || !stripe || !elements}
id="submit"
type="submit"
Expand Down
15 changes: 13 additions & 2 deletions src/checkout/sections/SignIn/SignIn.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FC } from "react";
import type { FC, MouseEventHandler } from "react";
import { Button } from "@/checkout/components/Button";
import { PasswordInput } from "@/checkout/components/PasswordInput";
import { TextInput } from "@/checkout/components/TextInput";
Expand Down Expand Up @@ -61,6 +61,16 @@ export const SignIn: FC<SignInProps> = ({
},
});

const handlePasswordResetClick: MouseEventHandler<HTMLButtonElement> = (event) => {
event.preventDefault();

if (isSubmitting) {
return;
}

onPasswordResetRequest();
};

return (
<SignInFormContainer
title="Sign in"
Expand All @@ -87,7 +97,8 @@ export const SignIn: FC<SignInProps> = ({
variant="tertiary"
label={passwordResetSent ? "Resend?" : "Forgot password?"}
className="ml-1 mr-4"
onClick={(e) => (isSubmitting ? e.preventDefault() : onPasswordResetRequest)}
type="button"
onClick={handlePasswordResetClick}
/>
<Button
type="submit"
Expand Down
1 change: 1 addition & 0 deletions src/checkout/sections/SignIn/useSignInForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const useSignInForm = ({ onSuccess, initialEmail }: SignInFormProps) => {
const onSubmit = useFormSubmit<SignInFormData, typeof signIn, AccountErrorCode>({
onSubmit: signIn,
scope: "signIn",
parse: ({ email, password }) => ({ email, password }),
onSuccess,
onError: ({ errors, formHelpers: { setErrors } }) => {
const parsedErrors = errors.reduce((result, error) => {
Expand Down
81 changes: 24 additions & 57 deletions src/ui/components/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,32 @@
import { redirect } from "next/navigation";
import { getServerAuthClient } from "@/app/config";
import { LoginFormClient } from "./LoginFormClient";

export async function LoginForm() {
return (
<div className="mx-auto mt-16 w-full max-w-lg">
<form
className="rounded border p-8 shadow-md"
action={async (formData) => {
"use server";
interface LoginFormProps {
channel: string;
}

export async function LoginForm({ channel }: LoginFormProps) {
const handleLogin = async (formData: FormData) => {
"use server";

const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();

const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
if (!email || !password) {
throw new Error("Email and password are required");
}

if (!email || !password) {
throw new Error("Email and password are required");
}
const { data } = await (await getServerAuthClient()).signIn({ email, password }, { cache: "no-store" });

const { data } = await (
await getServerAuthClient()
).signIn({ email, password }, { cache: "no-store" });
if (data.tokenCreate.errors.length > 0) {
const errorMessage = data.tokenCreate.errors.map((e) => e.message).join(", ");
throw new Error(errorMessage);
}

if (data.tokenCreate.errors.length > 0) {
// setErrors(data.tokenCreate.errors.map((error) => error.message));
// setFormValues(DefaultValues);
}
}}
>
<div className="mb-2">
<label className="sr-only" htmlFor="email">
Email
</label>
<input
required
type="email"
name="email"
placeholder="Email"
className="w-full rounded border bg-neutral-50 px-4 py-2"
/>
</div>
<div className="mb-4">
<label className="sr-only" htmlFor="password">
Password
</label>
<input
required
type="password"
name="password"
placeholder="Password"
autoCapitalize="off"
autoComplete="off"
className="w-full rounded border bg-neutral-50 px-4 py-2"
/>
</div>
// Success - redirect to channel home
redirect(`/${channel}`);
};

<button
className="rounded bg-neutral-800 px-4 py-2 text-neutral-200 hover:bg-neutral-700"
type="submit"
>
Log In
</button>
</form>
<div></div>
</div>
);
return <LoginFormClient action={handleLogin} />;
}
70 changes: 70 additions & 0 deletions src/ui/components/LoginFormClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import { useActionState } from "react";
import { useFormStatus } from "react-dom";

interface LoginFormClientProps {
action: (formData: FormData) => Promise<void>;
}

function SubmitButton() {
const { pending } = useFormStatus();

return (
<button
className="rounded bg-neutral-800 px-4 py-2 text-neutral-200 hover:bg-neutral-700 disabled:opacity-50"
type="submit"
disabled={pending}
>
{pending ? "Logging in..." : "Log In"}
</button>
);
}

export function LoginFormClient({ action }: LoginFormClientProps) {
const [state, formAction] = useActionState(async (_previousState: string | null, formData: FormData) => {
try {
await action(formData);
return null;
} catch (error) {
return error instanceof Error ? error.message : "An error occurred";
}
}, null);

return (
<div className="mx-auto mt-16 w-full max-w-lg">
{state && <div className="mb-4 rounded border border-red-300 bg-red-50 p-3 text-red-700">{state}</div>}

<form className="rounded border p-8 shadow-md" action={formAction}>
<div className="mb-2">
<label className="sr-only" htmlFor="email">
Email
</label>
<input
required
type="email"
name="email"
placeholder="Email"
className="w-full rounded border bg-neutral-50 px-4 py-2"
/>
</div>
<div className="mb-4">
<label className="sr-only" htmlFor="password">
Password
</label>
<input
required
type="password"
name="password"
placeholder="Password"
autoCapitalize="off"
autoComplete="off"
className="w-full rounded border bg-neutral-50 px-4 py-2"
/>
</div>

<SubmitButton />
</form>
</div>
);
}
Loading