-
Notifications
You must be signed in to change notification settings - Fork 11.2k
Commit
…primitives (#13160) ## Description This PR adds the new "Protect Account" screen used when clicking "Create a new account" button and as the second step of the "Import Passphrase" and "Import Private Key" flows. As part of this, I attempted to create some new form primitives to make building forms more manageable (all of the existing components aren't quite up to spec, use Formik, and aren't fully accessible). I ended up following an approach similar to https://www.brendonovich.dev/blog/the-ultimate-form-abstraction which gave some creds to @Jordan-Mysten 😆 As a rough outline, we have generic, non-form-library-specific input controls like `TextArea`, `Input`, `PasswordInput`, `Checkbox` which are used to create `react-hook-form` specific controls such as `TextField`, `TextAreaField`, `CheckboxField`, and so forth. We also have some helper components like `Form` and `FormField` to help abstract away some specific form details such as error states when using react-hook-form. I considered using the Radix form primitives, but I didn't really see the immediate value 🤷🏼 Additional note #1: Some of these pages are used in different flows and have different submission logic depending on the usage context. I think I might need to brainstorm on the best way to handle that and tackle it in a follow-up PR Additional note #2: the auto-lock input is still a WIP on the design side, so I have a TODO to add that once it's ready. Checkbox in Figma - https://www.figma.com/file/T06obgYVOUD2JDGXM8QEDX?node-id=341%3A378&main-component=1&fuid=1209977329759347633 Input in Figma - https://www.figma.com/file/T06obgYVOUD2JDGXM8QEDX/01-Components-%3A-Shared?node-id=19%3A312&mode=dev <img width="631" alt="image" src="https://github.com/MystenLabs/sui/assets/7453188/4c851808-b751-412a-b25e-06d4660b5fa3"> ## Test Plan - Manual testing (error states, successful submission, accessibility, focus/disabled/hover states, etc.) - CI --- If your changes are not user-facing and not a breaking change, you can skip the following section. Otherwise, please indicate what changed, and then add to the Release Notes section as highlighted during the release process. ### Type of Change (Check all that apply) - [ ] protocol change - [ ] user-visible impact - [ ] breaking change for a client SDKs - [ ] breaking change for FNs (FN binary must upgrade) - [ ] breaking change for validators or node operators (must upgrade binaries) - [ ] breaking change for on-chain data layout - [ ] necessitate either a data wipe or data migration ### Release notes
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// Copyright (c) Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { yupResolver } from '@hookform/resolvers/yup'; | ||
This comment has been minimized.
Sorry, something went wrong. |
||
import { useForm, type SubmitHandler } from 'react-hook-form'; | ||
import { useNavigate } from 'react-router-dom'; | ||
import * as Yup from 'yup'; | ||
import { CheckboxField } from '../../shared/forms/CheckboxField'; | ||
import { Form } from '../../shared/forms/Form'; | ||
import { TextField } from '../../shared/forms/TextField'; | ||
import ExternalLink from '../external-link'; | ||
import { Button } from '_app/shared/ButtonUI'; | ||
import { ToS_LINK } from '_src/shared/constants'; | ||
|
||
const formSchema = Yup.object({ | ||
password: Yup.string().required('Required'), | ||
confirmedPassword: Yup.string().required('Required'), | ||
acceptedTos: Yup.boolean().required().oneOf([true]), | ||
enabledAutolock: Yup.boolean(), | ||
}); | ||
|
||
type FormValues = Yup.InferType<typeof formSchema>; | ||
|
||
type ProtectAccountFormProps = { | ||
submitButtonText: string; | ||
cancelButtonText: string; | ||
onSubmit: SubmitHandler<FormValues>; | ||
}; | ||
|
||
export function ProtectAccountForm({ | ||
submitButtonText, | ||
cancelButtonText, | ||
onSubmit, | ||
}: ProtectAccountFormProps) { | ||
const form = useForm({ | ||
mode: 'all', | ||
defaultValues: { | ||
password: '', | ||
confirmedPassword: '', | ||
acceptedTos: false, | ||
enabledAutolock: true, | ||
}, | ||
resolver: yupResolver(formSchema), | ||
}); | ||
const { | ||
register, | ||
formState: { isSubmitting, isValid }, | ||
} = form; | ||
const navigate = useNavigate(); | ||
|
||
return ( | ||
<Form className="flex flex-col gap-6 h-full" form={form} onSubmit={onSubmit}> | ||
<TextField type="password" label="Create Account Password" {...register('password')} /> | ||
<TextField | ||
type="password" | ||
label="Confirm Account Password" | ||
{...register('confirmedPassword')} | ||
/> | ||
<div className="flex flex-col gap-4"> | ||
<CheckboxField name="enabledAutolock" label="Auto-lock after I am inactive for" /> | ||
{/* TODO: Abhi is working on designs for the auto-lock input, we'll add this when it's ready */} | ||
</div> | ||
<div className="flex flex-col gap-5 mt-auto"> | ||
<CheckboxField | ||
name="acceptedTos" | ||
label={ | ||
<> | ||
I read and agreed to the{' '} | ||
<ExternalLink href={ToS_LINK} className="text-[#1F6493] no-underline"> | ||
Terms of Services | ||
</ExternalLink> | ||
</> | ||
} | ||
/> | ||
<div className="flex gap-2.5"> | ||
<Button | ||
variant="outline" | ||
size="tall" | ||
text={cancelButtonText} | ||
onClick={() => navigate(-1)} | ||
/> | ||
<Button | ||
type="submit" | ||
disabled={isSubmitting || !isValid} | ||
variant="primary" | ||
size="tall" | ||
loading={isSubmitting} | ||
text={submitButtonText} | ||
/> | ||
</div> | ||
</div> | ||
</Form> | ||
); | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// Copyright (c) Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { ProtectAccountForm } from '../../components/accounts/ProtectAccountForm'; | ||
import { Heading } from '../../shared/heading'; | ||
import { Text } from '_app/shared/text'; | ||
|
||
export function ProtectAccountPage() { | ||
return ( | ||
<div className="rounded-20 bg-sui-lightest shadow-wallet-content flex flex-col items-center px-6 py-10 h-full"> | ||
<Text variant="caption" color="steel-dark" weight="semibold"> | ||
Wallet Setup | ||
</Text> | ||
<div className="text-center mt-2.5"> | ||
<Heading variant="heading1" color="gray-90" as="h1" weight="bold"> | ||
Protect Account with a Password Lock | ||
</Heading> | ||
</div> | ||
<div className="mt-6 w-full grow"> | ||
<ProtectAccountForm | ||
cancelButtonText="Back" | ||
submitButtonText="Create Wallet" | ||
onSubmit={(formValues) => { | ||
// eslint-disable-next-line no-console | ||
console.log( | ||
'TODO: Do something when the user submits the form successfully', | ||
formValues, | ||
); | ||
}} | ||
/> | ||
</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// Copyright (c) Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { type ComponentProps, type ReactNode, forwardRef } from 'react'; | ||
import { Controller, useFormContext } from 'react-hook-form'; | ||
import { Checkbox } from './controls/Checkbox'; | ||
|
||
type CheckboxFieldProps = { | ||
name: string; | ||
label: ReactNode; | ||
} & Omit<ComponentProps<'button'>, 'ref'>; | ||
|
||
export const CheckboxField = forwardRef<HTMLButtonElement, CheckboxFieldProps>( | ||
({ label, name, ...props }, forwardedRef) => { | ||
const { control } = useFormContext(); | ||
return ( | ||
<Controller | ||
control={control} | ||
name={name} | ||
render={({ field: { onChange, name, value } }) => ( | ||
<Checkbox | ||
label={label} | ||
onCheckedChange={onChange} | ||
name={name} | ||
checked={value} | ||
ref={forwardedRef} | ||
{...props} | ||
/> | ||
)} | ||
/> | ||
); | ||
}, | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// Copyright (c) Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { type ComponentProps } from 'react'; | ||
import { | ||
type FieldValues, | ||
FormProvider, | ||
type SubmitHandler, | ||
type UseFormReturn, | ||
} from 'react-hook-form'; | ||
|
||
type FormProps<T extends FieldValues> = Omit<ComponentProps<'form'>, 'onSubmit'> & { | ||
form: UseFormReturn<T>; | ||
onSubmit: SubmitHandler<T>; | ||
}; | ||
|
||
export function Form<T extends FieldValues>({ form, onSubmit, children, ...props }: FormProps<T>) { | ||
return ( | ||
<FormProvider {...form}> | ||
<form onSubmit={form.handleSubmit(onSubmit)} {...props}> | ||
{children} | ||
</form> | ||
</FormProvider> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// Copyright (c) Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
import { type ReactNode } from 'react'; | ||
import { useFormContext } from 'react-hook-form'; | ||
import { FormLabel } from './FormLabel'; | ||
import Alert from '../../components/alert'; | ||
|
||
type FormFieldProps = { | ||
name: string; | ||
label: ReactNode; | ||
children: ReactNode; | ||
}; | ||
|
||
export function FormField({ children, name, label }: FormFieldProps) { | ||
const { getFieldState, formState } = useFormContext(); | ||
const state = getFieldState(name, formState); | ||
|
||
return ( | ||
<div className="flex flex-col gap-2.5"> | ||
<FormLabel label={label}>{children}</FormLabel> | ||
{state.error && <Alert>{state.error.message}</Alert>} | ||
</div> | ||
); | ||
} | ||
|
||
export default FormField; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// Copyright (c) Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { Text } from '_app/shared/text'; | ||
|
||
import type { ReactNode } from 'react'; | ||
|
||
export type FormLabelProps = { | ||
label: ReactNode; | ||
children: ReactNode; | ||
}; | ||
|
||
export function FormLabel({ label, children }: FormLabelProps) { | ||
return ( | ||
<label className="flex flex-col flex-nowrap gap-2.5"> | ||
<div className="pl-2.5"> | ||
<Text variant="body" color="steel-darker" weight="semibold"> | ||
{label} | ||
</Text> | ||
</div> | ||
{children} | ||
</label> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Copyright (c) Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { type ComponentProps, type ReactNode, forwardRef } from 'react'; | ||
import FormField from './FormField'; | ||
import { TextArea } from './controls/TextArea'; | ||
|
||
type TextAreaFieldProps = { | ||
name: string; | ||
label: ReactNode; | ||
} & ComponentProps<'textarea'>; | ||
|
||
export const TextAreaField = forwardRef<HTMLTextAreaElement, TextAreaFieldProps>( | ||
({ label, ...props }, forwardedRef) => ( | ||
<FormField name={props.name} label={label}> | ||
<TextArea {...props} ref={forwardedRef} /> | ||
</FormField> | ||
), | ||
); |
6 comments
on commit ffb4258
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
sui-typescript-docs – ./sdk/docs
sui-typescript-docs-git-main-mysten-labs.vercel.app
sui-typescript-docs.vercel.app
sui-typescript-docs-mysten-labs.vercel.app
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
mysten-ui – ./apps/ui
mysten-ui.vercel.app
mysten-ui-mysten-labs.vercel.app
mysten-ui-git-main-mysten-labs.vercel.app
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
sui-kiosk – ./dapps/kiosk
sui-kiosk.vercel.app
sui-kiosk-mysten-labs.vercel.app
sui-kiosk-git-main-mysten-labs.vercel.app
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
multisig-toolkit – ./dapps/multisig-toolkit
multisig-toolkit-mysten-labs.vercel.app
multisig-toolkit-git-main-mysten-labs.vercel.app
offline-signer-helper.vercel.app
multisig-toolkit.vercel.app
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
explorer – ./apps/explorer
explorer-mysten-labs.vercel.app
explorer-git-main-mysten-labs.vercel.app
www.explorer.sui.io
explorer.sui.io
explorer-topaz.vercel.app
suiexplorer.com
www.suiexplorer.com
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
sui-wallet-kit – ./sdk/wallet-adapter/site
sui-wallet-kit-git-main-mysten-labs.vercel.app
sui-wallet-kit-mysten-labs.vercel.app
sui-wallet-kit.vercel.app
How can I convince you to move this to Zod.