Skip to content

Commit

Permalink
feat(wallet): rebrand password page (#1550)
Browse files Browse the repository at this point in the history
* feat(wallet): rebrand import mnemonic screen

* feat: add showText button and icon

* feat: add import mnemonic page

* fix: remove header

* fix: bring back custom onPaste function and add buttonHtmlType

* feat: first steps

* feat: refine page

* feat: update checkbox

* feat: refine checkbox

* feat: update styles

* refactor: update to latest figma changes

* fix: remove unnecessary prop after merging

* fix: update header

Co-authored-by: evavirseda <[email protected]>

* feat: update styles and cleanup

* feat: show missing errors

* feat: improvements

* feat: add missing template

* feat: update gap

* fix: input trailing icon sizing

* refactor: use gap token

Co-authored-by: Eugene P. <[email protected]>

* feat: add overflow-auto

---------

Co-authored-by: JCNoguera <[email protected]>
Co-authored-by: JCNoguera <[email protected]>
Co-authored-by: Begoña Álvarez de la Cruz <[email protected]>
Co-authored-by: Eugene P. <[email protected]>
  • Loading branch information
5 people authored Aug 12, 2024
1 parent 3dd456f commit 4311bd8
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 131 deletions.
103 changes: 66 additions & 37 deletions apps/ui-kit/src/lib/components/atoms/checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { useEffect, useRef } from 'react';
import { forwardRef, useEffect, useRef } from 'react';
import cx from 'classnames';
import { Dash, Checkmark } from '@iota/ui-icons';

interface CheckboxProps {
/**
* The label of the checkbox.
*/
label?: string;
label?: string | React.ReactNode;
/**
* The state of the checkbox.
*/
Expand All @@ -29,56 +29,85 @@ interface CheckboxProps {
/**
* The callback to call when the checkbox is clicked.
*/
onChange?: (checked: boolean) => void;
onCheckedChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
/**
* The name of the checkbox.
*/
name?: string;
}

export function Checkbox({
isChecked,
isIndeterminate,
label,
isLabelFirst,
isDisabled,
onChange,
}: CheckboxProps) {
const inputRef = useRef<HTMLInputElement>(null);
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
(
{
isChecked,
isIndeterminate,
label,
isLabelFirst,
isDisabled,
onCheckedChange,
name,
}: CheckboxProps,
ref,
) => {
const inputRef = useRef<HTMLInputElement | null>(null);

useEffect(() => {
if (inputRef.current) {
inputRef.current.indeterminate = isIndeterminate ?? false;
}
}, [isIndeterminate, inputRef]);
useEffect(() => {
if (inputRef.current) {
inputRef.current.indeterminate = isIndeterminate ?? false;
}
}, [isIndeterminate, inputRef]);

const CheckmarkIcon = isIndeterminate ? Dash : Checkmark;
const CheckmarkIcon = isIndeterminate ? Dash : Checkmark;

return (
<label
className={cx('group inline-flex', isLabelFirst ? 'flex-row-reverse' : 'flex-row', {
disabled: isDisabled,
'gap-x-2': label,
})}
>
<div className="relative h-5 w-5">
function assignRefs(element: HTMLInputElement) {
if (ref) {
if (typeof ref === 'function') {
ref(element);
} else {
ref.current = element;
}
}
inputRef.current = element;
}

return (
<div
className={cx('group inline-flex', isLabelFirst ? 'flex-row-reverse' : 'flex-row', {
disabled: isDisabled,
'gap-x-2': label,
})}
>
<input
id={name}
name={name}
type="checkbox"
className="enabled:state-layer peer h-full w-full appearance-none rounded border border-neutral-80 disabled:opacity-40 dark:border-neutral-20 [&:is(:checked,:indeterminate)]:border-primary-30 [&:is(:checked,:indeterminate)]:bg-primary-30 disabled:[&:is(:checked,:indeterminate)]:border-neutral-60 disabled:[&:is(:checked,:indeterminate)]:bg-neutral-60 dark:disabled:[&:is(:checked,:indeterminate)]:border-neutral-40 dark:disabled:[&:is(:checked,:indeterminate)]:bg-neutral-40 disabled:[&:not(:checked,:indeterminate)]:border-neutral-70 dark:disabled:[&:not(:checked,:indeterminate)]:border-neutral-30"
className="peer hidden appearance-none"
checked={isChecked}
ref={inputRef}
ref={assignRefs}
disabled={isDisabled}
onChange={(e) => onChange?.(e.target.checked)}
onChange={(e) => {
onCheckedChange?.(e);
}}
/>
<span className="absolute inset-0 flex h-full w-full items-center justify-center text-neutral-40 peer-enabled:cursor-pointer peer-disabled:text-neutral-70 peer-disabled:text-opacity-40 peer-[&:is(:checked,:indeterminate)]:text-white peer-[&:not(:checked,:indeterminate)]:text-opacity-40 dark:text-neutral-60 dark:peer-disabled:text-neutral-30 dark:peer-disabled:text-opacity-40">
<span
onClick={() => inputRef.current?.click()}
className="peer-enabled:state-layer relative inset-0 flex h-5 w-5 items-center justify-center rounded border border-neutral-80 text-neutral-40 peer-disabled:text-neutral-70 peer-disabled:text-opacity-40 peer-disabled:opacity-40 peer-[&:is(:checked,:indeterminate)]:border-primary-30 peer-[&:is(:checked,:indeterminate)]:bg-primary-30 peer-[&:is(:checked,:indeterminate)]:text-white peer-[&:not(:checked,:indeterminate)]:text-opacity-40 disabled:peer-[&:not(:checked,:indeterminate)]:border-neutral-70 dark:border-neutral-20 dark:text-neutral-60 dark:peer-disabled:text-neutral-30 dark:peer-disabled:text-opacity-40 dark:peer-disabled:peer-[&:not(:checked,:indeterminate)]:border-neutral-30 dark:peer-disabled:peer-[&:is(:checked,:indeterminate)]:bg-neutral-40 peer-disabled:[&:is(:checked,:indeterminate)]:border-neutral-60 peer-disabled:[&:is(:checked,:indeterminate)]:bg-neutral-60 dark:peer-disabled:[&:is(:checked,:indeterminate)]:border-neutral-40"
>
<CheckmarkIcon width={16} height={16} />
</span>
<LabelText label={label} name={name} />
</div>
<LabelText label={label} />
</label>
);
}
);
},
);

function LabelText({ label }: Pick<CheckboxProps, 'label'>) {
function LabelText({ label, name }: Pick<CheckboxProps, 'label' | 'name'>) {
return (
<span className="text-label-lg text-neutral-40 group-[.disabled]:text-opacity-40 dark:text-neutral-60 group-[.disabled]:dark:text-opacity-40">
<label
htmlFor={name}
className="text-label-lg text-neutral-40 group-[.disabled]:text-opacity-40 dark:text-neutral-60 group-[.disabled]:dark:text-opacity-40"
>
{label}
</span>
</label>
);
}
10 changes: 8 additions & 2 deletions apps/ui-kit/src/lib/components/molecules/input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,12 @@ function InputTrailingElement({
const showPasswordToggle = Boolean(type === InputType.Password && onToggleButtonClick);
const showTrailingElement = Boolean(trailingElement && !showClearInput && !showPasswordToggle);

const ICON_WIDTH_HEIGHT = 20;

if (showClearInput) {
return (
<ButtonUnstyled className="text-neutral-10 dark:text-neutral-92" onClick={onClearInput}>
<Close />
<Close width={ICON_WIDTH_HEIGHT} height={ICON_WIDTH_HEIGHT} />
</ButtonUnstyled>
);
} else if (showPasswordToggle) {
Expand All @@ -195,7 +197,11 @@ function InputTrailingElement({
onClick={onToggleButtonClick}
className="text-neutral-10 dark:text-neutral-92"
>
{isContentVisible ? <VisibilityOn /> : <VisibilityOff />}
{isContentVisible ? (
<VisibilityOn width={ICON_WIDTH_HEIGHT} height={ICON_WIDTH_HEIGHT} />
) : (
<VisibilityOff width={ICON_WIDTH_HEIGHT} height={ICON_WIDTH_HEIGHT} />
)}
</ButtonUnstyled>
);
} else if (showTrailingElement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ type TableCellCheckbox = {
/**
* The function to call when the checkbox is clicked.
*/
onChange?: (checked: boolean) => void;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
/**
* If true the checkbox will override the styles to show an indeterminate state.
*/
Expand Down Expand Up @@ -138,7 +138,7 @@ export function TableCell(props: TableCellProps): JSX.Element {
return (
<Checkbox
isChecked={isChecked}
onChange={onChange}
onCheckedChange={onChange}
isIndeterminate={isIndeterminate}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export interface TableHeaderCellProps {
/**
* On Checkbox change.
*/
onCheckboxChange?: (checked: boolean) => void;
onCheckboxChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
/**
* Whether the cell content should be centered.
*/
Expand Down Expand Up @@ -97,7 +97,7 @@ export function TableHeaderCell({
<Checkbox
isChecked={isChecked}
isIndeterminate={isIndeterminate}
onChange={onCheckboxChange}
onCheckedChange={onCheckboxChange}
/>
) : (
<span>{label}</span>
Expand Down
12 changes: 6 additions & 6 deletions apps/ui-kit/src/lib/components/organisms/table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ function TableRowCheckbox({
<TableHeaderCell
isContentCentered
hasCheckbox
onCheckboxChange={(checked) => {
toggleHeaderChecked(checked);
onHeaderCheckboxChange?.(checked);
onCheckboxChange={(event) => {
toggleHeaderChecked(event.target.checked);
onHeaderCheckboxChange?.(event.target.checked);
}}
isChecked={isHeaderChecked}
columnKey={1}
Expand All @@ -200,10 +200,10 @@ function TableRowCheckbox({
return (
<TableCell
isContentCentered
onChange={(checked) => {
onChange={(event) => {
if (rowIndex !== undefined) {
const checkboxValues = toggleRowChecked?.(checked, rowIndex);
onRowCheckboxChange?.(checked, rowIndex, checkboxValues);
const checkboxValues = toggleRowChecked?.(event.target.checked, rowIndex);
onRowCheckboxChange?.(event.target.checked, rowIndex, checkboxValues);
}
}}
type={TableCellType.Checkbox}
Expand Down
6 changes: 3 additions & 3 deletions apps/wallet/src/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export const ToS_LINK = 'https://mystenlabs.com/legal#iotawallettermsofservice';
export const PRIVACY_POLICY_LINK = 'https://mystenlabs.com/legal#privacypolicy';
export const FAQ_LINK = 'https://docs.mystenlabs.com/faq';
export const ToS_LINK = 'https://www.iota.org/terms-of-use';
export const PRIVACY_POLICY_LINK = 'https://www.iota.org/privacy-policy';
export const FAQ_LINK = 'https://wiki.iota.org/';

// number of epochs before earning
// Staking Rewards Redeemable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ export function AutoLockSelector({ disabled }: AutoLockSelectorProps) {
return unsubscribe;
}, [watch, trigger]);
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-xs">
<CheckboxField
name="autoLock.enabled"
label="Auto-lock after I am inactive for"
label="Auto-lock after I'm inactive for"
disabled={disabled}
/>
<FormField name="autoLock.timer">
<div className="flex items-start justify-between gap-2">
<div className="flex items-start justify-between gap-xs">
<Input
disabled={disabled || !timerEnabled}
type="number"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export function ImportRecoveryPhraseForm({
className="relative flex h-full flex-col justify-between"
onSubmit={handleSubmit(onSubmit)}
>
<div className="grid h-full grid-cols-2 gap-2 overflow-auto pb-md">
<div className="grid grid-cols-2 gap-2 overflow-auto pb-md">
{recoveryPhrase.map((_, index) => {
const recoveryPhraseId = `recoveryPhrase.${index}` as const;
return (
Expand All @@ -124,7 +124,7 @@ export function ImportRecoveryPhraseForm({
})}
</div>

<div className="sticky bottom-0 left-0 flex flex-col gap-2.5 bg-neutral-100 pt-sm">
<div className="sticky bottom-0 left-0 flex flex-col gap-2.5 bg-neutral-100 py-sm">
{touchedFields.recoveryPhrase && errors.recoveryPhrase && (
<InfoBox
type={InfoBoxType.Default}
Expand Down
73 changes: 38 additions & 35 deletions apps/wallet/src/ui/app/components/accounts/ProtectAccountForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { Button } from '_app/shared/ButtonUI';
import { ToS_LINK } from '_src/shared/constants';
import { useZodForm } from '@iota/core';
import { useEffect } from 'react';
import { type SubmitHandler } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { z } from 'zod';
import zxcvbn from 'zxcvbn';

import { parseAutoLock, useAutoLockMinutes } from '../../hooks/useAutoLockMinutes';
import { CheckboxField } from '../../shared/forms/CheckboxField';
import { Form } from '../../shared/forms/Form';
import { TextField } from '../../shared/forms/TextField';
import { Link } from '../../shared/Link';
import { AutoLockSelector, zodSchema } from './AutoLockSelector';
import { Button, ButtonHtmlType, ButtonType, Input, InputType } from '@iota/apps-ui-kit';

function addDot(str: string | undefined) {
if (str && !str.endsWith('.')) {
Expand Down Expand Up @@ -104,55 +101,61 @@ export function ProtectAccountForm({
return unsubscribe;
}, [watch, trigger, getValues]);
return (
<Form className="flex h-full flex-col gap-6" form={form} onSubmit={onSubmit}>
<TextField
autoFocus
type="password"
label="Create Account Password"
{...register('password.input')}
/>
<TextField
type="password"
label="Confirm Account Password"
{...register('password.confirmation')}
/>
<AutoLockSelector />
<div className="flex-1" />
<div className="flex flex-col gap-5">
<Form className="flex h-full flex-col justify-between" form={form} onSubmit={onSubmit}>
<div className="flex h-full flex-col gap-6">
<Input
autoFocus
type={InputType.Password}
isVisibilityToggleEnabled
label="Create Password"
placeholder="Password"
errorMessage={form.formState.errors.password?.input?.message}
{...register('password.input')}
name="password.input"
/>
<Input
type={InputType.Password}
isVisibilityToggleEnabled
label="Confirm Password"
placeholder="Password"
errorMessage={form.formState.errors.password?.confirmation?.message}
{...register('password.confirmation')}
name="password.confirmation"
/>
<AutoLockSelector />
</div>
<div className="flex flex-col gap-4 pt-xxxs">
{displayToS ? null : (
<CheckboxField
name="acceptedTos"
label={
<div className="whitespace-nowrap text-bodySmall">
I read and agreed to the{' '}
<span className="inline-block">
<Link
href={ToS_LINK}
beforeColor="steelDarker"
color="iotaDark"
text="Terms of Services"
/>
<div className="flex items-center gap-x-0.5 whitespace-nowrap">
<span className="text-label-lg text-neutral-40 dark:text-neutral-60">
I read and agreed to the
</span>
<a href={ToS_LINK} className="text-label-lg text-primary-30">
Terms of Services
</a>
</div>
}
/>
)}
<div className="flex gap-2.5">

<div className="flex flex-row justify-stretch gap-2.5">
{cancelButtonText ? (
<Button
variant="outline"
size="tall"
type={ButtonType.Secondary}
text={cancelButtonText}
onClick={() => navigate(-1)}
fullWidth
/>
) : null}
<Button
type="submit"
type={ButtonType.Primary}
disabled={isSubmitting || !isValid}
variant="primary"
size="tall"
loading={isSubmitting}
text={submitButtonText}
fullWidth
htmlType={ButtonHtmlType.Submit}
/>
</div>
</div>
Expand Down
Loading

0 comments on commit 4311bd8

Please sign in to comment.