-
Notifications
You must be signed in to change notification settings - Fork 137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: otp new styles and flow #1250
Changes from 8 commits
a61b0fc
c819c8b
57d2b6c
4e94608
b00b3d4
099952d
c4cc6a3
3c1b585
0782fa5
902736d
b7ff4e6
3a127a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import { useEffect, useState } from "react"; | ||
import { useState } from "react"; | ||
import { EmailIllustration } from "../../../../icons/illustrations/email.js"; | ||
import { ls } from "../../../../strings.js"; | ||
import { | ||
|
@@ -8,54 +8,50 @@ import { | |
isOTPCodeType, | ||
} from "../../../otp-input/otp-input.js"; | ||
import { Spinner } from "../../../../icons/spinner.js"; | ||
import { useAuthContext } from "../../context.js"; | ||
import { AuthStepStatus, useAuthContext } from "../../context.js"; | ||
import { useAuthenticate } from "../../../../hooks/useAuthenticate.js"; | ||
import { useSignerStatus } from "../../../../hooks/useSignerStatus.js"; | ||
|
||
export const LoadingOtp = () => { | ||
const { isConnected } = useSignerStatus(); | ||
const { authStep, setAuthStep } = useAuthContext("otp_verify"); | ||
const { authStep } = useAuthContext("otp_verify"); | ||
const [otpCode, setOtpCode] = useState<OTPCodeType>(initialOTPValue); | ||
const [errorText, setErrorText] = useState( | ||
getUserErrorMessage(authStep.error) | ||
); | ||
const resetOTP = () => { | ||
const [errorText, setErrorText] = useState(authStep.error?.message || ""); | ||
const [titleText, setTitleText] = useState(ls.loadingOtp.title); | ||
const { setAuthStep } = useAuthContext(); | ||
const resetOTP = (errorText = "") => { | ||
setOtpCode(initialOTPValue); | ||
setErrorText(""); | ||
setErrorText(errorText); | ||
|
||
if (errorText) { | ||
setTitleText(ls.loadingOtp.title); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does resetting the OTP only reset the title if there is error text? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just so we don't call that function in case there wasn't an error, in that case the text doesn't have to be updated, but since removing the conditional is not affecting the functionality I'll just do it. |
||
}; | ||
const { authenticate } = useAuthenticate({ | ||
onError: (error: any) => { | ||
console.error(error); | ||
const { email } = authStep; | ||
setAuthStep({ type: "otp_verify", email, error }); | ||
|
||
setAuthStep({ ...authStep, error, status: null }); | ||
resetOTP(getUserErrorMessage(error)); | ||
}, | ||
onSuccess: () => { | ||
if (isConnected) { | ||
setAuthStep({ type: "complete" }); | ||
setAuthStep({ ...authStep, status: AuthStepStatus.success }); | ||
setTitleText(ls.loadingOtp.verified); | ||
setTimeout(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks to me like we might be able to derive the title text from the auth step, so we don't need to maintain state for it. Is that right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need the state since the error text is not only updated by the auth step but also the input field which is a child component, the error can come from there too. |
||
setAuthStep({ type: "complete" }); | ||
}, 3000); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's extract this delay into a top-level constant in this file. |
||
}, | ||
}); | ||
|
||
useEffect(() => { | ||
if (isOTPCodeType(otpCode)) { | ||
setAuthStep({ | ||
type: "otp_completing", | ||
email: authStep.email, | ||
otp: otpCode.join(""), | ||
}); | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [otpCode]); | ||
|
||
const setValue = (otpCode: OTPCodeType) => { | ||
setOtpCode(otpCode); | ||
if (isOTPCodeType(otpCode)) { | ||
const otp = otpCode.join(""); | ||
setAuthStep({ | ||
type: "otp_completing", | ||
email: authStep.email, | ||
otp, | ||
}); | ||
|
||
setAuthStep({ ...authStep, status: AuthStepStatus.verifying }); | ||
setTitleText(ls.loadingOtp.verifying); | ||
authenticate({ type: "otp", otpCode: otp }); | ||
} | ||
}; | ||
|
@@ -71,7 +67,7 @@ export const LoadingOtp = () => { | |
/> | ||
</div> | ||
<h3 className="text-fg-primary font-semibold text-lg mb-2"> | ||
{ls.loadingOtp.title} | ||
{titleText} | ||
</h3> | ||
<p className="text-fg-secondary text-center text-sm mb-1"> | ||
{ls.loadingOtp.body} | ||
|
@@ -80,32 +76,18 @@ export const LoadingOtp = () => { | |
{authStep.email} | ||
</p> | ||
<OTPInput | ||
disabled={authStep.status === AuthStepStatus.verifying} | ||
value={otpCode} | ||
setValue={setValue} | ||
setErrorText={setErrorText} | ||
errorText={errorText} | ||
handleReset={resetOTP} | ||
isVerified={authStep.status === AuthStepStatus.success} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export const CompletingOtp = () => { | ||
return ( | ||
<div className="flex flex-col items-center justify-center "> | ||
<div className="flex flex-col items-center justify-center h-12 w-12 mb-5"> | ||
<Spinner /> | ||
</div> | ||
<h2 className="text-fg-primary font-semibold text-lg mb-2"> | ||
{ls.completingOtp.title} | ||
</h2> | ||
<p className="text-fg-secondary text-center text-sm"> | ||
{ls.completingOtp.body} | ||
</p> | ||
</div> | ||
); | ||
}; | ||
|
||
function getUserErrorMessage(error: Error | undefined): string { | ||
if (!error) { | ||
return ""; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,20 @@ import type { Connector } from "@wagmi/core"; | |
import { createContext, useContext } from "react"; | ||
import type { AuthType } from "./types"; | ||
|
||
export enum AuthStepStatus { | ||
success = "success", | ||
error = "error", | ||
verifying = "verifying", | ||
} | ||
|
||
export type AuthStep = | ||
| { type: "email_verify"; email: string } | ||
| { type: "otp_verify"; email: string; error?: Error } | ||
| { | ||
type: "otp_verify"; | ||
email: string; | ||
error?: Error; | ||
status?: AuthStepStatus | null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah I will replace the null with a base state, good feedback, thanks. |
||
} | ||
| { type: "passkey_verify"; error?: Error } | ||
| { type: "passkey_create"; error?: Error } | ||
| { type: "passkey_create_success" } | ||
|
@@ -16,12 +27,6 @@ export type AuthStep = | |
config: Extract<AuthType, { type: "social" }>; | ||
error?: Error; | ||
} | ||
| { | ||
type: "otp_completing"; | ||
email: string; | ||
otp: string; | ||
error?: Error; | ||
} | ||
| { type: "initial"; error?: Error } | ||
| { type: "complete" } | ||
| { type: "eoa_connect"; connector: Connector; error?: Error } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,11 +7,12 @@ const OTP_LENGTH = 6; | |
type OTPInputProps = { | ||
errorText?: string; | ||
value: OTPCodeType; | ||
setValue: (otp: OTPCodeType) => void; | ||
setErrorText: (error: string) => void; | ||
setValue: (otpCode: OTPCodeType) => void; | ||
setErrorText: React.Dispatch<React.SetStateAction<string>>; | ||
disabled?: boolean; | ||
handleReset: () => void; | ||
className?: string; | ||
isVerified?: boolean; | ||
}; | ||
|
||
export const isOTPCodeType = (arg: string[]): arg is OTPCodeType => { | ||
|
@@ -30,6 +31,7 @@ export const OTPInput: React.FC<OTPInputProps> = ({ | |
setErrorText, | ||
handleReset, | ||
className, | ||
isVerified, | ||
}) => { | ||
const [autoComplete, setAutoComplete] = useState<string>(""); | ||
const [activeElement, setActiveElement] = useState<number | null>(0); | ||
|
@@ -140,9 +142,17 @@ export const OTPInput: React.FC<OTPInputProps> = ({ | |
<div className="flex gap-2.5"> | ||
{initialOTPValue.map((_, i) => ( | ||
<input | ||
className={`border w-8 bg-bg-surface-default h-10 text-fg-primary rounded text-center focus:outline-active ${ | ||
!!errorText && "border-fg-critical" | ||
}`} | ||
className={` | ||
border w-8 h-10 rounded text-center | ||
focus:outline-none focus:border-active | ||
${!disabled && "bg-bg-surface-default text-fg-primary"} | ||
${!!errorText && "border-fg-critical"} | ||
${isVerified && "border-fg-success"} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is maybe pretty nitpicky, but this can end up adding classes with names like "null", "undefined", or "false", which due to the global nature of CSS could maybe conceivably be classes defined in the surrounding app and therefore inadvertently add styling to this element. I know it's pretty unlikely someone would choose those classnames, but it would still be more correct to do these with ternaries, e.g. ${isVerified ? "border-fg-success" : ""} If writing that all the time is too burdensome, we can make ourselves a helper similar to clsx. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No nitpicky at all, this is great feedback, thanks! |
||
${ | ||
disabled && | ||
"border-fg-disabled bg-bg-surface-inset text-fg-disabled" | ||
} | ||
`} | ||
ref={(el) => (refs.current[i] = el)} | ||
tabIndex={i + 1} | ||
type="text" | ||
|
@@ -158,7 +168,7 @@ export const OTPInput: React.FC<OTPInputProps> = ({ | |
onInput={focusNextElement} | ||
onKeyDown={handleKeydown} | ||
key={i} | ||
disabled={disabled} | ||
disabled={disabled || isVerified} | ||
value={value[i]} | ||
aria-invalid={!!errorText} | ||
/> | ||
|
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.
For consistency, can we use the tailwind class
opacity-100
instead of astyle
prop here?