diff --git a/packages/authentication/modules/access/index.ts b/packages/authentication/modules/access/index.ts index 942bc185..b029e310 100644 --- a/packages/authentication/modules/access/index.ts +++ b/packages/authentication/modules/access/index.ts @@ -16,5 +16,5 @@ export type * from './useResetPassword/types' export { default as useSignUp } from './useSignUp' export type * from './useSignUp/types' -export { default as useChangeExpiredPassword } from './useChangeExpiredPassword' -export type * from './useChangeExpiredPassword/types' +export { default as useChangePassword } from './useChangePassword' +export type * from './useChangePassword/types' diff --git a/packages/authentication/modules/access/useChangeExpiredPassword/__tests__/useChangeExpiredPassword.test.tsx b/packages/authentication/modules/access/useChangePassword/__tests__/useChangePassword.test.tsx similarity index 72% rename from packages/authentication/modules/access/useChangeExpiredPassword/__tests__/useChangeExpiredPassword.test.tsx rename to packages/authentication/modules/access/useChangePassword/__tests__/useChangePassword.test.tsx index 2cf63513..42897cc8 100644 --- a/packages/authentication/modules/access/useChangeExpiredPassword/__tests__/useChangeExpiredPassword.test.tsx +++ b/packages/authentication/modules/access/useChangePassword/__tests__/useChangePassword.test.tsx @@ -7,13 +7,12 @@ import { import { z } from 'zod' -import useChangeExpiredPassword from '../index' +import useChangePassword from '../index' -describe('useChangeExpiredPassword', () => { +describe('useChangePassword', () => { const currentPassword = '1234' const password = '12345#Abcde' - const token = 'fake-token' - const changePasswordUrl = '/change-expired-password' + const changePasswordUrl = '/users/change-password' afterEach(() => { ;(global.fetch as jest.Mock).mockClear() // Clear the mock between tests @@ -27,7 +26,6 @@ describe('useChangeExpiredPassword', () => { response: { currentPassword, newPassword: password, - token, }, }) @@ -35,8 +33,7 @@ describe('useChangeExpiredPassword', () => { const { result } = renderHook( () => - useChangeExpiredPassword({ - token, + useChangePassword({ defaultValues: { currentPassword, newPassword: password, @@ -66,8 +63,7 @@ describe('useChangeExpiredPassword', () => { const { result } = renderHook( () => - useChangeExpiredPassword({ - token, + useChangePassword({ defaultValues: { currentPassword, newPassword: password, @@ -101,8 +97,7 @@ describe('useChangeExpiredPassword', () => { const { result } = renderHook( () => - useChangeExpiredPassword({ - token, + useChangePassword({ defaultValues: { currentPassword, newPassword: password, @@ -148,8 +143,7 @@ describe('useChangeExpiredPassword', () => { const { result } = renderHook( () => - useChangeExpiredPassword({ - token, + useChangePassword({ defaultValues: customDefaultValues, validationSchema: customValidationSchema, options: { @@ -168,3 +162,54 @@ describe('useChangeExpiredPassword', () => { expect(hasOnSuccessRan).toBe(true) }) }) + +describe('useChangePassword with token for expired passwords', () => { + const currentPassword = '1234' + const password = 'abcABC@123456' + const token = 'fake-token' + const changePasswordUrl = '/change-expired-password' + + afterEach(() => { + ;(global.fetch as jest.Mock).mockClear() // Clear the mock between tests + }) + + // This is just to ensure that running with token has the same behavior as running without token + test('should run onSuccess', async () => { + // Mock the fetch call with a success response for POST method + mockFetch(changePasswordUrl, { + method: 'POST', + status: 200, + response: { + currentPassword, + newPassword: password, + token, + }, + }) + + let hasOnSuccessRan = false + + const { result } = renderHook( + () => + useChangePassword({ + token, + defaultValues: { + currentPassword, + newPassword: password, + confirmNewPassword: password, + }, + options: { + onSuccess: () => { + hasOnSuccessRan = true + }, + }, + }), + { + wrapper: ComponentWithProviders, + }, + ) + + await result.current.form.handleSubmit() + + expect(hasOnSuccessRan).toBe(true) + }) +}) diff --git a/packages/authentication/modules/access/useChangeExpiredPassword/constants.ts b/packages/authentication/modules/access/useChangePassword/constants.ts similarity index 84% rename from packages/authentication/modules/access/useChangeExpiredPassword/constants.ts rename to packages/authentication/modules/access/useChangePassword/constants.ts index e21f4e8b..73851133 100644 --- a/packages/authentication/modules/access/useChangeExpiredPassword/constants.ts +++ b/packages/authentication/modules/access/useChangePassword/constants.ts @@ -2,7 +2,7 @@ import { PASSWORD_REGEX, ZOD_MESSAGE } from '@baseapp-frontend/utils' import { z } from 'zod' -import type { ChangeExpiredPasswordForm } from './types' +import type { ChangePasswordForm } from './types' export const DEFAULT_VALIDATION_SCHEMA = z .object({ @@ -17,7 +17,7 @@ export const DEFAULT_VALIDATION_SCHEMA = z path: ['confirmNewPassword'], }) -export const DEFAULT_INITIAL_VALUES: ChangeExpiredPasswordForm = { +export const DEFAULT_INITIAL_VALUES: ChangePasswordForm = { currentPassword: '', newPassword: '', confirmNewPassword: '', diff --git a/packages/authentication/modules/access/useChangeExpiredPassword/index.ts b/packages/authentication/modules/access/useChangePassword/index.ts similarity index 77% rename from packages/authentication/modules/access/useChangeExpiredPassword/index.ts rename to packages/authentication/modules/access/useChangePassword/index.ts index 87b8b104..feaaf1b7 100644 --- a/packages/authentication/modules/access/useChangeExpiredPassword/index.ts +++ b/packages/authentication/modules/access/useChangePassword/index.ts @@ -6,16 +6,16 @@ import { type SubmitHandler, useForm } from 'react-hook-form' import AuthApi from '../../../services/auth' import { DEFAULT_INITIAL_VALUES, DEFAULT_VALIDATION_SCHEMA } from './constants' -import type { ChangeExpiredPasswordForm, UseChangeExpiredPassword } from './types' +import type { ChangePasswordForm, UseChangePassword } from './types' -const useChangeExpiredPassword = ({ +const useChangePassword = ({ token, validationSchema = DEFAULT_VALIDATION_SCHEMA, defaultValues = DEFAULT_INITIAL_VALUES, ApiClass = AuthApi, enableFormApiErrors = true, options = {}, -}: UseChangeExpiredPassword) => { +}: UseChangePassword) => { const form = useForm({ defaultValues, resolver: zodResolver(validationSchema), @@ -24,7 +24,9 @@ const useChangeExpiredPassword = ({ const mutation = useMutation({ mutationFn: ({ currentPassword, newPassword }) => - ApiClass.changeExpiredPassword({ currentPassword, newPassword, token }), + token + ? ApiClass.changeExpiredPassword({ currentPassword, newPassword, token }) + : ApiClass.changePassword({ currentPassword, newPassword }), ...options, // needs to be placed below all overridable options onError: (err, variables, context) => { options?.onError?.(err, variables, context) @@ -37,7 +39,7 @@ const useChangeExpiredPassword = ({ }, }) - const handleSubmit: SubmitHandler = async (values) => { + const handleSubmit: SubmitHandler = async (values) => { try { await mutation.mutateAsync(values) } catch (error) { @@ -55,4 +57,4 @@ const useChangeExpiredPassword = ({ } } -export default useChangeExpiredPassword +export default useChangePassword diff --git a/packages/authentication/modules/access/useChangeExpiredPassword/types.ts b/packages/authentication/modules/access/useChangePassword/types.ts similarity index 55% rename from packages/authentication/modules/access/useChangeExpiredPassword/types.ts rename to packages/authentication/modules/access/useChangePassword/types.ts index e6a09e27..6488f866 100644 --- a/packages/authentication/modules/access/useChangeExpiredPassword/types.ts +++ b/packages/authentication/modules/access/useChangePassword/types.ts @@ -3,19 +3,19 @@ import { z } from 'zod' import AuthApi from '../../../services/auth' -type ApiClass = Pick +type ApiClass = Pick -export type ChangeExpiredPasswordForm = { +export type ChangePasswordForm = { currentPassword: string newPassword: string confirmNewPassword: string } -export interface UseChangeExpiredPassword { - token: string +export interface UseChangePassword { + token?: string validationSchema?: z.ZodObject | z.ZodEffects> - defaultValues?: ChangeExpiredPasswordForm - options?: UseMutationOptions + defaultValues?: ChangePasswordForm + options?: UseMutationOptions ApiClass?: ApiClass enableFormApiErrors?: boolean } diff --git a/packages/authentication/services/auth.ts b/packages/authentication/services/auth.ts index f689ee46..38016a36 100644 --- a/packages/authentication/services/auth.ts +++ b/packages/authentication/services/auth.ts @@ -2,6 +2,7 @@ import { baseAppFetch } from '@baseapp-frontend/utils' import type { ChangeExpiredPasswordRequest, + ChangePasswordRequest, ForgotPasswordRequest, LoginRequest, LoginResponse, @@ -26,6 +27,13 @@ export default class AuthApi { return baseAppFetch(`/register`, { method: 'POST', body: request }) } + static changePassword({ currentPassword, newPassword }: ChangePasswordRequest) { + return baseAppFetch('/users/change-password', { + method: 'POST', + body: { currentPassword, newPassword }, + }) + } + static changeExpiredPassword({ currentPassword, newPassword, diff --git a/packages/authentication/types/auth.ts b/packages/authentication/types/auth.ts index a64ac1bd..f12f8cfd 100644 --- a/packages/authentication/types/auth.ts +++ b/packages/authentication/types/auth.ts @@ -49,8 +49,11 @@ export interface CustomJWTKeyNames { refreshKeyName?: string } -export interface ChangeExpiredPasswordRequest { +export interface ChangePasswordRequest { currentPassword: string newPassword: string +} + +export interface ChangeExpiredPasswordRequest extends ChangePasswordRequest { token: string } diff --git a/packages/design-system/components/native/icons/AlertTriangleIcon/index.tsx b/packages/design-system/components/native/icons/AlertTriangleIcon/index.tsx new file mode 100644 index 00000000..8b374a56 --- /dev/null +++ b/packages/design-system/components/native/icons/AlertTriangleIcon/index.tsx @@ -0,0 +1,32 @@ +import { FC } from 'react' + +import Svg, { Path } from 'react-native-svg' + +import { useTheme } from '../../../../providers/native' +import { SvgIconProps } from '../types' + +const AlertTriangleIcon: FC = ({ + isActive = false, + color, + width = '24', + height = '25', + ...props +}) => { + const { colors } = useTheme() + + const defaultColor = color ?? colors.object.high + const svgColor = isActive ? colors.primary.high : defaultColor + + return ( + + + + ) +} + +export default AlertTriangleIcon diff --git a/packages/design-system/components/native/icons/index.ts b/packages/design-system/components/native/icons/index.ts index 1776fd12..44c522de 100644 --- a/packages/design-system/components/native/icons/index.ts +++ b/packages/design-system/components/native/icons/index.ts @@ -1,3 +1,4 @@ +export { default as AlertTriangleIcon } from './AlertTriangleIcon' export { default as BellIcon } from './BellIcon' export { default as BiometricsIcon } from './BiometricsIcon' export { default as BlockIcon } from './BlockIcon' diff --git a/packages/design-system/components/native/inputs/TextInput/index.tsx b/packages/design-system/components/native/inputs/TextInput/index.tsx index fdf8c1dd..d5071561 100644 --- a/packages/design-system/components/native/inputs/TextInput/index.tsx +++ b/packages/design-system/components/native/inputs/TextInput/index.tsx @@ -1,6 +1,7 @@ import { FC, useState } from 'react' import { Ionicons } from '@expo/vector-icons' +import { LayoutChangeEvent } from 'react-native' import { TextInput as PaperTextInput } from 'react-native-paper' import { useTheme } from '../../../../providers/native' @@ -14,8 +15,14 @@ const TextInput: FC = (props) => { const { disabled, helperText } = props const [isFocused, setIsFocused] = useState(false) + const [errorContainerWidth, setErrorContainerWidth] = useState(0) const theme = useTheme() + const onLayout = (event: LayoutChangeEvent) => { + const { width } = event.nativeEvent.layout + setErrorContainerWidth(width) + } + const outlinedStyles = createOutlinedStyles(theme, { isFocused, isError: !!helperText, @@ -23,7 +30,7 @@ const TextInput: FC = (props) => { }) return ( - + setIsFocused(true)} onBlur={() => setIsFocused(false)} @@ -32,7 +39,9 @@ const TextInput: FC = (props) => { {...props} /> {helperText && !disabled && ( - + // Had to do this adjustment to the error container width because the error text was overflowing the container + // The 12px subtraction is to account for the padding of the error container + {helperText} diff --git a/packages/design-system/components/native/inputs/TextInput/styles.ts b/packages/design-system/components/native/inputs/TextInput/styles.ts index 8509d73e..a958c473 100644 --- a/packages/design-system/components/native/inputs/TextInput/styles.ts +++ b/packages/design-system/components/native/inputs/TextInput/styles.ts @@ -53,7 +53,7 @@ export const styles = StyleSheet.create({ gap: 8, }, errorContainer: { - alignItems: 'center', + alignItems: 'flex-start', flexDirection: 'row', gap: 4, paddingLeft: 12,