Skip to content

Commit

Permalink
fix(DIA-930): tidy-up login with OTP step [WIP] (#10954)
Browse files Browse the repository at this point in the history
* adds textContentType for password managers

* prevented users from switching code types for on_demand

* add title to otp step

* started writing tests

* change sentence case to title case
  • Loading branch information
iskounen authored Oct 16, 2024
1 parent 59aea94 commit 29ed098
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ const ForgotPasswordStepForm: React.FC = () => {
haptic="impactMedium"
testID="returnToLoginButton"
>
Return to login
Return to Login
</Button>

<Spacer y={1} />
Expand Down
193 changes: 100 additions & 93 deletions src/app/Scenes/Onboarding/Auth2/scenes/LoginOTPStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "app/Scenes/Onboarding/Auth2/hooks/useAuthNavigation"
import { useInputAutofocus } from "app/Scenes/Onboarding/Auth2/hooks/useInputAutofocus"
import { GlobalStore } from "app/store/GlobalStore"
import { Formik, useFormikContext } from "formik"
import { Formik } from "formik"
import { useRef, useState } from "react"
import * as Yup from "yup"

Expand All @@ -24,13 +24,22 @@ interface LoginOTPStepFormValues {
}

export const LoginOTPStep: React.FC = () => {
const [codeType, setCodeType] = useState<"authentication" | "recovery">("authentication")

const navigation = useAuthNavigation()
const screen = useAuthScreen()
const otpRef = useRef<Input>(null)

const { color } = useTheme()

useInputAutofocus({
screenName: "LoginOTPStep",
inputRef: otpRef,
})

return (
<Formik<LoginOTPStepFormValues>
initialValues={{ otp: "" }}
validateOnChange={true}
validateOnMount={false}
validationSchema={Yup.object().shape({
otp: Yup.string().required("This field is required"),
})}
Expand All @@ -54,101 +63,99 @@ export const LoginOTPStep: React.FC = () => {
}
}}
>
<LoginOTPStepForm />
</Formik>
)
}

