Skip to content
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

Add Code Password Reset to the Bootstrapper #343

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import { Main } from '@screens/main'
import { Login } from '@screens/auth/login'
import { SignUp } from '@screens/auth/sign-up'
import { Bounceable } from 'rn-bounceable'
import { ForgotPassword } from './forgot-password'

const Tab = createMaterialTopTabNavigator()

const tabs = [
{ name: 'home', label: 'Home', component: Main },
{ name: 'login', label: 'Login', component: Login },
{ name: 'signup', label: 'Signup', component: SignUp },
{ name: 'forgot-password', label: 'Forgot Password', component: ForgotPassword },
]

const TopTab = ({ navigation, state }: MaterialTopTabBarProps) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { MultiPlatformSafeAreaView } from '@components/multi-platform-safe-area-view'
import { BounceableWind } from '@components/styled'
import { TextFormField } from '@components/text-form-field'
import {
ForgotPasswordInput,
ForgotPasswordForm,
TForgotPasswordForm,
userApi,
} from '@services/user'
import { FormProvider, useTnForm } from '@thinknimble/tn-forms-react'
import { ScrollView, Text, View } from 'react-native'
import { getNavio } from '../routes'

const ForgotPasswordInner = () => {
const { form, overrideForm } = useTnForm<TForgotPasswordForm>()
const handleSubmit = async () => {
//TODO:
if (!form.isValid) {
const newForm = form.replicate() as TForgotPasswordForm
newForm.validate()
overrideForm(newForm)
} else {
try {
// HACK FOR TN-Forms
await userApi.csc.requestPasswordResetCode(form.value as { email: string })
getNavio().stacks.push('ResetPasswordStack')
} catch (e) {
console.log(e)
}
}
}
return (
<MultiPlatformSafeAreaView safeAreaClassName="h-full mt-5">
<View className="w-full content-center mx-auto py-10 bg-slate-200 rounded-lg items-center px-4">
<Text className="text-primary-bold text-black text-3xl">
Reset Password
</Text>
<ScrollView className="w-full" contentContainerClassName="self-start w-full">
<TextFormField field={form.email} keyboardType="email-address" autoCapitalize="none" />
</ScrollView>
<BounceableWind
contentContainerClassName="w-full pt-5"
onPress={handleSubmit}
disabled={!form.isValid}
>
<View className="rounded-lg bg-[#042642] w-full items-center py-2">
<Text className="text-primary-bold text-white text-lg">
Reset Password
</Text>
</View>
</BounceableWind>
</View>
</MultiPlatformSafeAreaView>
)
}

