-
Notifications
You must be signed in to change notification settings - Fork 583
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(DIA-785): fuse signup and login flows into a single user experie…
…nce (#10746) * adds a feature flag * uses the feature flag to display a new onboarding screen * uses a shared formik context for all steps * uses individual formik contexts for all steps * uses react-navigation to transition between steps * refactors the context to be smaller * adds placeholders for all of the steps * adds rough end-to-end sign up functionality * adds the end-to-end login with otp flow * adds rough end-to-end forgot password flow * adds email verification * begins to add social sign-in and -up * includes recaptcha in email step * addresses code review * polished validation of email step * adds os version to user agent string * gives each step its own formik context * tidied up a bit
- Loading branch information
Showing
12 changed files
with
1,187 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
src/app/Scenes/Onboarding/AuthenticationDialog/AuthenticationDialog.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { Flex, useTheme } from "@artsy/palette-mobile" | ||
import BottomSheet from "@gorhom/bottom-sheet" | ||
import { NavigationContainer, NavigationState } from "@react-navigation/native" | ||
import { createStackNavigator } from "@react-navigation/stack" | ||
import { EmailStep } from "app/Scenes/Onboarding/AuthenticationDialog/EmailStep" | ||
import { ForgotPasswordStep } from "app/Scenes/Onboarding/AuthenticationDialog/ForgotPasswordStep" | ||
import { LoginOTPStep } from "app/Scenes/Onboarding/AuthenticationDialog/LoginOTPStep" | ||
import { LoginPasswordStep } from "app/Scenes/Onboarding/AuthenticationDialog/LoginPasswordStep" | ||
import { SignUpNameStep } from "app/Scenes/Onboarding/AuthenticationDialog/SignUpNameStep" | ||
import { SignUpPasswordStep } from "app/Scenes/Onboarding/AuthenticationDialog/SignUpPasswordStep" | ||
import { WelcomeStep } from "app/Scenes/Onboarding/AuthenticationDialog/WelcomeStep" | ||
import { | ||
OnboardingHomeNavigationStack, | ||
OnboardingHomeStore, | ||
} from "app/Scenes/Onboarding/OnboardingHome" | ||
import React from "react" | ||
|
||
export const AuthenticationDialog: React.FC = () => { | ||
const setCurrentStep = OnboardingHomeStore.useStoreActions((actions) => actions.setCurrentStep) | ||
|
||
const { space } = useTheme() | ||
|
||
const handleStateChange = (state: NavigationState | undefined) => { | ||
const currentStep = state?.routes?.at(state.index)?.name | ||
|
||
if (currentStep) { | ||
setCurrentStep(currentStep) | ||
} else { | ||
setCurrentStep(undefined) | ||
} | ||
} | ||
|
||
return ( | ||
<Flex flex={1}> | ||
<BottomSheet | ||
snapPoints={["100%"]} | ||
detached | ||
enableContentPanningGesture={false} | ||
handleComponent={null} | ||
> | ||
<Flex style={{ borderRadius: space(2), overflow: "hidden", flex: 1 }}> | ||
<NavigationContainer independent onStateChange={handleStateChange}> | ||
<Stack.Navigator | ||
screenOptions={{ | ||
headerShown: false, | ||
gestureEnabled: false, | ||
cardStyle: { backgroundColor: "white" }, | ||
}} | ||
initialRouteName="WelcomeStep" | ||
> | ||
<Stack.Screen name="WelcomeStep" component={WelcomeStep} /> | ||
<Stack.Screen name="EmailStep" component={EmailStep} /> | ||
<Stack.Screen name="SignUpPasswordStep" component={SignUpPasswordStep} /> | ||
<Stack.Screen name="SignUpNameStep" component={SignUpNameStep} /> | ||
<Stack.Screen name="LoginPasswordStep" component={LoginPasswordStep} /> | ||
<Stack.Screen name="LoginOTPStep" component={LoginOTPStep} /> | ||
<Stack.Screen name="ForgotPasswordStep" component={ForgotPasswordStep} /> | ||
</Stack.Navigator> | ||
</NavigationContainer> | ||
</Flex> | ||
</BottomSheet> | ||
</Flex> | ||
) | ||
} | ||
|
||
const Stack = createStackNavigator<OnboardingHomeNavigationStack>() |
110 changes: 110 additions & 0 deletions
110
src/app/Scenes/Onboarding/AuthenticationDialog/EmailStep.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { BackButton, Button, Flex, Text, useTheme } from "@artsy/palette-mobile" | ||
import { BottomSheetScrollView } from "@gorhom/bottom-sheet" | ||
import { useNavigation } from "@react-navigation/native" | ||
import { StackNavigationProp, StackScreenProps } from "@react-navigation/stack" | ||
import { BottomSheetInput } from "app/Components/BottomSheetInput" | ||
import { useRecaptcha } from "app/Components/Recaptcha/Recaptcha" | ||
import { OnboardingHomeNavigationStack } from "app/Scenes/Onboarding/OnboardingHome" | ||
import { GlobalStore } from "app/store/GlobalStore" | ||
import { FormikProvider, useFormik, useFormikContext } from "formik" | ||
import { useEffect } from "react" | ||
import { Alert } from "react-native" | ||
import * as Yup from "yup" | ||
|
||
type EmailStepProps = StackScreenProps<OnboardingHomeNavigationStack, "EmailStep"> | ||
|
||
interface EmailStepFormValues { | ||
email: string | ||
recaptchaToken: string | null | ||
} | ||
|
||
export const EmailStep: React.FC<EmailStepProps> = ({ navigation }) => { | ||
const formik = useFormik<EmailStepFormValues>({ | ||
initialValues: { email: "", recaptchaToken: null }, | ||
onSubmit: async ({ email, recaptchaToken }, { setFieldValue }) => { | ||
if (!recaptchaToken) { | ||
Alert.alert("Something went wrong. Please try again, or contact [email protected]") | ||
return | ||
} | ||
|
||
const res = await GlobalStore.actions.auth.verifyUser({ email, recaptchaToken }) | ||
|
||
setFieldValue("recaptchaToken", null) | ||
|
||
if (res === "user_exists") { | ||
navigation.navigate("LoginPasswordStep", { email }) | ||
} else if (res === "user_does_not_exist") { | ||
navigation.navigate("SignUpPasswordStep", { email }) | ||
} else if (res === "something_went_wrong") { | ||
Alert.alert("Something went wrong. Please try again, or contact [email protected]") | ||
} | ||
}, | ||
validateOnMount: false, | ||
validationSchema: Yup.object().shape({ | ||
email: Yup.string() | ||
.email("Please provide a valid email address") | ||
.required("Email field is required"), | ||
}), | ||
}) | ||
|
||
return ( | ||
<BottomSheetScrollView> | ||
<FormikProvider value={formik}> | ||
<EmailStepForm /> | ||
</FormikProvider> | ||
</BottomSheetScrollView> | ||
) | ||
} | ||
|
||
const EmailStepForm: React.FC = () => { | ||
const { errors, handleChange, handleSubmit, isValid, setFieldValue } = | ||
useFormikContext<EmailStepFormValues>() | ||
|
||
const navigation = useNavigation<StackNavigationProp<OnboardingHomeNavigationStack>>() | ||
|
||
const { Recaptcha, token } = useRecaptcha({ source: "authentication", action: "verify_email" }) | ||
|
||
const { color, space } = useTheme() | ||
|
||
// TODO: reset recaptchaToken when the user navigates back | ||
useEffect(() => { | ||
setFieldValue("recaptchaToken", token) | ||
}, [setFieldValue, token]) | ||
|
||
const handleBackButtonPress = () => { | ||
navigation.goBack() | ||
} | ||
|
||
return ( | ||
<Flex padding={2} gap={space(1)}> | ||
<BackButton onPress={handleBackButtonPress} /> | ||
|
||
<Text variant="sm-display">Sign up or log in</Text> | ||
<Recaptcha /> | ||
|
||
<BottomSheetInput | ||
autoCapitalize="none" | ||
autoComplete="email" | ||
autoFocus={true} | ||
keyboardType="email-address" | ||
onChangeText={(text) => { | ||
handleChange("email")(text.trim()) | ||
}} | ||
blurOnSubmit={false} // This is needed to avoid UI jump when the user submits | ||
placeholderTextColor={color("black30")} | ||
title="Email" | ||
returnKeyType="next" | ||
spellCheck={false} | ||
autoCorrect={false} | ||
// We need to to set textContentType to username (instead of emailAddress) here | ||
// enable autofill of login details from the device keychain. | ||
textContentType="username" | ||
error={errors.email} | ||
/> | ||
|
||
<Button block width={100} onPress={handleSubmit} disabled={!isValid}> | ||
Continue | ||
</Button> | ||
</Flex> | ||
) | ||
} |
147 changes: 147 additions & 0 deletions
147
src/app/Scenes/Onboarding/AuthenticationDialog/ForgotPasswordStep.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { BackButton, Button, Flex, Spacer, Text, useTheme } from "@artsy/palette-mobile" | ||
import { BottomSheetScrollView } from "@gorhom/bottom-sheet" | ||
import { RouteProp, useNavigation, useRoute } from "@react-navigation/native" | ||
import { StackNavigationProp, StackScreenProps } from "@react-navigation/stack" | ||
import { BottomSheetInput } from "app/Components/BottomSheetInput" | ||
import { OnboardingNavigationStack } from "app/Scenes/Onboarding/Onboarding" | ||
import { OnboardingHomeNavigationStack } from "app/Scenes/Onboarding/OnboardingHome" | ||
import { GlobalStore } from "app/store/GlobalStore" | ||
import { FormikProvider, useFormik, useFormikContext } from "formik" | ||
import * as Yup from "yup" | ||
|
||
type ForgotPasswordStepProps = StackScreenProps<OnboardingHomeNavigationStack, "ForgotPasswordStep"> | ||
|
||
interface ForgotPasswordStepFormValues { | ||
email: string | ||
} | ||
|
||
export const ForgotPasswordStep: React.FC<ForgotPasswordStepProps> = ({ navigation }) => { | ||
const formik = useFormik<ForgotPasswordStepFormValues>({ | ||
initialValues: { email: "" }, | ||
onSubmit: async ({ email }, { setErrors }) => { | ||
const res = await GlobalStore.actions.auth.forgotPassword({ | ||
email, | ||
}) | ||
if (!res) { | ||
// For security purposes, we are returning a generic error message | ||
setErrors({ | ||
email: | ||
"Couldn’t send reset password link. Please try again, or contact [email protected]", | ||
}) | ||
} else { | ||
navigation.navigate("ForgotPasswordStep", { requestedPasswordReset: true }) | ||
} | ||
}, | ||
validationSchema: Yup.string().email("Please provide a valid email address"), | ||
}) | ||
|
||
return ( | ||
<BottomSheetScrollView> | ||
<FormikProvider value={formik}> | ||
<ForgotPasswordStepForm /> | ||
</FormikProvider> | ||
</BottomSheetScrollView> | ||
) | ||
} | ||
|
||
const ForgotPasswordStepForm: React.FC = () => { | ||
const { dirty, handleChange, handleSubmit, isSubmitting, isValid, validateForm, values } = | ||
useFormikContext<ForgotPasswordStepFormValues>() | ||
|
||
const navigation = useNavigation<StackNavigationProp<OnboardingNavigationStack>>() | ||
const route = useRoute<RouteProp<OnboardingHomeNavigationStack, "ForgotPasswordStep">>() | ||
|
||
const { color, space } = useTheme() | ||
|
||
const handleBackButtonPress = () => { | ||
navigation.goBack() | ||
} | ||
|
||
const requestedPasswordReset = route.params?.requestedPasswordReset | ||
|
||
return ( | ||
<Flex padding={2} gap={space(1)}> | ||
<BackButton onPress={handleBackButtonPress} /> | ||
<Flex flex={1} px={2} pt={6} justifyContent="flex-start"> | ||
<Text variant="lg-display">Forgot Password?</Text> | ||
|
||
<Text pt={0.5} color="black100" variant="xs"> | ||
Please enter the email address associated with your Artsy account to receive a reset link. | ||
</Text> | ||
|
||
<Spacer y={2} /> | ||
|
||
{!!requestedPasswordReset ? ( | ||
<Text color="blue100">Password reset link sent. Please check your email.</Text> | ||
) : ( | ||
<BottomSheetInput | ||
autoCapitalize="none" | ||
autoComplete="email" | ||
enableClearButton | ||
keyboardType="email-address" | ||
onChangeText={(text) => { | ||
handleChange("email")(text.trim()) | ||
}} | ||
onSubmitEditing={() => { | ||
if (dirty) { | ||
handleSubmit() | ||
} | ||
}} | ||
onBlur={() => { | ||
validateForm() | ||
}} | ||
blurOnSubmit={false} // This is needed to avoid UI jump when the user submits | ||
placeholder="Email address" | ||
placeholderTextColor={color("black30")} | ||
value={values.email} | ||
returnKeyType="done" | ||
spellCheck={false} | ||
autoCorrect={false} | ||
textContentType="emailAddress" | ||
testID="email-address" | ||
/> | ||
)} | ||
</Flex> | ||
|
||
<Flex px={2} paddingBottom={2}> | ||
{!!requestedPasswordReset ? ( | ||
<> | ||
<Button | ||
variant="fillDark" | ||
onPress={() => navigation.goBack()} | ||
block | ||
haptic="impactMedium" | ||
testID="returnToLoginButton" | ||
> | ||
Return to login | ||
</Button> | ||
<Spacer y={1} /> | ||
<Button | ||
onPress={handleSubmit} | ||
block | ||
haptic="impactMedium" | ||
disabled={!isValid || !dirty} | ||
loading={isSubmitting} | ||
testID="resetButton" | ||
variant="outline" | ||
> | ||
Send Again | ||
</Button> | ||
</> | ||
) : ( | ||
<Button | ||
onPress={handleSubmit} | ||
block | ||
variant="fillDark" | ||
haptic="impactMedium" | ||
disabled={!isValid || !dirty} | ||
loading={isSubmitting} | ||
testID="resetButton" | ||
> | ||
Send Reset Link | ||
</Button> | ||
)} | ||
</Flex> | ||
</Flex> | ||
) | ||
} |
Oops, something went wrong.