@shipfox/client-auth 6.0.3 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/dist/components/password-login-form.d.ts +7 -0
  4. package/dist/components/password-login-form.d.ts.map +1 -0
  5. package/dist/components/password-login-form.js +164 -0
  6. package/dist/components/password-login-form.js.map +1 -0
  7. package/dist/continuation.d.ts +2 -1
  8. package/dist/continuation.d.ts.map +1 -1
  9. package/dist/continuation.js +2 -1
  10. package/dist/continuation.js.map +1 -1
  11. package/dist/pages/invitation-context.js +1 -1
  12. package/dist/pages/invitation-context.js.map +1 -1
  13. package/dist/pages/login-page.d.ts.map +1 -1
  14. package/dist/pages/login-page.js +12 -168
  15. package/dist/pages/login-page.js.map +1 -1
  16. package/dist/redirect-context.d.ts.map +1 -0
  17. package/dist/{components/redirect-context.js → redirect-context.js} +1 -1
  18. package/dist/redirect-context.js.map +1 -0
  19. package/dist/tsconfig.test.tsbuildinfo +1 -1
  20. package/package.json +7 -3
  21. package/src/components/password-login-form.test.tsx +34 -0
  22. package/src/components/password-login-form.tsx +149 -0
  23. package/src/continuation.test.ts +2 -0
  24. package/src/continuation.ts +5 -1
  25. package/src/pages/invitation-context.ts +1 -1
  26. package/src/pages/login-page.tsx +4 -141
  27. package/src/{components/redirect-context.test.ts → redirect-context.test.ts} +13 -2
  28. package/src/{components/redirect-context.ts → redirect-context.ts} +1 -1
  29. package/tsconfig.build.tsbuildinfo +1 -1
  30. package/dist/components/redirect-context.d.ts.map +0 -1
  31. package/dist/components/redirect-context.js.map +0 -1
  32. /package/dist/{components/redirect-context.d.ts → redirect-context.d.ts} +0 -0
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 41 files with swc (151.72ms)
2
+ Successfully compiled: 42 files with swc (198.18ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @shipfox/client-auth
2
2
 
3
+ ## 8.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 25f5f42: Export the password login form from the continuation entrypoint for downstream route composition.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [289d686]
12
+ - @shipfox/client-shell@8.0.0
13
+ - @shipfox/client-invitations@8.0.0
14
+
15
+ ## 7.0.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 61309db: Adds a Node-safe redirect-context entrypoint for shared authentication continuation parsing.
20
+
3
21
  ## 6.0.3
4
22
 
5
23
  ### Patch Changes
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface PasswordLoginFormProps {
3
+ children?: ReactNode;
4
+ invitationEmail?: string | undefined;
5
+ }
6
+ export declare function PasswordLoginForm({ children, invitationEmail }?: PasswordLoginFormProps): import("react").JSX.Element;
7
+ //# sourceMappingURL=password-login-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"password-login-form.d.ts","sourceRoot":"","sources":["../../src/components/password-login-form.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAC,KAAK,SAAS,EAA8B,MAAM,OAAO,CAAC;AAKlE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,wBAAgB,iBAAiB,CAAC,EAAC,QAAQ,EAAE,eAAe,EAAC,GAAE,sBAA2B,+BAmIzF"}
@@ -0,0 +1,164 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { loginBodySchema } from '@shipfox/api-auth-dto';
3
+ import { Button } from '@shipfox/react-ui/button';
4
+ import { Callout } from '@shipfox/react-ui/callout';
5
+ import { FormField, FormFieldInput, fieldError } from '@shipfox/react-ui/form-field';
6
+ import { Icon } from '@shipfox/react-ui/icon';
7
+ import { useForm } from '@tanstack/react-form';
8
+ import { useAtom } from 'jotai';
9
+ import { useEffect, useRef, useState } from 'react';
10
+ import { useLoginAuth } from '#hooks/api/login-auth.js';
11
+ import { loginErrorToFormError } from '#pages/form-errors.js';
12
+ import { authFormDraftAtom, initialAuthFormDraft } from '#state/auth.js';
13
+ export function PasswordLoginForm({ children, invitationEmail } = {}) {
14
+ const login = useLoginAuth();
15
+ const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);
16
+ const [formError, setFormError] = useState();
17
+ const draftRef = useRef(authFormDraft);
18
+ draftRef.current = authFormDraft;
19
+ // Set just before clearing the draft on success so the unmount cleanup
20
+ // below does not repersist the just-submitted credentials.
21
+ const skipDraftPersistRef = useRef(false);
22
+ const form = useForm({
23
+ defaultValues: {
24
+ email: authFormDraft.email,
25
+ password: authFormDraft.password
26
+ },
27
+ onSubmit: async ({ value })=>{
28
+ setFormError(undefined);
29
+ try {
30
+ await login.mutateAsync(value);
31
+ skipDraftPersistRef.current = true;
32
+ setAuthFormDraft(initialAuthFormDraft);
33
+ } catch (error) {
34
+ const mapped = loginErrorToFormError(error);
35
+ if (mapped.kind === 'field') {
36
+ form.setFieldMeta(mapped.field, (prev)=>({
37
+ ...prev,
38
+ errorMap: {
39
+ ...prev.errorMap,
40
+ onServer: mapped.message
41
+ }
42
+ }));
43
+ } else {
44
+ setFormError(mapped.message);
45
+ }
46
+ }
47
+ }
48
+ });
49
+ useEffect(()=>{
50
+ if (invitationEmail && form.state.values.email !== invitationEmail) {
51
+ form.setFieldValue('email', invitationEmail);
52
+ setAuthFormDraft((current)=>({
53
+ ...current,
54
+ email: invitationEmail
55
+ }));
56
+ }
57
+ }, [
58
+ form,
59
+ invitationEmail,
60
+ setAuthFormDraft
61
+ ]);
62
+ // Sync TanStack Form values back into the Jotai draft on unmount so a
63
+ // navigation to /signup or /reset preserves what the user typed. Skipped
64
+ // after a successful login because we just intentionally cleared the draft.
65
+ useEffect(()=>{
66
+ return ()=>{
67
+ if (skipDraftPersistRef.current) return;
68
+ const { email, password } = form.state.values;
69
+ if (email !== draftRef.current.email || password !== draftRef.current.password) {
70
+ setAuthFormDraft({
71
+ email,
72
+ password
73
+ });
74
+ }
75
+ };
76
+ }, [
77
+ form,
78
+ setAuthFormDraft
79
+ ]);
80
+ function persistDraft() {
81
+ const { email, password } = form.state.values;
82
+ setAuthFormDraft({
83
+ email,
84
+ password
85
+ });
86
+ }
87
+ return /*#__PURE__*/ _jsxs("form", {
88
+ className: "flex flex-col gap-18",
89
+ noValidate: true,
90
+ onSubmit: (event)=>{
91
+ event.preventDefault();
92
+ event.stopPropagation();
93
+ void form.handleSubmit();
94
+ },
95
+ children: [
96
+ formError ? /*#__PURE__*/ _jsx(Callout, {
97
+ role: "alert",
98
+ type: "error",
99
+ children: formError
100
+ }) : null,
101
+ /*#__PURE__*/ _jsx(form.Field, {
102
+ name: "email",
103
+ validators: {
104
+ onBlur: loginBodySchema.shape.email,
105
+ onSubmit: loginBodySchema.shape.email
106
+ },
107
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
108
+ label: "Email",
109
+ id: "email",
110
+ error: fieldError(field),
111
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
112
+ autoComplete: "email",
113
+ name: "email",
114
+ type: "email",
115
+ value: field.state.value,
116
+ onChange: (event)=>field.handleChange(event.target.value),
117
+ onBlur: ()=>{
118
+ field.handleBlur();
119
+ persistDraft();
120
+ },
121
+ readOnly: Boolean(invitationEmail),
122
+ iconRight: invitationEmail ? /*#__PURE__*/ _jsx(Icon, {
123
+ "aria-hidden": "true",
124
+ className: "size-16 text-foreground-neutral-disabled",
125
+ name: "lockLine"
126
+ }) : undefined
127
+ })
128
+ })
129
+ }),
130
+ /*#__PURE__*/ _jsx(form.Field, {
131
+ name: "password",
132
+ validators: {
133
+ onBlur: loginBodySchema.shape.password,
134
+ onSubmit: loginBodySchema.shape.password
135
+ },
136
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
137
+ label: "Password",
138
+ id: "password",
139
+ error: fieldError(field),
140
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
141
+ autoComplete: "current-password",
142
+ name: "password",
143
+ type: "password",
144
+ value: field.state.value,
145
+ onChange: (event)=>field.handleChange(event.target.value),
146
+ onBlur: ()=>{
147
+ field.handleBlur();
148
+ persistDraft();
149
+ }
150
+ })
151
+ })
152
+ }),
153
+ children,
154
+ /*#__PURE__*/ _jsx(Button, {
155
+ className: "w-full",
156
+ isLoading: login.isPending,
157
+ type: "submit",
158
+ children: login.isPending ? 'Logging in...' : 'Log in'
159
+ })
160
+ ]
161
+ });
162
+ }
163
+
164
+ //# sourceMappingURL=password-login-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/password-login-form.tsx"],"sourcesContent":["import {loginBodySchema} from '@shipfox/api-auth-dto';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {useForm} from '@tanstack/react-form';\nimport {useAtom} from 'jotai';\nimport {type ReactNode, useEffect, useRef, useState} from 'react';\nimport {useLoginAuth} from '#hooks/api/login-auth.js';\nimport {loginErrorToFormError} from '#pages/form-errors.js';\nimport {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';\n\nexport interface PasswordLoginFormProps {\n children?: ReactNode;\n invitationEmail?: string | undefined;\n}\n\nexport function PasswordLoginForm({children, invitationEmail}: PasswordLoginFormProps = {}) {\n const login = useLoginAuth();\n const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);\n const [formError, setFormError] = useState<string | undefined>();\n const draftRef = useRef(authFormDraft);\n draftRef.current = authFormDraft;\n // Set just before clearing the draft on success so the unmount cleanup\n // below does not repersist the just-submitted credentials.\n const skipDraftPersistRef = useRef(false);\n\n const form = useForm({\n defaultValues: {email: authFormDraft.email, password: authFormDraft.password},\n onSubmit: async ({value}) => {\n setFormError(undefined);\n try {\n await login.mutateAsync(value);\n skipDraftPersistRef.current = true;\n setAuthFormDraft(initialAuthFormDraft);\n } catch (error) {\n const mapped = loginErrorToFormError(error);\n if (mapped.kind === 'field') {\n form.setFieldMeta(mapped.field, (prev) => ({\n ...prev,\n errorMap: {...prev.errorMap, onServer: mapped.message},\n }));\n } else {\n setFormError(mapped.message);\n }\n }\n },\n });\n\n useEffect(() => {\n if (invitationEmail && form.state.values.email !== invitationEmail) {\n form.setFieldValue('email', invitationEmail);\n setAuthFormDraft((current) => ({...current, email: invitationEmail}));\n }\n }, [form, invitationEmail, setAuthFormDraft]);\n\n // Sync TanStack Form values back into the Jotai draft on unmount so a\n // navigation to /signup or /reset preserves what the user typed. Skipped\n // after a successful login because we just intentionally cleared the draft.\n useEffect(() => {\n return () => {\n if (skipDraftPersistRef.current) return;\n const {email, password} = form.state.values;\n if (email !== draftRef.current.email || password !== draftRef.current.password) {\n setAuthFormDraft({email, password});\n }\n };\n }, [form, setAuthFormDraft]);\n\n function persistDraft() {\n const {email, password} = form.state.values;\n setAuthFormDraft({email, password});\n }\n\n return (\n <form\n className=\"flex flex-col gap-18\"\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void form.handleSubmit();\n }}\n >\n {formError ? (\n <Callout role=\"alert\" type=\"error\">\n {formError}\n </Callout>\n ) : null}\n <form.Field\n name=\"email\"\n validators={{onBlur: loginBodySchema.shape.email, onSubmit: loginBodySchema.shape.email}}\n >\n {(field) => (\n <FormField label=\"Email\" id=\"email\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"email\"\n name=\"email\"\n type=\"email\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n readOnly={Boolean(invitationEmail)}\n iconRight={\n invitationEmail ? (\n <Icon\n aria-hidden=\"true\"\n className=\"size-16 text-foreground-neutral-disabled\"\n name=\"lockLine\"\n />\n ) : undefined\n }\n />\n </FormField>\n )}\n </form.Field>\n <form.Field\n name=\"password\"\n validators={{\n onBlur: loginBodySchema.shape.password,\n onSubmit: loginBodySchema.shape.password,\n }}\n >\n {(field) => (\n <FormField label=\"Password\" id=\"password\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"current-password\"\n name=\"password\"\n type=\"password\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n />\n </FormField>\n )}\n </form.Field>\n {children}\n <Button className=\"w-full\" isLoading={login.isPending} type=\"submit\">\n {login.isPending ? 'Logging in...' : 'Log in'}\n </Button>\n </form>\n );\n}\n"],"names":["loginBodySchema","Button","Callout","FormField","FormFieldInput","fieldError","Icon","useForm","useAtom","useEffect","useRef","useState","useLoginAuth","loginErrorToFormError","authFormDraftAtom","initialAuthFormDraft","PasswordLoginForm","children","invitationEmail","login","authFormDraft","setAuthFormDraft","formError","setFormError","draftRef","current","skipDraftPersistRef","form","defaultValues","email","password","onSubmit","value","undefined","mutateAsync","error","mapped","kind","setFieldMeta","field","prev","errorMap","onServer","message","state","values","setFieldValue","persistDraft","className","noValidate","event","preventDefault","stopPropagation","handleSubmit","role","type","Field","name","validators","onBlur","shape","label","id","autoComplete","onChange","handleChange","target","handleBlur","readOnly","Boolean","iconRight","aria-hidden","isLoading","isPending"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  export { AuthActions, AuthShell, type AuthShellProps } from '@shipfox/client-shell/runtime';
2
2
  export { EmailCodeVerification, type EmailCodeVerificationProps, } from './components/email-code-verification.js';
