@byline/admin 3.20.4 → 4.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.
@@ -110,6 +110,8 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
110
110
  id: htmlId
111
111
  });
112
112
  case 'integer':
113
+ case 'float':
114
+ case 'decimal':
113
115
  return /*#__PURE__*/ jsx(NumericalField, {
114
116
  field: hideLabel ? {
115
117
  ...field,
@@ -6,13 +6,15 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import type { CounterField, DecimalField, FieldComponentSlots, FloatField, IntegerField } from '@byline/core';
9
+ type NumericalValue = string | number | null;
9
10
  export declare const NumericalField: ({ field, value, defaultValue, onChange, id, path, components, }: {
10
11
  field: IntegerField | FloatField | DecimalField | CounterField;
11
12
  value?: string | number | null;
12
13
  defaultValue?: string | number | null;
13
- onChange?: (value: string) => void;
14
+ onChange?: (value: NumericalValue) => void;
14
15
  id?: string;
15
16
  path?: string;
16
17
  /** Optional UI component slot overrides from the admin config. */
17
18
  components?: FieldComponentSlots;
18
19
  }) => import("react").JSX.Element;
20
+ export {};
@@ -1,13 +1,39 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { normalizeNumericValue } from "@byline/core";
2
4
  import { Input } from "@byline/ui/react";
3
5
  import { useFieldError, useFieldValue } from "../../forms/form-context.js";
6
+ const COMPLETE_NUMERIC_RE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
4
7
  const NumericalField = ({ field, value, defaultValue, onChange, id, path, components })=>{
5
8
  const fieldPath = path ?? field.name;
6
9
  const fieldError = useFieldError(fieldPath);
7
10
  const fieldValue = useFieldValue(fieldPath);
8
- const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
11
+ const incomingValue = void 0 !== value ? value : void 0 !== fieldValue ? fieldValue : defaultValue ?? null;
9
12
  const htmlId = id ?? fieldPath;
10
- const displayValue = null == incomingValue ? '' : String(incomingValue);
13
+ const canonicalDisplay = null == incomingValue ? '' : String(incomingValue);
14
+ const [displayValue, setDisplayValue] = useState(canonicalDisplay);
15
+ const [isEditing, setIsEditing] = useState(false);
16
+ useEffect(()=>{
17
+ if (!isEditing) setDisplayValue(canonicalDisplay);
18
+ }, [
19
+ canonicalDisplay,
20
+ isEditing
21
+ ]);
22
+ const canonicalize = (nextValue)=>{
23
+ if (null == nextValue || 'string' == typeof nextValue && '' === nextValue.trim()) return null;
24
+ if ('counter' === field.type) return;
25
+ if ('string' == typeof nextValue && !COMPLETE_NUMERIC_RE.test(nextValue.trim())) return;
26
+ try {
27
+ return normalizeNumericValue(field.type, nextValue, fieldPath);
28
+ } catch {
29
+ return;
30
+ }
31
+ };
32
+ const commit = (nextValue)=>{
33
+ const canonical = canonicalize(nextValue);
34
+ if (void 0 !== canonical) onChange?.(canonical);
35
+ return canonical;
36
+ };
11
37
  const slots = components;
12
38
  const CustomLabel = slots?.Label;
13
39
  const CustomHelpText = slots?.HelpText;
@@ -35,12 +61,14 @@ const NumericalField = ({ field, value, defaultValue, onChange, id, path, compon
35
61
  const renderInput = ()=>{
36
62
  if (CustomField) return /*#__PURE__*/ jsx(CustomField, {
37
63
  ...slotBaseProps,
38
- onChange: (v)=>onChange?.(v),
64
+ onChange: (nextValue)=>commit(nextValue),
39
65
  defaultValue: defaultValue,
40
66
  placeholder: field.placeholder
41
67
  });
42
68
  return /*#__PURE__*/ jsx(Input, {
43
- type: "number",
69
+ type: "text",
70
+ inputMode: 'integer' === field.type || 'counter' === field.type ? 'numeric' : 'decimal',
71
+ step: 'integer' === field.type || 'counter' === field.type ? 1 : 'any',
44
72
  id: htmlId,
45
73
  name: field.name,
46
74
  label: suppressInputLabel ? void 0 : field.label,
@@ -48,7 +76,16 @@ const NumericalField = ({ field, value, defaultValue, onChange, id, path, compon
48
76
  readOnly: field.readOnly,
49
77
  helpText: suppressInputHelpText ? void 0 : field.helpText,
50
78
  value: displayValue,
51
- onChange: (e)=>onChange?.(e.target.value),
79
+ onFocus: ()=>setIsEditing(true),
80
+ onChange: (e)=>{
81
+ setDisplayValue(e.target.value);
82
+ commit(e.target.value);
83
+ },
84
+ onBlur: ()=>{
85
+ setIsEditing(false);
86
+ const canonical = commit(displayValue);
87
+ setDisplayValue(void 0 === canonical ? canonicalDisplay : canonical?.toString() ?? '');
88
+ },
52
89
  error: null != fieldError,
53
90
  errorText: fieldError
54
91
  });
@@ -1,5 +1,7 @@
1
- interface SignInFormProps {
2
- /** Destination after successful sign-in. Defaults to `/admin`. */
1
+ export interface SignInFormProps {
2
+ /** Host-validated root-relative destination after successful sign-in. */
3
+ redirectTo?: string;
4
+ /** @deprecated Use `redirectTo`. */
3
5
  callbackUrl?: string;
4
6
  /**
5
7
  * Optional plain "Home" link rendered on the left of the action row.
@@ -8,5 +10,4 @@ interface SignInFormProps {
8
10
  */
9
11
  homeUrl?: string;
10
12
  }
11
- export declare function SignInForm({ callbackUrl, homeUrl }: SignInFormProps): import("react").JSX.Element;
12
- export {};
13
+ export declare function SignInForm({ redirectTo, callbackUrl, homeUrl }: SignInFormProps): import("react").JSX.Element;
@@ -1,12 +1,14 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
+ import { getClientConfig } from "@byline/core";
4
5
  import { useTranslation } from "@byline/i18n/react";
5
6
  import { Alert, Button, Card, Input, LoaderEllipsis } from "@byline/ui/react";
6
7
  import classnames from "classnames";
7
8
  import { useBylineAdminServices } from "../../../services/admin-services-context.js";
9
+ import { normalizeRootRelativeRedirect, resolveSignInFormRedirect } from "../safe-redirect.js";
8
10
  import sign_in_form_module from "./sign-in-form.module.js";
9
- function SignInForm({ callbackUrl, homeUrl }) {
11
+ function SignInForm({ redirectTo, callbackUrl, homeUrl }) {
10
12
  const { adminSignIn } = useBylineAdminServices();
11
13
  const { t } = useTranslation('byline-admin');
12
14
  const [email, setEmail] = useState('');
@@ -17,6 +19,7 @@ function SignInForm({ callbackUrl, homeUrl }) {
17
19
  event.preventDefault();
18
20
  if (pending) return;
19
21
  if (0 === email.trim().length || 0 === password.length) return void setError(t('auth.signIn.errors.empty'));
22
+ const destination = resolveSignInFormRedirect(redirectTo, callbackUrl, getClientConfig().routes.admin);
20
23
  setPending(true);
21
24
  setError(null);
22
25
  try {
@@ -26,13 +29,13 @@ function SignInForm({ callbackUrl, homeUrl }) {
26
29
  password
27
30
  }
28
31
  });
29
- const target = callbackUrl && callbackUrl.length > 0 ? callbackUrl : '/admin';
30
- window.location.assign(target);
31
32
  } catch (err) {
32
33
  console.warn('sign-in failed', err);
33
34
  setError(t('auth.signIn.errors.invalidCredentials'));
34
35
  setPending(false);
36
+ return;
35
37
  }
38
+ window.location.assign(normalizeRootRelativeRedirect(destination) ?? '/');
36
39
  }
37
40
  return /*#__PURE__*/ jsxs(Card, {
38
41
  className: classnames('byline-sign-in-card', sign_in_form_module.card),
@@ -0,0 +1,4 @@
1
+ /** Accept only canonical, unencoded, root-relative same-origin redirect paths. */
2
+ export declare function normalizeRootRelativeRedirect(value: string): string | undefined;
3
+ /** Resolve the new prop first, then the deprecated prop, then a trusted fallback. */
4
+ export declare function resolveSignInFormRedirect(redirectTo: string | undefined, callbackUrl: string | undefined, fallback: string | (() => string)): string;
@@ -0,0 +1,20 @@
1
+ import { normalizeRootRelativeRedirect } from "@byline/core";
2
+ function safe_redirect_normalizeRootRelativeRedirect(value) {
3
+ const normalized = normalizeRootRelativeRedirect(value);
4
+ if (!normalized) return;
5
+ let url;
6
+ try {
7
+ url = new URL(normalized, 'https://byline.invalid');
8
+ } catch {
9
+ return;
10
+ }
11
+ if ('https://byline.invalid' !== url.origin) return;
12
+ return `${url.pathname}${url.search}${url.hash}`;
13
+ }
14
+ function resolveSignInFormRedirect(redirectTo, callbackUrl, fallback) {
15
+ const requested = (redirectTo ? safe_redirect_normalizeRootRelativeRedirect(redirectTo) : void 0) ?? (callbackUrl ? safe_redirect_normalizeRootRelativeRedirect(callbackUrl) : void 0);
16
+ if (requested) return requested;
17
+ const defaultPath = 'function' == typeof fallback ? fallback() : fallback;
18
+ return safe_redirect_normalizeRootRelativeRedirect(defaultPath) ?? '/';
19
+ }
20
+ export { resolveSignInFormRedirect, safe_redirect_normalizeRootRelativeRedirect as normalizeRootRelativeRedirect };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/admin",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "3.20.4",
5
+ "version": "4.0.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -155,10 +155,10 @@
155
155
  "react-diff-viewer-continued": "^4.2.2",
156
156
  "uuid": "^14.0.1",
157
157
  "zod": "^4.4.3",
158
- "@byline/auth": "3.20.4",
159
- "@byline/core": "3.20.4",
160
- "@byline/ui": "3.20.4",
161
- "@byline/i18n": "3.20.4"
158
+ "@byline/auth": "4.0.0",
159
+ "@byline/core": "4.0.0",
160
+ "@byline/ui": "4.0.0",
161
+ "@byline/i18n": "4.0.0"
162
162
  },
163
163
  "peerDependencies": {
164
164
  "react": "^19.0.0",
@@ -195,6 +195,8 @@ export const FieldRenderer = ({
195
195
  />
196
196
  )
197
197
  case 'integer':
198
+ case 'float':
199
+ case 'decimal':
198
200
  return (
199
201
  <NumericalField
200
202
  field={hideLabel ? { ...field, label: undefined } : field}
@@ -6,6 +6,8 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
 
9
+ import { useEffect, useState } from 'react'
10
+
9
11
  import type {
10
12
  CounterField,
11
13
  DecimalField,
@@ -14,10 +16,15 @@ import type {
14
16
  FloatField,
15
17
  IntegerField,
16
18
  } from '@byline/core'
19
+ import { normalizeNumericValue } from '@byline/core'
17
20
  import { Input } from '@byline/ui/react'
18
21
 
19
22
  import { useFieldError, useFieldValue } from '../../forms/form-context'
20
23
 
24
+ type NumericalValue = string | number | null
25
+
26
+ const COMPLETE_NUMERIC_RE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/
27
+
21
28
  export const NumericalField = ({
22
29
  field,
23
30
  value,
@@ -30,7 +37,7 @@ export const NumericalField = ({
30
37
  field: IntegerField | FloatField | DecimalField | CounterField
31
38
  value?: string | number | null
32
39
  defaultValue?: string | number | null
33
- onChange?: (value: string) => void
40
+ onChange?: (value: NumericalValue) => void
34
41
  id?: string
35
42
  path?: string
36
43
  /** Optional UI component slot overrides from the admin config. */
@@ -38,11 +45,36 @@ export const NumericalField = ({
38
45
  }) => {
39
46
  const fieldPath = path ?? field.name
40
47
  const fieldError = useFieldError(fieldPath)
41
- const fieldValue = useFieldValue<string | number | undefined>(fieldPath)
42
- const incomingValue = value ?? fieldValue ?? defaultValue ?? ''
48
+ const fieldValue = useFieldValue<NumericalValue | undefined>(fieldPath)
49
+ const incomingValue =
50
+ value !== undefined ? value : fieldValue !== undefined ? fieldValue : (defaultValue ?? null)
43
51
  const htmlId = id ?? fieldPath
44
- const displayValue =
45
- incomingValue === undefined || incomingValue === null ? '' : String(incomingValue)
52
+ const canonicalDisplay = incomingValue == null ? '' : String(incomingValue)
53
+ const [displayValue, setDisplayValue] = useState(canonicalDisplay)
54
+ const [isEditing, setIsEditing] = useState(false)
55
+
56
+ useEffect(() => {
57
+ if (!isEditing) setDisplayValue(canonicalDisplay)
58
+ }, [canonicalDisplay, isEditing])
59
+
60
+ const canonicalize = (nextValue: NumericalValue): NumericalValue | undefined => {
61
+ if (nextValue == null || (typeof nextValue === 'string' && nextValue.trim() === '')) return null
62
+ if (field.type === 'counter') return undefined
63
+ if (typeof nextValue === 'string' && !COMPLETE_NUMERIC_RE.test(nextValue.trim())) {
64
+ return undefined
65
+ }
66
+ try {
67
+ return normalizeNumericValue(field.type, nextValue, fieldPath)
68
+ } catch {
69
+ return undefined
70
+ }
71
+ }
72
+
73
+ const commit = (nextValue: NumericalValue) => {
74
+ const canonical = canonicalize(nextValue)
75
+ if (canonical !== undefined) onChange?.(canonical)
76
+ return canonical
77
+ }
46
78
 
47
79
  // Custom component slots (from admin config)
48
80
  const slots = components
@@ -79,7 +111,7 @@ export const NumericalField = ({
79
111
  return (
80
112
  <CustomField
81
113
  {...slotBaseProps}
82
- onChange={(v: any) => onChange?.(v)}
114
+ onChange={(nextValue: NumericalValue) => commit(nextValue)}
83
115
  defaultValue={defaultValue}
84
116
  placeholder={field.placeholder}
85
117
  />
@@ -87,7 +119,9 @@ export const NumericalField = ({
87
119
  }
88
120
  return (
89
121
  <Input
90
- type="number"
122
+ type="text"
123
+ inputMode={field.type === 'integer' || field.type === 'counter' ? 'numeric' : 'decimal'}
124
+ step={field.type === 'integer' || field.type === 'counter' ? 1 : 'any'}
91
125
  id={htmlId}
92
126
  name={field.name}
93
127
  label={suppressInputLabel ? undefined : field.label}
@@ -95,7 +129,18 @@ export const NumericalField = ({
95
129
  readOnly={field.readOnly}
96
130
  helpText={suppressInputHelpText ? undefined : field.helpText}
97
131
  value={displayValue}
98
- onChange={(e) => onChange?.(e.target.value)}
132
+ onFocus={() => setIsEditing(true)}
133
+ onChange={(e) => {
134
+ setDisplayValue(e.target.value)
135
+ commit(e.target.value)
136
+ }}
137
+ onBlur={() => {
138
+ setIsEditing(false)
139
+ const canonical = commit(displayValue)
140
+ setDisplayValue(
141
+ canonical === undefined ? canonicalDisplay : (canonical?.toString() ?? '')
142
+ )
143
+ }}
99
144
  error={fieldError != null}
100
145
  errorText={fieldError}
101
146
  />
@@ -12,8 +12,8 @@
12
12
  * Admin sign-in form.
13
13
  *
14
14
  * Client component — collects email + password, calls the `adminSignIn`
15
- * server fn, and on success navigates to the caller-supplied
16
- * `callbackUrl` (or `/admin`). On failure renders a generic "Invalid
15
+ * server fn, and on success navigates to a safe caller-supplied destination.
16
+ * On failure renders a generic "Invalid
17
17
  * credentials" alert; the provider equalises timing between
18
18
  * unknown-email and wrong-password so the UI doesn't distinguish the two.
19
19
  *
@@ -25,15 +25,19 @@
25
25
 
26
26
  import { type FormEvent, useState } from 'react'
27
27
 
28
+ import { getClientConfig } from '@byline/core'
28
29
  import { useTranslation } from '@byline/i18n/react'
29
30
  import { Alert, Button, Card, Input, LoaderEllipsis } from '@byline/ui/react'
30
31
  import cx from 'classnames'
31
32
 
32
33
  import { useBylineAdminServices } from '../../../services/admin-services-context.js'
34
+ import { normalizeRootRelativeRedirect, resolveSignInFormRedirect } from '../safe-redirect.js'
33
35
  import styles from './sign-in-form.module.css'
34
36
 
35
- interface SignInFormProps {
36
- /** Destination after successful sign-in. Defaults to `/admin`. */
37
+ export interface SignInFormProps {
38
+ /** Host-validated root-relative destination after successful sign-in. */
39
+ redirectTo?: string
40
+ /** @deprecated Use `redirectTo`. */
37
41
  callbackUrl?: string
38
42
  /**
39
43
  * Optional plain "Home" link rendered on the left of the action row.
@@ -43,7 +47,7 @@ interface SignInFormProps {
43
47
  homeUrl?: string
44
48
  }
45
49
 
46
- export function SignInForm({ callbackUrl, homeUrl }: SignInFormProps) {
50
+ export function SignInForm({ redirectTo, callbackUrl, homeUrl }: SignInFormProps) {
47
51
  const { adminSignIn } = useBylineAdminServices()
48
52
  const { t } = useTranslation('byline-admin')
49
53
  const [email, setEmail] = useState('')
@@ -59,19 +63,26 @@ export function SignInForm({ callbackUrl, homeUrl }: SignInFormProps) {
59
63
  return
60
64
  }
61
65
 
66
+ const destination = resolveSignInFormRedirect(
67
+ redirectTo,
68
+ callbackUrl,
69
+ getClientConfig().routes.admin
70
+ )
71
+
62
72
  setPending(true)
63
73
  setError(null)
64
74
  try {
65
75
  await adminSignIn({ data: { email: email.trim(), password } })
66
- const target = callbackUrl && callbackUrl.length > 0 ? callbackUrl : '/admin'
67
- // Full-page navigation — the admin layout needs to re-run its
68
- // `beforeLoad` guard against the freshly-set session cookies.
69
- window.location.assign(target)
70
76
  } catch (err) {
71
77
  console.warn('sign-in failed', err)
72
78
  setError(t('auth.signIn.errors.invalidCredentials'))
73
79
  setPending(false)
80
+ return
74
81
  }
82
+
83
+ // Keep navigation outside the credential error boundary. A browser-level
84
+ // navigation failure must not relabel a successful sign-in as bad credentials.
85
+ window.location.assign(normalizeRootRelativeRedirect(destination) ?? '/')
75
86
  }
76
87
 
77
88
  return (
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { normalizeRootRelativeRedirect, resolveSignInFormRedirect } from './safe-redirect.js'
4
+
5
+ describe('normalizeRootRelativeRedirect', () => {
6
+ it.each([
7
+ ['/cms', '/cms'],
8
+ ['/cms/account?tab=profile#name', '/cms/account?tab=profile#name'],
9
+ ])('accepts %j', (value, expected) => {
10
+ expect(normalizeRootRelativeRedirect(value)).toBe(expected)
11
+ })
12
+
13
+ it.each([
14
+ '',
15
+ ' /cms',
16
+ 'https://evil.test',
17
+ '//evil.test',
18
+ '/\\evil.test',
19
+ '/cms\\account',
20
+ '/cms\naccount',
21
+ '%2Fcms',
22
+ '/%2F%2Fevil.test',
23
+ '/cms/%5cevil.test',
24
+ '/cms/%2e/account',
25
+ '/cms/../account',
26
+ '/cms/./account',
27
+ '/cms\u0085account',
28
+ ])('rejects %j', (value) => {
29
+ expect(normalizeRootRelativeRedirect(value)).toBeUndefined()
30
+ })
31
+ })
32
+
33
+ describe('resolveSignInFormRedirect', () => {
34
+ it('prefers a safe redirectTo over the deprecated callbackUrl', () => {
35
+ expect(resolveSignInFormRedirect('/cms/account', '/cms/users', '/cms')).toBe('/cms/account')
36
+ expect(
37
+ resolveSignInFormRedirect('/cms/account', undefined, () => {
38
+ throw new Error('fallback must be lazy')
39
+ })
40
+ ).toBe('/cms/account')
41
+ })
42
+
43
+ it('supports a safe deprecated callbackUrl when redirectTo is absent or unsafe', () => {
44
+ expect(resolveSignInFormRedirect(undefined, '/cms/users', '/cms')).toBe('/cms/users')
45
+ expect(resolveSignInFormRedirect('https://evil.test', '/cms/users', '/cms')).toBe('/cms/users')
46
+ })
47
+
48
+ it('uses the safe configured fallback without permitting an open redirect', () => {
49
+ expect(resolveSignInFormRedirect('//evil.test', 'https://evil.test', '/cms')).toBe('/cms')
50
+ expect(resolveSignInFormRedirect(undefined, undefined, 'https://evil.test')).toBe('/')
51
+ })
52
+ })
@@ -0,0 +1,33 @@
1
+ import { normalizeRootRelativeRedirect as normalizeCoreRedirect } from '@byline/core'
2
+
3
+ /** Accept only canonical, unencoded, root-relative same-origin redirect paths. */
4
+ export function normalizeRootRelativeRedirect(value: string): string | undefined {
5
+ const normalized = normalizeCoreRedirect(value)
6
+ if (!normalized) return undefined
7
+
8
+ // Keep an origin check next to the navigation sink even if core validation changes.
9
+ let url: URL
10
+ try {
11
+ url = new URL(normalized, 'https://byline.invalid')
12
+ } catch {
13
+ return undefined
14
+ }
15
+
16
+ if (url.origin !== 'https://byline.invalid') return undefined
17
+ return `${url.pathname}${url.search}${url.hash}`
18
+ }
19
+
20
+ /** Resolve the new prop first, then the deprecated prop, then a trusted fallback. */
21
+ export function resolveSignInFormRedirect(
22
+ redirectTo: string | undefined,
23
+ callbackUrl: string | undefined,
24
+ fallback: string | (() => string)
25
+ ): string {
26
+ const requested =
27
+ (redirectTo ? normalizeRootRelativeRedirect(redirectTo) : undefined) ??
28
+ (callbackUrl ? normalizeRootRelativeRedirect(callbackUrl) : undefined)
29
+ if (requested) return requested
30
+
31
+ const defaultPath = typeof fallback === 'function' ? fallback() : fallback
32
+ return normalizeRootRelativeRedirect(defaultPath) ?? '/'
33
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, expectTypeOf, it } from 'vitest'
2
+
3
+ import type { SignInFormProps } from './components/sign-in-form.js'
4
+
5
+ describe('SignInFormProps', () => {
6
+ it('keeps legacy callbackUrl-only callers type-compatible', () => {
7
+ expectTypeOf<{ callbackUrl: string }>().toMatchTypeOf<SignInFormProps>()
8
+ })
9
+
10
+ it('accepts the preferred redirectTo prop and no destination prop', () => {
11
+ expectTypeOf<{ redirectTo: string }>().toMatchTypeOf<SignInFormProps>()
12
+ expectTypeOf<Record<string, never>>().toMatchTypeOf<SignInFormProps>()
13
+ })
14
+ })