export const ForgotPassword = () => {
return (
<FormProvider<ForgotPasswordInput> formClass={ForgotPasswordForm}>
<ForgotPasswordInner />
</FormProvider>
)
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { Login } from './login'
export { SignUp } from './sign-up'
export { SignUp } from './sign-up'
export { ForgotPassword } from './forgot-password'
export { ResetPassword } from './reset-password'
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { MultiPlatformSafeAreaView } from '@components/multi-platform-safe-area-view'
import { BounceableWind } from '@components/styled'
import { TextFormField } from '@components/text-form-field'
import { ResetPasswordForm, ResetPasswordInput, TResetPasswordForm, userApi } from '@services/user'
import { FormProvider, useTnForm } from '@thinknimble/tn-forms-react'
import { ScrollView, Text, View } from 'react-native'
import { getNavio } from '../routes'
import { useAuth } from '@stores/auth'

const ResetPasswordInner = () => {
const { form, overrideForm } = useTnForm<TResetPasswordForm>()
const { changeToken, changeUserId } = useAuth.use.actions()
const handleSubmit = async () => {
//TODO:
if (!form.isValid) {
const newForm = form.replicate() as TResetPasswordForm
newForm.validate()
overrideForm(newForm)
} else {
try {
// HACK FOR TN-Forms
const res = await userApi.csc.resetPassword(
form.value as { email: string; code: string; password: string },
)
if (!res?.token) {
throw 'Missing token from response'
}
changeUserId(res.id)
changeToken(res.token)
getNavio().stacks.push('MainStack')
} catch (e) {
console.log(e)
}
}
}
return (
<MultiPlatformSafeAreaView safeAreaClassName="h-full mt-5">
<View className="w-full content-center mx-auto py-10 bg-slate-200 rounded-lg items-center px-4">
<Text className="text-primary-bold text-black text-3xl">
Reset Password
</Text>
<ScrollView className="w-full" contentContainerClassName="self-start w-full">
<TextFormField field={form.email} keyboardType="email-address" autoCapitalize="none" />
<TextFormField field={form.code} containerClassName="pt-4" />
<TextFormField field={form.password} secureTextEntry containerClassName="pt-4" />
<TextFormField field={form.confirmPassword} secureTextEntry containerClassName="pt-4" />
</ScrollView>
<BounceableWind
contentContainerClassName="w-full pt-5"
onPress={handleSubmit}
disabled={!form.isValid}
>
<View className="rounded-lg bg-[#042642] w-full items-center py-2">
<Text className="text-primary-bold text-white text-lg">
Reset Password
</Text>
</View>
</BounceableWind>
</View>
</MultiPlatformSafeAreaView>
)
}

export const ResetPassword = () => {
return (
<FormProvider<ResetPasswordInput> formClass={ResetPasswordForm}>
<ResetPasswordInner />
</FormProvider>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Platform } from 'react-native'
import { Navio } from 'rn-navio'
import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
import { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs'
import { Login, SignUp } from '@screens/auth'
import { ForgotPassword, Login, ResetPassword, SignUp } from '@screens/auth'
import { Main } from '@screens/main'
import { Auth } from '@screens/auth/auth'
import { DashboardScreen } from '@screens/dashboard'
Expand Down Expand Up @@ -30,11 +30,25 @@ export const tabDefaultOptions = (): BottomTabNavigationOptions => ({
})
// NAVIO
export const navio = Navio.build({
screens: { Auth, Login, SignUp, Main, DashboardScreen, ComponentsPreview, Settings, ContactUs, EditProfile },
screens: {
Auth,
Login,
SignUp,
Main,
DashboardScreen,
ComponentsPreview,
Settings,
ContactUs,
EditProfile,
ForgotPassword,
ResetPassword,
},
stacks: {
AuthStack: ['Auth'],
MainStack: ['DashboardScreen'],
SettingsStack: ['Settings', 'ContactUs', 'EditProfile'],
ForgotPasswordStack: ['ForgotPassword'],
ResetPasswordStack: ['ResetPassword'],
/**
* Set me as the root to see the components preview
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const login = createCustomServiceCall({

const requestPasswordResetCode = createCustomServiceCall({
inputShape: forgotPasswordShape,
cb: async ({ client, input }) => {
await client.get(`/password/reset/code/${input.email}/`)
cb: async ({ client, input, utils }) => {
await client.post(`/password/reset/`, utils.toApi(input))
},
})

Expand All @@ -25,7 +25,7 @@ const resetPassword = createCustomServiceCall({
cb: async ({ client, input, utils }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { email, ...rest } = utils.toApi(input)
const res = await client.post(`/password/reset/code/confirm/${input.email}/`, rest)
const res = await client.post(`/password/reset/confirm/${input.email}/`, rest)
return utils.fromApi(res.data)
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ import Form, {
placeholder: 'Verification Code',
type: 'number',
validators: [
new MinLengthValidator({ message: 'Please enter a valid 5 digit code', minLength: 5 }),
new MinLengthValidator({ message: 'Please enter a valid 7 digit code', minLength: 7 }),
],
})
static password = FormField.create({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export const customFonts = {
[`${baseFamily}-Medium` as const]: require(`../../assets/fonts/${baseFamily}-Medium.${fontFormat}`),
[`${baseFamily}-MediumItalic` as const]: require(`../../assets/fonts/${baseFamily}-MediumItalic.${fontFormat}`),
[`${baseFamily}-Regular` as const]: require(`../../assets/fonts/${baseFamily}-Regular.${fontFormat}`),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@ import { getErrorMessages } from 'src/utils/errors'

export const RequestPasswordResetInner = () => {
const [errorMessage, setErrorMessage] = useState<string[] | undefined>()
const [resetLinkSent, setResetLinkSent] = useState(false)
const { createFormFieldChangeHandler, form } = useTnForm<TEmailForgotPasswordForm>()
const navigate = useNavigate()

const { mutate: requestReset } = useMutation({
mutationFn: userApi.csc.requestPasswordReset,
onSuccess: (data) => {
setErrorMessage(undefined)
setResetLinkSent(true)
navigate('/password/reset/confirm/' + form.email.value)
},
onError(e: any) {
const errors = getErrorMessages(e)
Expand All @@ -42,58 +41,32 @@ export const RequestPasswordResetInner = () => {
return (
<AuthLayout title="Request Password Reset">
<div className="mt-6 sm:mx-auto sm:w-full sm:max-w-sm">
{resetLinkSent ? (
<>
<p className="text-md">
Your request has been submitted. If there is an account associated with the email
provided, you should receive an email momentarily with instructions to reset your
password.
</p>
<p className="text-md">
If you do not see the email in your main folder soon, please make sure to check your
spam folder.
</p>
<div className="pt-6">
<Button
onClick={() => {
navigate('/log-in')
}}
variant="primary"
>
Return to Login
</Button>
</div>
</>
) : (
<>
<form
onSubmit={(e) => {
e.preventDefault()
}}
className="flex flex-col gap-2"
>
<Input
placeholder="Enter email..."
onChange={(e) => createFormFieldChangeHandler(form.email)(e.target.value)}
value={form.email.value ?? ''}
data-cy="email"
id="id"
label="Email address"
/>
<ErrorsList errors={form.email.errors} />
<div className="mb-2">
<ErrorMessage>{errorMessage}</ErrorMessage>
</div>
<Button
onClick={handleRequest}
disabled={!form.isValid}
variant={form.isValid ? 'primary' : 'disabled'}
>
Request Password Reset
</Button>
</form>
</>
)}
<form
onSubmit={(e) => {
e.preventDefault()
}}
className="flex flex-col gap-2"
>
<Input
placeholder="Enter email..."
onChange={(e) => createFormFieldChangeHandler(form.email)(e.target.value)}
value={form.email.value ?? ''}
data-cy="email"
id="id"
label="Email address"
/>
<ErrorsList errors={form.email.errors} />
<div className="mb-2">
<ErrorMessage>{errorMessage}</ErrorMessage>
</div>
<Button
onClick={handleRequest}
disabled={!form.isValid}
variant={form.isValid ? 'primary' : 'disabled'}
>
Request Password Reset
</Button>
</form>
</div>
</AuthLayout>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { Button } from 'src/components/button'
import { ErrorMessage, ErrorsList } from 'src/components/errors'
import { PasswordInput } from 'src/components/password-input'
import { ResetPasswordForm, TResetPasswordForm, userApi } from 'src/services/user'
import { Input } from 'src/components/input'

export const ResetPasswordInner = () => {
const { form, createFormFieldChangeHandler, overrideForm } = useTnForm<TResetPasswordForm>()
const { userId, token } = useParams()
console.log(userId, token)
const { userEmail } = useParams()
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)

Expand All @@ -32,19 +32,19 @@ export const ResetPasswordInner = () => {
})

useEffect(() => {
if (token && userId) {
overrideForm(ResetPasswordForm.create({ token: token, uid: userId }) as TResetPasswordForm)
if (userEmail) {
overrideForm(ResetPasswordForm.create({ email: userEmail }) as TResetPasswordForm)
}
}, [overrideForm, token, userId])
}, [overrideForm, userEmail])

const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
console.log(form.value)

if (form.isValid) {
confirmResetPassword({
userId: form.value.uid!,
token: form.value.token!,
email: form.value.email!,
code: form.value.code!,
password: form.value.password!,
})
}
Expand All @@ -63,10 +63,18 @@ export const ResetPasswordInner = () => {
)
}
return (
<AuthLayout title="Reset password" description="Choose a new password">
<AuthLayout title="Reset password" description="Enter verification code sent to your email and choose a new password">
<div className="mt-6 sm:mx-auto sm:w-full sm:max-w-sm">
<form className="flex w-full flex-col gap-3" onSubmit={onSubmit}>
<section>
<Input
placeholder="Verification code"
onChange={(e) => createFormFieldChangeHandler(form.code)(e.target.value)}
value={form.code.value ?? ''}
data-cy="code"
id="code"
/>
<ErrorsList errors={form.code.errors} />
<PasswordInput
value={form.password.value}
onChange={(e) => createFormFieldChangeHandler(form.password)(e.target.value)}
Expand Down
Loading
Loading