1- import { ArrowLeftIcon , EnvelopeIcon } from "@heroicons/react/20/solid" ;
1+ import { ArrowLeftIcon } from "@heroicons/react/20/solid" ;
22import { InboxArrowDownIcon } from "@heroicons/react/24/solid" ;
33import {
44 redirect ,
55 type ActionFunctionArgs ,
66 type LoaderFunctionArgs ,
77 type MetaFunction ,
88} from "@remix-run/node" ;
9- import { Form , useNavigation } from "@remix-run/react" ;
9+ import { Form } from "@remix-run/react" ;
1010import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
1111import { z } from "zod" ;
1212import { LoginPageLayout } from "~/components/LoginPageLayout" ;
@@ -15,11 +15,7 @@ import { Fieldset } from "~/components/primitives/Fieldset";
1515import { FormButtons } from "~/components/primitives/FormButtons" ;
1616import { FormError } from "~/components/primitives/FormError" ;
1717import { Header1 } from "~/components/primitives/Headers" ;
18- import { Input } from "~/components/primitives/Input" ;
19- import { InputGroup } from "~/components/primitives/InputGroup" ;
2018import { Paragraph } from "~/components/primitives/Paragraph" ;
21- import { Spinner } from "~/components/primitives/Spinner" ;
22- import { TextLink } from "~/components/primitives/TextLink" ;
2319import { authenticator } from "~/services/auth.server" ;
2420import { commitSession , getUserSession } from "~/services/sessionStorage.server" ;
2521import {
@@ -66,13 +62,15 @@ export async function loader({ request }: LoaderFunctionArgs) {
6662 const session = await getUserSession ( request ) ;
6763
6864 // The email form now lives inline on /login; this route is only the
69- // "magic link sent" confirmation. Any visit without a pending magic link
70- // forwards to /login — keeping the inlined form the single source of truth
71- // and avoiding an orphaned duplicate page. This also catches the expired-
72- // link callback in routes/magic.tsx (which redirects here on error): with
73- // no pending link it lands on /login, where the error surfaces via the
74- // shared auth:error handling. Checked before reading auth:error so a flashed
75- // error isn't consumed here before /login can display it.
65+ // "magic link sent" confirmation. A visit without a pending magic link
66+ // forwards to /login — keeping the inlined form the single source of truth,
67+ // avoiding an orphaned duplicate page, and letting /login surface any flashed
68+ // auth:error (e.g. an invalid-email submit that never sent a link). The guard
69+ // runs before reading auth:error so that error isn't consumed here before
70+ // /login can show it. An expired/invalid link click (routes/magic.tsx) is
71+ // different: the email-link strategy only clears the magic-link key on a
72+ // successful verify, so the key is still set and the request lands here — the
73+ // confirmation renders the flashed error as magicLinkError below.
7674 const url = new URL ( request . url ) ;
7775 const sanitized = sanitizeRedirectPath ( url . searchParams . get ( "redirectTo" ) ) ;
7876 // The submitted address is carried on the success redirect so the
@@ -82,7 +80,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
8280 if ( ! session . has ( "triggerdotdev:magiclink" ) ) {
8381 // Throw (not return) so the redirect doesn't widen the loader's return
8482 // type — otherwise useTypedLoaderData sees TypedResponse<never> in the
85- // union and the component can't read magicLinkSent/ magicLinkError.
83+ // union and the component can't read magicLinkError/email .
8684 throw redirect (
8785 sanitized === "/" ? "/login" : `/login?redirectTo=${ encodeURIComponent ( sanitized ) } `
8886 ) ;
@@ -111,7 +109,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
111109
112110 return typedjson (
113111 {
114- magicLinkSent : session . has ( "triggerdotdev:magiclink" ) ,
115112 magicLinkError,
116113 email,
117114 } ,
@@ -238,127 +235,53 @@ export async function action({ request }: ActionFunctionArgs) {
238235}
239236
240237export default function LoginMagicLinkPage ( ) {
241- const { magicLinkSent, magicLinkError, email } = useTypedLoaderData < typeof loader > ( ) ;
242- const navigate = useNavigation ( ) ;
243-
244- const isLoading =
245- ( navigate . state === "loading" || navigate . state === "submitting" ) &&
246- navigate . formAction !== undefined &&
247- navigate . formData ?. get ( "action" ) === "send" ;
238+ const { magicLinkError, email } = useTypedLoaderData < typeof loader > ( ) ;
248239
249240 return (
250241 < LoginPageLayout >
251242 < Form method = "post" >
252243 < div className = "flex flex-col items-center justify-center" >
253- { magicLinkSent ? (
254- < >
255- < Header1 className = "pb-6 text-center text-xl font-normal leading-7 md:text-xl lg:text-2xl" >
256- We've sent you a magic link!
257- </ Header1 >
258- < Fieldset className = "flex w-full flex-col items-center gap-y-2" >
259- < InboxArrowDownIcon className = "mb-4 h-12 w-12 text-indigo-500" />
260- < Paragraph className = "mb-6 text-center [text-wrap:balance]" >
261- { email ? (
262- < >
263- We emailed a magic link to < span className = "text-text-bright" > { email } </ span > { " " }
264- to log you in to your account.
265- </ >
266- ) : (
267- "We emailed you a magic link to log you in to your account."
268- ) }
269- </ Paragraph >
270- < FormButtons
271- cancelButton = {
272- < Button
273- type = "submit"
274- name = "action"
275- value = "reset"
276- variant = "minimal/small"
277- LeadingIcon = { ArrowLeftIcon }
278- leadingIconClassName = "text-text-dimmed group-hover:text-text-bright transition"
279- data-action = "re-enter email"
280- >
281- Re-enter email
282- </ Button >
283- }
284- confirmButton = {
285- < LinkButton
286- to = "/login"
287- variant = "minimal/small"
288- data-action = "log in using another option"
289- >
290- Log in using another option
291- </ LinkButton >
292- }
293- />
294- </ Fieldset >
295- </ >
296- ) : (
297- < >
298- < Header1 className = "pb-4 font-semibold sm:text-2xl md:text-3xl lg:text-4xl" >
299- Welcome
300- </ Header1 >
301- < Paragraph variant = "base" className = "mb-6 text-center" >
302- Create an account or login using email
303- </ Paragraph >
304- < Fieldset className = "flex w-full flex-col items-center gap-y-2" >
305- < InputGroup >
306- < Input
307- type = "email"
308- name = "email"
309- spellCheck = { false }
310- placeholder = "Email Address"
311- variant = "large"
312- required
313- autoFocus
314- />
315- </ InputGroup >
316-
244+ < Header1 className = "pb-6 text-center text-xl font-normal leading-7 md:text-xl lg:text-2xl" >
245+ We've sent you a magic link!
246+ </ Header1 >
247+ < Fieldset className = "flex w-full flex-col items-center gap-y-2" >
248+ < InboxArrowDownIcon className = "mb-4 h-12 w-12 text-indigo-500" />
249+ < Paragraph className = "mb-6 text-center [text-wrap:balance]" >
250+ { email ? (
251+ < >
252+ We emailed a magic link to < span className = "text-text-bright" > { email } </ span > to
253+ log you in to your account.
254+ </ >
255+ ) : (
256+ "We emailed you a magic link to log you in to your account."
257+ ) }
258+ </ Paragraph >
259+ { magicLinkError && < FormError > { magicLinkError } </ FormError > }
260+ < FormButtons
261+ cancelButton = {
317262 < Button
318- name = "action"
319- value = "send"
320263 type = "submit"
321- variant = "primary/large"
322- disabled = { isLoading }
323- fullWidth
324- data-action = "send a magic link"
264+ name = "action"
265+ value = "reset"
266+ variant = "minimal/small"
267+ LeadingIcon = { ArrowLeftIcon }
268+ leadingIconClassName = "text-text-dimmed group-hover:text-text-bright transition"
269+ data-action = "re-enter email"
325270 >
326- { isLoading ? (
327- < Spinner className = "mr-2 size-5" color = "white" />
328- ) : (
329- < EnvelopeIcon className = "mr-2 size-5 text-text-bright" />
330- ) }
331- { isLoading ? (
332- < span className = "text-text-bright" > Sending…</ span >
333- ) : (
334- < span className = "text-text-bright" > Send a magic link</ span >
335- ) }
271+ Re-enter email
336272 </ Button >
337- { magicLinkError && < FormError > { magicLinkError } </ FormError > }
338- </ Fieldset >
339- < Paragraph variant = "extra-small" className = "mb-4 mt-6 text-center" >
340- By signing up you agree to our{ " " }
341- < TextLink href = "https://trigger.dev/legal" target = "_blank" >
342- terms
343- </ TextLink >
344- { " " } and{ " " }
345- < TextLink href = "https://trigger.dev/legal/privacy" target = "_blank" >
346- privacy
347- </ TextLink >
348- { " " } policy.
349- </ Paragraph >
350-
351- < LinkButton
352- to = "/login"
353- variant = { "minimal/small" }
354- LeadingIcon = { ArrowLeftIcon }
355- leadingIconClassName = "text-text-dimmed group-hover:text-text-bright transition"
356- data-action = "all login options"
357- >
358- All login options
359- </ LinkButton >
360- </ >
361- ) }
273+ }
274+ confirmButton = {
275+ < LinkButton
276+ to = "/login"
277+ variant = "minimal/small"
278+ data-action = "log in using another option"
279+ >
280+ Log in using another option
281+ </ LinkButton >
282+ }
283+ />
284+ </ Fieldset >
362285 </ div >
363286 </ Form >
364287 </ LoginPageLayout >
0 commit comments