const LoginOTPStepForm: React.FC = () => {
const [recoveryCodeMode, setRecoveryCodeMode] = useState(false)

const {
errors,
handleChange,
handleSubmit,
isSubmitting,
isValid,
setErrors,
validateForm,
values,
resetForm,
} = useFormikContext<LoginOTPStepFormValues>()
{({
errors,
handleChange,
handleSubmit,
isSubmitting,
isValid,
validateForm,
values,
resetForm,
}) => (
<Flex padding={2}>
<BackButton
onPress={() => {
navigation.goBack()
resetForm()
setCodeType("authentication")
}}
/>

<Spacer y={1} />

<Text variant="sm-display">Authentication Code</Text>

<Input
autoCapitalize="none"
autoComplete="one-time-code"
autoCorrect={false}
blurOnSubmit={false}
error={errors.otp}
keyboardType={codeType === "authentication" ? "numeric" : "ascii-capable"}
placeholder={
codeType === "authentication"
? "Enter an authentication code"
: "Enter a recovery code"
}
placeholderTextColor={color("black30")}
ref={otpRef}
returnKeyType="done"
title={codeType === "authentication" ? "Authentication code" : "Recovery code"}
value={values.otp}
textContentType="oneTimeCode"
onChangeText={(text) => {
handleChange("otp")(text)
}}
onBlur={() => validateForm()}
/>

<Spacer y={1} />

{screen.params?.otpMode === "on_demand" && (
<>
<Spacer y={2} />

<SimpleMessage>
Your safety and security are important to us. Please check your email for a one-time
authentication code to complete your login.
</SimpleMessage>
</>
)}

const navigation = useAuthNavigation()
const screen = useAuthScreen()
const otpRef = useRef<Input>(null)

const { color } = useTheme()

useInputAutofocus({
screenName: "LoginOTPStep",
inputRef: otpRef,
})
<Spacer y={2} />

const handleBackButtonPress = () => {
resetForm()
navigation.goBack()
setRecoveryCodeMode(false)
}
<Button
block
width="100%"
onPress={handleSubmit}
disabled={!isValid}
loading={isSubmitting}
>
Continue
</Button>

return (
<Flex padding={2}>
<BackButton onPress={handleBackButtonPress} />

<Input
autoCapitalize="none"
autoComplete="one-time-code"
autoCorrect={false}
blurOnSubmit={false}
error={errors.otp}
keyboardType={recoveryCodeMode ? "ascii-capable" : "numeric"}
placeholder={recoveryCodeMode ? "Enter a recovery code" : "Enter an authentication code"}
placeholderTextColor={color("black30")}
ref={otpRef}
returnKeyType="done"
title={recoveryCodeMode ? "Recovery code" : "Authentication code"}
value={values.otp}
onChangeText={(text) => {
// Hide error when the user starts to type again
if (errors.otp) {
setErrors({ otp: undefined })
validateForm()
}
handleChange("otp")(text)
}}
onBlur={() => validateForm()}
/>

<Spacer y={1} />

{screen.params?.otpMode === "on_demand" && (
<>
<Spacer y={2} />

<SimpleMessage testID="on_demand_message">
Your safety and security are important to us. Please check your email for a one-time
authentication code to complete your login.
</SimpleMessage>
</>
{screen.params?.otpMode === "standard" && (
<Touchable
onPress={() => {
if (codeType === "authentication") {
setCodeType("recovery")
} else {
setCodeType("authentication")
}
}}
>
<Text variant="xs" color="black60" underline>
{codeType === "authentication"
? "Enter recovery code instead"
: "Enter authentication code"}
</Text>
</Touchable>
)}
</Flex>
)}

<Spacer y={2} />

<Button block width="100%" onPress={handleSubmit} disabled={!isValid} loading={isSubmitting}>
Continue
</Button>

<Spacer y={2} />

<Touchable
onPress={() => {
setRecoveryCodeMode((mode) => !mode)
}}
>
<Text variant="xs" color="black60" underline>
{recoveryCodeMode ? "Enter authentication code" : "Enter recovery code instead"}
</Text>
</Touchable>
</Flex>
</Formik>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const LoginWelcomeStepForm: React.FC = () => {
ref={emailRef}
spellCheck={false}
keyboardType="email-address"
textContentType="none"
textContentType="username"
returnKeyType="next"
title="Email"
value={values.email}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { fireEvent, screen } from "@testing-library/react-native"
import { LoginOTPStep } from "app/Scenes/Onboarding/Auth2/scenes/LoginOTPStep"
import { renderWithWrappers } from "app/utils/tests/renderWithWrappers"

const mockUseAuthScreen = jest.fn()

jest.mock("app/Scenes/Onboarding/Auth2/hooks/useAuthNavigation", () => ({
useAuthNavigation: jest.fn(),
useAuthScreen: () => mockUseAuthScreen(),
}))

describe("LoginOTPStep", () => {
beforeEach(() => {
mockUseAuthScreen.mockReturnValue({ params: { otpMode: "standard" } })
})

it("renders correctly", () => {
renderWithWrappers(<LoginOTPStep />)

expect(screen.getByText("Authentication Code")).toBeDefined()
expect(screen.getByText("Authentication code")).toBeDefined()
})

it("allows the user to switch between authentication and recovery codes", () => {
renderWithWrappers(<LoginOTPStep />)

const recoveryCodeButton = screen.getByText("Enter recovery code instead")
expect(recoveryCodeButton).toBeDefined()

fireEvent.press(recoveryCodeButton)

expect(screen.getByText("Recovery code")).toBeDefined()
})

it("renders instructions when OTP mode is 'on_demand'", () => {
mockUseAuthScreen.mockReturnValue({ params: { otpMode: "on_demand" } })

renderWithWrappers(<LoginOTPStep />)

expect(
screen.getByText(
"Your safety and security are important to us. Please check your email for a one-time authentication code to complete your login."
)
).toBeDefined()
})
})

0 comments on commit 29ed098

Please sign in to comment.