3
- export { parseRedirectContext, type RedirectContext } from './components/redirect-context.js';
3
+ export { PasswordLoginForm, type PasswordLoginFormProps, } from './components/password-login-form.js';
4
+ export { parseRedirectContext, type RedirectContext } from './redirect-context.js';
4
5
  //# sourceMappingURL=continuation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"continuation.d.ts","sourceRoot":"","sources":["../src/continuation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,+BAA+B,CAAC;AAC1F,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAC,oBAAoB,EAAE,KAAK,eAAe,EAAC,MAAM,kCAAkC,CAAC"}
1
+ {"version":3,"file":"continuation.d.ts","sourceRoot":"","sources":["../src/continuation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,+BAA+B,CAAC;AAC1F,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,iBAAiB,EACjB,KAAK,sBAAsB,GAC5B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAC,oBAAoB,EAAE,KAAK,eAAe,EAAC,MAAM,uBAAuB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  export { AuthActions, AuthShell } from '@shipfox/client-shell/runtime';
2
2
  export { EmailCodeVerification } from './components/email-code-verification.js';
3
- export { parseRedirectContext } from './components/redirect-context.js';
3
+ export { PasswordLoginForm } from './components/password-login-form.js';
4
+ export { parseRedirectContext } from './redirect-context.js';
4
5
 
5
6
  //# sourceMappingURL=continuation.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/continuation.ts"],"sourcesContent":["export {AuthActions, AuthShell, type AuthShellProps} from '@shipfox/client-shell/runtime';\nexport {\n EmailCodeVerification,\n type EmailCodeVerificationProps,\n} from './components/email-code-verification.js';\nexport {parseRedirectContext, type RedirectContext} from './components/redirect-context.js';\n"],"names":["AuthActions","AuthShell","EmailCodeVerification","parseRedirectContext"],"mappings":"AAAA,SAAQA,WAAW,EAAEC,SAAS,QAA4B,gCAAgC;AAC1F,SACEC,qBAAqB,QAEhB,0CAA0C;AACjD,SAAQC,oBAAoB,QAA6B,mCAAmC"}
1
+ {"version":3,"sources":["../src/continuation.ts"],"sourcesContent":["export {AuthActions, AuthShell, type AuthShellProps} from '@shipfox/client-shell/runtime';\nexport {\n EmailCodeVerification,\n type EmailCodeVerificationProps,\n} from './components/email-code-verification.js';\nexport {\n PasswordLoginForm,\n type PasswordLoginFormProps,\n} from './components/password-login-form.js';\nexport {parseRedirectContext, type RedirectContext} from './redirect-context.js';\n"],"names":["AuthActions","AuthShell","EmailCodeVerification","PasswordLoginForm","parseRedirectContext"],"mappings":"AAAA,SAAQA,WAAW,EAAEC,SAAS,QAA4B,gCAAgC;AAC1F,SACEC,qBAAqB,QAEhB,0CAA0C;AACjD,SACEC,iBAAiB,QAEZ,sCAAsC;AAC7C,SAAQC,oBAAoB,QAA6B,wBAAwB"}
@@ -1,5 +1,5 @@
1
1
  import { pendingInvitation, usePreviewInvitation } from '@shipfox/client-invitations';
2
- import { parseRedirectContext } from '#/components/redirect-context.js';
2
+ import { parseRedirectContext } from '#/redirect-context.js';
3
3
  export function extractInvitationToken(redirect) {
4
4
  return parseRedirectContext(redirect).invitationToken;
5
5
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pages/invitation-context.ts"],"sourcesContent":["import {\n type InvitationPreview,\n pendingInvitation,\n usePreviewInvitation,\n} from '@shipfox/client-invitations';\nimport {parseRedirectContext} from '#/components/redirect-context.js';\n\nexport function extractInvitationToken(redirect: unknown): string | undefined {\n return parseRedirectContext(redirect).invitationToken;\n}\n\nexport function useInvitationContext(token: string | undefined) {\n return usePreviewInvitation(token);\n}\n\nexport {type InvitationPreview, pendingInvitation};\n"],"names":["pendingInvitation","usePreviewInvitation","parseRedirectContext","extractInvitationToken","redirect","invitationToken","useInvitationContext","token"],"mappings":"AAAA,SAEEA,iBAAiB,EACjBC,oBAAoB,QACf,8BAA8B;AACrC,SAAQC,oBAAoB,QAAO,mCAAmC;AAEtE,OAAO,SAASC,uBAAuBC,QAAiB;IACtD,OAAOF,qBAAqBE,UAAUC,eAAe;AACvD;AAEA,OAAO,SAASC,qBAAqBC,KAAyB;IAC5D,OAAON,qBAAqBM;AAC9B;AAEA,SAAgCP,iBAAiB,GAAE"}
1
+ {"version":3,"sources":["../../src/pages/invitation-context.ts"],"sourcesContent":["import {\n type InvitationPreview,\n pendingInvitation,\n usePreviewInvitation,\n} from '@shipfox/client-invitations';\nimport {parseRedirectContext} from '#/redirect-context.js';\n\nexport function extractInvitationToken(redirect: unknown): string | undefined {\n return parseRedirectContext(redirect).invitationToken;\n}\n\nexport function useInvitationContext(token: string | undefined) {\n return usePreviewInvitation(token);\n}\n\nexport {type InvitationPreview, pendingInvitation};\n"],"names":["pendingInvitation","usePreviewInvitation","parseRedirectContext","extractInvitationToken","redirect","invitationToken","useInvitationContext","token"],"mappings":"AAAA,SAEEA,iBAAiB,EACjBC,oBAAoB,QACf,8BAA8B;AACrC,SAAQC,oBAAoB,QAAO,wBAAwB;AAE3D,OAAO,SAASC,uBAAuBC,QAAiB;IACtD,OAAOF,qBAAqBE,UAAUC,eAAe;AACvD;AAEA,OAAO,SAASC,qBAAqBC,KAAyB;IAC5D,OAAON,qBAAqBM;AAC9B;AAEA,SAAgCP,iBAAiB,GAAE"}
@@ -1 +1 @@
1
- {"version":3,"file":"login-page.d.ts","sourceRoot":"","sources":["../../src/pages/login-page.tsx"],"names":[],"mappings":"AAqBA,wBAAgB,SAAS,gCAmKxB"}
1
+ {"version":3,"file":"login-page.d.ts","sourceRoot":"","sources":["../../src/pages/login-page.tsx"],"names":[],"mappings":"AAYA,wBAAgB,SAAS,gCAmCxB"}
@@ -1,190 +1,34 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { loginBodySchema } from '@shipfox/api-auth-dto';
3
2
  import { AuthShell, useRouteSearch } from '@shipfox/client-shell/runtime';
4
- import { Button, ButtonLink } from '@shipfox/react-ui/button';
5
- import { Callout } from '@shipfox/react-ui/callout';
6
- import { FormField, FormFieldInput, fieldError } from '@shipfox/react-ui/form-field';
7
- import { Icon } from '@shipfox/react-ui/icon';
3
+ import { ButtonLink } from '@shipfox/react-ui/button';
8
4
  import { Text } from '@shipfox/react-ui/typography';
9
- import { useForm } from '@tanstack/react-form';
10
5
  import { Link } from '@tanstack/react-router';
11
- import { useAtom } from 'jotai';
12
- import { useEffect, useRef, useState } from 'react';
13
- import { useLoginAuth } from '#hooks/api/login-auth.js';
14
- import { authFormDraftAtom, initialAuthFormDraft } from '#state/auth.js';
6
+ import { PasswordLoginForm } from '#components/password-login-form.js';
15
7
  import { validateRedirectSearch } from '../routes/inputs.js';
16
- import { loginErrorToFormError } from './form-errors.js';
17
8
  import { extractInvitationToken, pendingInvitation, useInvitationContext } from './invitation-context.js';
18
9
  export function LoginPage() {
19
- const login = useLoginAuth();
20
10
  const search = useRouteSearch(validateRedirectSearch);
21
11
  const invitationToken = extractInvitationToken(search.redirect);
22
12
  const invitationPreview = useInvitationContext(invitationToken);
23
13
  const invitationPending = pendingInvitation(invitationPreview.data);
24
- const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);
25
- const [formError, setFormError] = useState();
26
- const draftRef = useRef(authFormDraft);
27
- draftRef.current = authFormDraft;
28
- // Set just before clearing the draft on success so the unmount cleanup
29
- // below does not repersist the just-submitted credentials.
30
- const skipDraftPersistRef = useRef(false);
31
- const form = useForm({
32
- defaultValues: {
33
- email: authFormDraft.email,
34
- password: authFormDraft.password
35
- },
36
- onSubmit: async ({ value })=>{
37
- setFormError(undefined);
38
- try {
39
- await login.mutateAsync(value);
40
- skipDraftPersistRef.current = true;
41
- setAuthFormDraft(initialAuthFormDraft);
42
- } catch (error) {
43
- const mapped = loginErrorToFormError(error);
44
- if (mapped.kind === 'field') {
45
- form.setFieldMeta(mapped.field, (prev)=>({
46
- ...prev,
47
- errorMap: {
48
- ...prev.errorMap,
49
- onServer: mapped.message
50
- }
51
- }));
52
- } else {
53
- setFormError(mapped.message);
54
- }
55
- }
56
- }
57
- });
58
- // Lock the email field when arriving from an invitation link so the user
59
- // can't log in as a different account than the one the invitation targets.
60
- useEffect(()=>{
61
- if (invitationPending && form.state.values.email !== invitationPending.email) {
62
- form.setFieldValue('email', invitationPending.email);
63
- setAuthFormDraft((current)=>({
64
- ...current,
65
- email: invitationPending.email
66
- }));
67
- }
68
- }, [
69
- invitationPending,
70
- form,
71
- setAuthFormDraft
72
- ]);
73
- // Sync TanStack Form values back into the Jotai draft on unmount so a
74
- // navigation to /signup or /reset preserves what the user typed. Skipped
75
- // after a successful login because we just intentionally cleared the draft.
76
- useEffect(()=>{
77
- return ()=>{
78
- if (skipDraftPersistRef.current) return;
79
- const { email, password } = form.state.values;
80
- if (email !== draftRef.current.email || password !== draftRef.current.password) {
81
- setAuthFormDraft({
82
- email,
83
- password
84
- });
85
- }
86
- };
87
- }, [
88
- form,
89
- setAuthFormDraft
90
- ]);
91
- const isInvitationEmailLocked = Boolean(invitationPending);
92
14
  const invitationRedirect = invitationToken ? `/invitations/accept?token=${encodeURIComponent(invitationToken)}` : undefined;
93
15
  const headerTitle = invitationPending ? `Join ${invitationPending.workspaceName}` : 'Connect to Shipfox';
94
16
  const headerDescription = invitationPending ? 'Log in to accept your invitation.' : 'Log in to access Shipfox.';
95
- function persistDraft() {
96
- const { email, password } = form.state.values;
97
- setAuthFormDraft({
98
- email,
99
- password
100
- });
101
- }
102
17
  return /*#__PURE__*/ _jsxs(AuthShell, {
103
18
  title: headerTitle,
104
19
  description: headerDescription,
105
20
  children: [
106
- /*#__PURE__*/ _jsxs("form", {
107
- className: "flex flex-col gap-18",
108
- noValidate: true,
109
- onSubmit: (event)=>{
110
- event.preventDefault();
111
- event.stopPropagation();
112
- void form.handleSubmit();
113
- },
114
- children: [
115
- formError ? /*#__PURE__*/ _jsx(Callout, {
116
- role: "alert",
117
- type: "error",
118
- children: formError
119
- }) : null,
120
- /*#__PURE__*/ _jsx(form.Field, {
121
- name: "email",
122
- validators: {
123
- onBlur: loginBodySchema.shape.email,
124
- onSubmit: loginBodySchema.shape.email
125
- },
126
- children: (field)=>/*#__PURE__*/ _jsx(FormField, {
127
- label: "Email",
128
- id: "email",
129
- error: fieldError(field),
130
- children: /*#__PURE__*/ _jsx(FormFieldInput, {
131
- autoComplete: "email",
132
- name: "email",
133
- type: "email",
134
- value: field.state.value,
135
- onChange: (event)=>field.handleChange(event.target.value),
136
- onBlur: ()=>{
137
- field.handleBlur();
138
- persistDraft();
139
- },
140
- readOnly: isInvitationEmailLocked,
141
- iconRight: isInvitationEmailLocked ? /*#__PURE__*/ _jsx(Icon, {
142
- "aria-hidden": "true",
143
- className: "size-16 text-foreground-neutral-disabled",
144
- name: "lockLine"
145
- }) : undefined
146
- })
147
- })
148
- }),
149
- /*#__PURE__*/ _jsx(form.Field, {
150
- name: "password",
151
- validators: {
152
- onBlur: loginBodySchema.shape.password,
153
- onSubmit: loginBodySchema.shape.password
154
- },
155
- children: (field)=>/*#__PURE__*/ _jsx(FormField, {
156
- label: "Password",
157
- id: "password",
158
- error: fieldError(field),
159
- children: /*#__PURE__*/ _jsx(FormFieldInput, {
160
- autoComplete: "current-password",
161
- name: "password",
162
- type: "password",
163
- value: field.state.value,
164
- onChange: (event)=>field.handleChange(event.target.value),
165
- onBlur: ()=>{
166
- field.handleBlur();
167
- persistDraft();
168
- }
169
- })
170
- })
171
- }),
172
- /*#__PURE__*/ _jsx(ButtonLink, {
173
- asChild: true,
174
- variant: "subtle",
175
- className: "-mt-8 self-end",
176
- children: /*#__PURE__*/ _jsx(Link, {
177
- to: "/auth/reset",
178
- children: "Forgot password?"
179
- })
180
- }),
181
- /*#__PURE__*/ _jsx(Button, {
182
- className: "w-full",
183
- isLoading: login.isPending,
184
- type: "submit",
185
- children: login.isPending ? 'Logging in...' : 'Log in'
21
+ /*#__PURE__*/ _jsx(PasswordLoginForm, {
22
+ invitationEmail: invitationPending?.email,
23
+ children: /*#__PURE__*/ _jsx(ButtonLink, {
24
+ asChild: true,
25
+ variant: "subtle",
26
+ className: "-mt-8 self-end",
27
+ children: /*#__PURE__*/ _jsx(Link, {
28
+ to: "/auth/reset",
29
+ children: "Forgot password?"
186
30
  })
187
- ]
31
+ })
188
32
  }),
189
33
  /*#__PURE__*/ _jsxs(Text, {
190
34
  size: "sm",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pages/login-page.tsx"],"sourcesContent":["import {loginBodySchema} from '@shipfox/api-auth-dto';\nimport {AuthShell, useRouteSearch} from '@shipfox/client-shell/runtime';\nimport {Button, ButtonLink} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {Text} from '@shipfox/react-ui/typography';\nimport {useForm} from '@tanstack/react-form';\nimport {Link} from '@tanstack/react-router';\nimport {useAtom} from 'jotai';\nimport {useEffect, useRef, useState} from 'react';\nimport {useLoginAuth} from '#hooks/api/login-auth.js';\nimport {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';\nimport {validateRedirectSearch} from '../routes/inputs.js';\nimport {loginErrorToFormError} from './form-errors.js';\nimport {\n extractInvitationToken,\n pendingInvitation,\n useInvitationContext,\n} from './invitation-context.js';\n\nexport function LoginPage() {\n const login = useLoginAuth();\n const search = useRouteSearch(validateRedirectSearch);\n const invitationToken = extractInvitationToken(search.redirect);\n const invitationPreview = useInvitationContext(invitationToken);\n const invitationPending = pendingInvitation(invitationPreview.data);\n const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);\n const [formError, setFormError] = useState<string | undefined>();\n const draftRef = useRef(authFormDraft);\n draftRef.current = authFormDraft;\n // Set just before clearing the draft on success so the unmount cleanup\n // below does not repersist the just-submitted credentials.\n const skipDraftPersistRef = useRef(false);\n\n const form = useForm({\n defaultValues: {email: authFormDraft.email, password: authFormDraft.password},\n onSubmit: async ({value}) => {\n setFormError(undefined);\n try {\n await login.mutateAsync(value);\n skipDraftPersistRef.current = true;\n setAuthFormDraft(initialAuthFormDraft);\n } catch (error) {\n const mapped = loginErrorToFormError(error);\n if (mapped.kind === 'field') {\n form.setFieldMeta(mapped.field, (prev) => ({\n ...prev,\n errorMap: {...prev.errorMap, onServer: mapped.message},\n }));\n } else {\n setFormError(mapped.message);\n }\n }\n },\n });\n\n // Lock the email field when arriving from an invitation link so the user\n // can't log in as a different account than the one the invitation targets.\n useEffect(() => {\n if (invitationPending && form.state.values.email !== invitationPending.email) {\n form.setFieldValue('email', invitationPending.email);\n setAuthFormDraft((current) => ({...current, email: invitationPending.email}));\n }\n }, [invitationPending, form, setAuthFormDraft]);\n\n // Sync TanStack Form values back into the Jotai draft on unmount so a\n // navigation to /signup or /reset preserves what the user typed. Skipped\n // after a successful login because we just intentionally cleared the draft.\n useEffect(() => {\n return () => {\n if (skipDraftPersistRef.current) return;\n const {email, password} = form.state.values;\n if (email !== draftRef.current.email || password !== draftRef.current.password) {\n setAuthFormDraft({email, password});\n }\n };\n }, [form, setAuthFormDraft]);\n\n const isInvitationEmailLocked = Boolean(invitationPending);\n const invitationRedirect = invitationToken\n ? `/invitations/accept?token=${encodeURIComponent(invitationToken)}`\n : undefined;\n const headerTitle = invitationPending\n ? `Join ${invitationPending.workspaceName}`\n : 'Connect to Shipfox';\n const headerDescription = invitationPending\n ? 'Log in to accept your invitation.'\n : 'Log in to access Shipfox.';\n\n function persistDraft() {\n const {email, password} = form.state.values;\n setAuthFormDraft({email, password});\n }\n\n return (\n <AuthShell title={headerTitle} description={headerDescription}>\n <form\n className=\"flex flex-col gap-18\"\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void form.handleSubmit();\n }}\n >\n {formError ? (\n <Callout role=\"alert\" type=\"error\">\n {formError}\n </Callout>\n ) : null}\n <form.Field\n name=\"email\"\n validators={{onBlur: loginBodySchema.shape.email, onSubmit: loginBodySchema.shape.email}}\n >\n {(field) => (\n <FormField label=\"Email\" id=\"email\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"email\"\n name=\"email\"\n type=\"email\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n readOnly={isInvitationEmailLocked}\n iconRight={\n isInvitationEmailLocked ? (\n <Icon\n aria-hidden=\"true\"\n className=\"size-16 text-foreground-neutral-disabled\"\n name=\"lockLine\"\n />\n ) : undefined\n }\n />\n </FormField>\n )}\n </form.Field>\n <form.Field\n name=\"password\"\n validators={{\n onBlur: loginBodySchema.shape.password,\n onSubmit: loginBodySchema.shape.password,\n }}\n >\n {(field) => (\n <FormField label=\"Password\" id=\"password\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"current-password\"\n name=\"password\"\n type=\"password\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n />\n </FormField>\n )}\n </form.Field>\n <ButtonLink asChild variant=\"subtle\" className=\"-mt-8 self-end\">\n <Link to=\"/auth/reset\">Forgot password?</Link>\n </ButtonLink>\n <Button className=\"w-full\" isLoading={login.isPending} type=\"submit\">\n {login.isPending ? 'Logging in...' : 'Log in'}\n </Button>\n </form>\n <Text size=\"sm\" className=\"text-center text-foreground-neutral-subtle\">\n New to Shipfox?{' '}\n <ButtonLink asChild variant=\"interactive\" underline>\n <Link\n to=\"/auth/signup\"\n search={invitationRedirect ? {redirect: invitationRedirect} : undefined}\n >\n Create an account\n </Link>\n </ButtonLink>\n </Text>\n </AuthShell>\n );\n}\n"],"names":["loginBodySchema","AuthShell","useRouteSearch","Button","ButtonLink","Callout","FormField","FormFieldInput","fieldError","Icon","Text","useForm","Link","useAtom","useEffect","useRef","useState","useLoginAuth","authFormDraftAtom","initialAuthFormDraft","validateRedirectSearch","loginErrorToFormError","extractInvitationToken","pendingInvitation","useInvitationContext","LoginPage","login","search","invitationToken","redirect","invitationPreview","invitationPending","data","authFormDraft","setAuthFormDraft","formError","setFormError","draftRef","current","skipDraftPersistRef","form","defaultValues","email","password","onSubmit","value","undefined","mutateAsync","error","mapped","kind","setFieldMeta","field","prev","errorMap","onServer","message","state","values","setFieldValue","isInvitationEmailLocked","Boolean","invitationRedirect","encodeURIComponent","headerTitle","workspaceName","headerDescription","persistDraft","title","description","className","noValidate","event","preventDefault","stopPropagation","handleSubmit","role","type","Field","name","validators","onBlur","shape","label","id","autoComplete","onChange","handleChange","target","handleBlur","readOnly","iconRight","aria-hidden","asChild","variant","to","isLoading","isPending","size","underline"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/pages/login-page.tsx"],"sourcesContent":["import {AuthShell, useRouteSearch} from '@shipfox/client-shell/runtime';\nimport {ButtonLink} from '@shipfox/react-ui/button';\nimport {Text} from '@shipfox/react-ui/typography';\nimport {Link} from '@tanstack/react-router';\nimport {PasswordLoginForm} from '#components/password-login-form.js';\nimport {validateRedirectSearch} from '../routes/inputs.js';\nimport {\n extractInvitationToken,\n pendingInvitation,\n useInvitationContext,\n} from './invitation-context.js';\n\nexport function LoginPage() {\n const search = useRouteSearch(validateRedirectSearch);\n const invitationToken = extractInvitationToken(search.redirect);\n const invitationPreview = useInvitationContext(invitationToken);\n const invitationPending = pendingInvitation(invitationPreview.data);\n const invitationRedirect = invitationToken\n ? `/invitations/accept?token=${encodeURIComponent(invitationToken)}`\n : undefined;\n const headerTitle = invitationPending\n ? `Join ${invitationPending.workspaceName}`\n : 'Connect to Shipfox';\n const headerDescription = invitationPending\n ? 'Log in to accept your invitation.'\n : 'Log in to access Shipfox.';\n\n return (\n <AuthShell title={headerTitle} description={headerDescription}>\n <PasswordLoginForm invitationEmail={invitationPending?.email}>\n <ButtonLink asChild variant=\"subtle\" className=\"-mt-8 self-end\">\n <Link to=\"/auth/reset\">Forgot password?</Link>\n </ButtonLink>\n </PasswordLoginForm>\n <Text size=\"sm\" className=\"text-center text-foreground-neutral-subtle\">\n New to Shipfox?{' '}\n <ButtonLink asChild variant=\"interactive\" underline>\n <Link\n to=\"/auth/signup\"\n search={invitationRedirect ? {redirect: invitationRedirect} : undefined}\n >\n Create an account\n </Link>\n </ButtonLink>\n </Text>\n </AuthShell>\n );\n}\n"],"names":["AuthShell","useRouteSearch","ButtonLink","Text","Link","PasswordLoginForm","validateRedirectSearch","extractInvitationToken","pendingInvitation","useInvitationContext","LoginPage","search","invitationToken","redirect","invitationPreview","invitationPending","data","invitationRedirect","encodeURIComponent","undefined","headerTitle","workspaceName","headerDescription","title","description","invitationEmail","email","asChild","variant","className","to","size","underline"],"mappings":";AAAA,SAAQA,SAAS,EAAEC,cAAc,QAAO,gCAAgC;AACxE,SAAQC,UAAU,QAAO,2BAA2B;AACpD,SAAQC,IAAI,QAAO,+BAA+B;AAClD,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,iBAAiB,QAAO,qCAAqC;AACrE,SAAQC,sBAAsB,QAAO,sBAAsB;AAC3D,SACEC,sBAAsB,EACtBC,iBAAiB,EACjBC,oBAAoB,QACf,0BAA0B;AAEjC,OAAO,SAASC;IACd,MAAMC,SAASV,eAAeK;IAC9B,MAAMM,kBAAkBL,uBAAuBI,OAAOE,QAAQ;IAC9D,MAAMC,oBAAoBL,qBAAqBG;IAC/C,MAAMG,oBAAoBP,kBAAkBM,kBAAkBE,IAAI;IAClE,MAAMC,qBAAqBL,kBACvB,CAAC,0BAA0B,EAAEM,mBAAmBN,kBAAkB,GAClEO;IACJ,MAAMC,cAAcL,oBAChB,CAAC,KAAK,EAAEA,kBAAkBM,aAAa,EAAE,GACzC;IACJ,MAAMC,oBAAoBP,oBACtB,sCACA;IAEJ,qBACE,MAACf;QAAUuB,OAAOH;QAAaI,aAAaF;;0BAC1C,KAACjB;gBAAkBoB,iBAAiBV,mBAAmBW;0BACrD,cAAA,KAACxB;oBAAWyB,OAAO;oBAACC,SAAQ;oBAASC,WAAU;8BAC7C,cAAA,KAACzB;wBAAK0B,IAAG;kCAAc;;;;0BAG3B,MAAC3B;gBAAK4B,MAAK;gBAAKF,WAAU;;oBAA6C;oBACrD;kCAChB,KAAC3B;wBAAWyB,OAAO;wBAACC,SAAQ;wBAAcI,SAAS;kCACjD,cAAA,KAAC5B;4BACC0B,IAAG;4BACHnB,QAAQM,qBAAqB;gCAACJ,UAAUI;4BAAkB,IAAIE;sCAC/D;;;;;;;AAOX"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect-context.d.ts","sourceRoot":"","sources":["../src/redirect-context.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAQpE"}
@@ -1,4 +1,4 @@
1
- import { isAuthPath, resolveRedirectPath } from './redirect-target.js';
1
+ import { isAuthPath, resolveRedirectPath } from './components/redirect-target.js';
2
2
  const INVITATION_ACCEPT_PATH = '/invitations/accept';
3
3
  /**
4
4
  * Separates a safe post-authentication destination from an invitation token.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/redirect-context.ts"],"sourcesContent":["import {isAuthPath, resolveRedirectPath} from './components/redirect-target.js';\n\nconst INVITATION_ACCEPT_PATH = '/invitations/accept';\n\nexport interface RedirectContext {\n invitationToken?: string;\n returnTo?: string;\n}\n\n/**\n * Separates a safe post-authentication destination from an invitation token.\n * The token never remains in `returnTo`, so callers can keep it in their\n * short-lived invitation flow instead of forwarding it through generic redirects.\n */\nexport function parseRedirectContext(value: unknown): RedirectContext {\n const resolved = resolveRedirectPath(value);\n if (!resolved || isAuthPath(resolved.pathname)) return {};\n\n if (resolved.pathname !== INVITATION_ACCEPT_PATH) return {returnTo: resolved.redirect};\n\n const invitationToken = resolved.target.searchParams.get('token');\n return invitationToken ? {invitationToken} : {};\n}\n"],"names":["isAuthPath","resolveRedirectPath","INVITATION_ACCEPT_PATH","parseRedirectContext","value","resolved","pathname","returnTo","redirect","invitationToken","target","searchParams","get"],"mappings":"AAAA,SAAQA,UAAU,EAAEC,mBAAmB,QAAO,kCAAkC;AAEhF,MAAMC,yBAAyB;AAO/B;;;;CAIC,GACD,OAAO,SAASC,qBAAqBC,KAAc;IACjD,MAAMC,WAAWJ,oBAAoBG;IACrC,IAAI,CAACC,YAAYL,WAAWK,SAASC,QAAQ,GAAG,OAAO,CAAC;IAExD,IAAID,SAASC,QAAQ,KAAKJ,wBAAwB,OAAO;QAACK,UAAUF,SAASG,QAAQ;IAAA;IAErF,MAAMC,kBAAkBJ,SAASK,MAAM,CAACC,YAAY,CAACC,GAAG,CAAC;IACzD,OAAOH,kBAAkB;QAACA;IAAe,IAAI,CAAC;AAChD"}