This commit is contained in:
45 files changed
+1141
-906
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
@@ -13,6 +14,17 @@ jest.mock('@/utils', () => ({
|
||||
|
||||
const mockFetch = fetchWithTimeout as jest.Mock;
|
||||
|
||||
// FlatList spreads cell rendering over 50 ms batch timers; with real timers
|
||||
// they fire outside act() at random, with fake timers waitFor advances them
|
||||
// act-wrapped and deterministically
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
const event = {
|
||||
id: '1',
|
||||
title: 'Lørdagsquiz',
|
||||
@@ -88,10 +100,13 @@ it('renders an error state when the API is unreachable', async () => {
|
||||
expect(await findByText('Kunne ikke hente programmet...')).toBeTruthy();
|
||||
});
|
||||
|
||||
// stuck spinner bug, fix later
|
||||
it.failing('stops the refresh spinner and shows the error when refresh fails', async () => {
|
||||
it('stops the refresh spinner and shows the error when refresh fails', async () => {
|
||||
mockFetch.mockResolvedValueOnce({ json: async () => ({ events: [event] }) });
|
||||
const { result } = renderHook(() => useEventList());
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity } } });
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
const { result } = renderHook(() => useEventList(), { wrapper });
|
||||
await waitFor(() => expect(result.current.events).toHaveLength(1));
|
||||
|
||||
mockFetch.mockRejectedValue(new Error('network down'));
|
||||
|
||||
@@ -1,50 +1,16 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { EventType } from '@/types/data';
|
||||
import { fetchWithTimeout } from '@/utils';
|
||||
import { useEvents } from '@/queries/useEvents';
|
||||
|
||||
const useEventList = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [events, setEvents] = useState<EventType[]>([]);
|
||||
const [error, setError] = useState<Error | unknown | null>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
const fetchEvents = async () => {
|
||||
const url = `${process.env.EXPO_PUBLIC_EVENT_API_URL}?view=compact-app`;
|
||||
setLoading(true);
|
||||
|
||||
let response;
|
||||
let fetchedEvents;
|
||||
try {
|
||||
const res = await fetchWithTimeout(url);
|
||||
response = await res.json();
|
||||
fetchedEvents = response.events;
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setEvents(fetchedEvents);
|
||||
setError(response.error || null);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchEvents();
|
||||
}, []);
|
||||
const { data, error, isPending, isRefetching, refetch } = useEvents();
|
||||
|
||||
return {
|
||||
handleRefresh,
|
||||
handleRefresh: () => {
|
||||
refetch();
|
||||
},
|
||||
error,
|
||||
events,
|
||||
refreshing,
|
||||
loading,
|
||||
events: data ?? [],
|
||||
refreshing: isRefetching,
|
||||
loading: isPending,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { requestLogin } from '@/api';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks';
|
||||
import { useLoginMutation } from '@/queries/useLoginMutation';
|
||||
|
||||
interface LoginFormValues {
|
||||
email: string;
|
||||
@@ -12,12 +10,7 @@ interface LoginFormValues {
|
||||
|
||||
const useLogin = () => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isAuthenticated = useAppSelector((store) => store.isAuthenticated);
|
||||
const isLoggingIn = useAppSelector((store) => store.isLoggingIn);
|
||||
const loginError = useAppSelector((store) => store.loginError);
|
||||
const { mutate: login, isPending, error } = useLoginMutation();
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -27,17 +20,11 @@ const useLogin = () => {
|
||||
} = useForm<LoginFormValues>();
|
||||
|
||||
const onSubmit = handleSubmit(({ email, password }) => {
|
||||
dispatch(requestLogin(email, password));
|
||||
// goBack works since Login is always pushed from Membership
|
||||
login({ username: email, password }, { onSuccess: () => navigation.goBack() });
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
// navigation.goBack onLogin works since LoginScreen is allways navigated to from MembershipScreen
|
||||
navigation.goBack();
|
||||
}
|
||||
}, [isAuthenticated, navigation]);
|
||||
|
||||
return { onSubmit, isLoggingIn, isSubmitted, setFocus, control, errors: { ...errors }, loginError };
|
||||
return { onSubmit, isLoggingIn: isPending, isSubmitted, setFocus, control, errors: { ...errors }, loginError: error };
|
||||
};
|
||||
|
||||
export default useLogin;
|
||||
@@ -7,6 +7,12 @@ import Membership from '@/modules/membership/Membership';
|
||||
import { renderWithProviders, testUser } from '@/testUtils';
|
||||
import type { UserType } from '@/types/data';
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
jest.mock('@react-navigation/native', () => ({
|
||||
...jest.requireActual('@react-navigation/native'),
|
||||
useNavigation: () => ({ navigate: mockNavigate }),
|
||||
}));
|
||||
|
||||
// Must match PROOF_QUERY in useProof.tsx
|
||||
const PROOF_QUERY = gql`
|
||||
{
|
||||
@@ -36,14 +42,6 @@ it('shows the membership pitch when logged out', async () => {
|
||||
it('lets a logged in non-member pick a membership type and shows its price', async () => {
|
||||
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
||||
|
||||
// Serve the non-member from the fake API so the refetch on mount does not
|
||||
// clobber the preloaded state.
|
||||
(global.fetch as jest.Mock).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => nonMember,
|
||||
});
|
||||
|
||||
const { findByText, getByText } = renderWithProviders(<Membership />, {
|
||||
initialToken: 'test-token',
|
||||
initialUser: nonMember,
|
||||
@@ -81,14 +79,6 @@ it('lets a logged in non-member pick a membership type and shows its price', asy
|
||||
});
|
||||
|
||||
it('shows the membership proof when logged in', async () => {
|
||||
// Mounting Membership with a token triggers requestUserData; serve the
|
||||
// logged in user from the fake API so the refetch does not clobber state.
|
||||
(global.fetch as jest.Mock).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => testUser,
|
||||
});
|
||||
|
||||
const { findByText } = renderWithProviders(<Membership />, {
|
||||
initialToken: 'test-token',
|
||||
initialUser: testUser,
|
||||
@@ -126,9 +116,6 @@ it('refetches and renders updated user data on pull-to-refresh', async () => {
|
||||
const fetchMock = global.fetch as jest.Mock;
|
||||
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
||||
|
||||
// the refetch on mount still sees the non-member
|
||||
fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => nonMember });
|
||||
|
||||
const { findByText, UNSAFE_getByType } = renderWithProviders(<Membership />, {
|
||||
initialToken: 'test-token',
|
||||
initialUser: nonMember,
|
||||
@@ -136,7 +123,7 @@ it('refetches and renders updated user data on pull-to-refresh', async () => {
|
||||
});
|
||||
expect(await findByText('Velg medlemskapstype')).toBeTruthy();
|
||||
|
||||
// but by the next pull-to-refresh the membership has been bought elsewhere
|
||||
// the server now reports an active membership, bought elsewhere
|
||||
fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => testUser });
|
||||
fireEvent(UNSAFE_getByType(RefreshControl), 'refresh');
|
||||
|
||||
@@ -149,3 +136,31 @@ it('refetches and renders updated user data on pull-to-refresh', async () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('signs out when the stored token has been revoked', async () => {
|
||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: async () => ({ detail: 'Ugyldig token.' }),
|
||||
});
|
||||
|
||||
const { findByText } = renderWithProviders(<Membership />, {
|
||||
initialToken: 'revoked-token',
|
||||
apolloMocks,
|
||||
});
|
||||
|
||||
expect(await findByText('Bli medlem')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('logs out and shows the logged-out membership screen again', async () => {
|
||||
const { findByText } = renderWithProviders(<Membership />, {
|
||||
initialToken: 'test-token',
|
||||
initialUser: testUser,
|
||||
apolloMocks,
|
||||
});
|
||||
|
||||
fireEvent.press(await findByText('Logg ut'));
|
||||
|
||||
expect(await findByText('Bli medlem')).toBeTruthy();
|
||||
expect(mockNavigate).toHaveBeenCalledWith('UserStack', { screen: 'Membership' });
|
||||
});
|
||||
@@ -10,7 +10,8 @@ import useMembership from './useMembership';
|
||||
|
||||
const Membership = () => {
|
||||
const {
|
||||
isFetchingUserData,
|
||||
isPending,
|
||||
isRefetching,
|
||||
onRegisterPress,
|
||||
onLoginPress,
|
||||
onLogoutPress,
|
||||
@@ -19,21 +20,22 @@ const Membership = () => {
|
||||
fetchUser,
|
||||
} = useMembership();
|
||||
|
||||
if (isFetchingUserData) {
|
||||
return (
|
||||
<View style={theme.card}>
|
||||
<ActivityIndicator color={theme.colors.neufOrange} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
// Only the very first fetch blocks, refreshes keep the proof mounted
|
||||
if (isPending) {
|
||||
return (
|
||||
<View style={theme.card}>
|
||||
<ActivityIndicator color={theme.colors.neufOrange} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Proof
|
||||
onLogoutPress={onLogoutPress}
|
||||
fetchUser={fetchUser}
|
||||
isFetchingUserData={isFetchingUserData}
|
||||
isRefetching={isRefetching}
|
||||
onDeletePress={onDeletePress}
|
||||
/>
|
||||
<PrivacyButton />
|
||||
|
||||
@@ -63,7 +63,7 @@ const renderProof = ({
|
||||
mocks = apolloMocks,
|
||||
}: { user?: UserType; fetchUser?: () => void; mocks?: readonly MockedResponse[] } = {}) =>
|
||||
renderWithProviders(
|
||||
<Proof onLogoutPress={jest.fn()} fetchUser={fetchUser} isFetchingUserData={false} onDeletePress={jest.fn()} />,
|
||||
<Proof onLogoutPress={jest.fn()} fetchUser={fetchUser} isRefetching={false} onDeletePress={jest.fn()} />,
|
||||
{
|
||||
initialToken: 'test-token',
|
||||
initialUser: user,
|
||||
@@ -133,10 +133,10 @@ it('purchases the preselected membership through the stripe payment sheet', asyn
|
||||
|
||||
it('purchases the selected membership type, not the default', async () => {
|
||||
mockPaymentSheetResponse();
|
||||
const { findByText, getByText } = renderProof();
|
||||
const { findByText } = renderProof();
|
||||
|
||||
fireEvent.press(await findByText('Student'));
|
||||
fireEvent.press(getByText('Kjøp medlemskap (150 NOK)'));
|
||||
fireEvent.press(await findByText('Kjøp medlemskap (150 NOK)'));
|
||||
|
||||
expect(await findByText('Your order is confirmed!')).toBeTruthy();
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
|
||||
@@ -254,10 +254,10 @@ const NotificationToggle = ({ user }: { user: UserType }) => {
|
||||
interface ProofProps {
|
||||
onLogoutPress: () => void;
|
||||
fetchUser: () => void;
|
||||
isFetchingUserData: boolean;
|
||||
isRefetching: boolean;
|
||||
onDeletePress: () => void;
|
||||
}
|
||||
const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData, onDeletePress }: ProofProps) => {
|
||||
const Proof = ({ onLogoutPress, fetchUser, isRefetching, onDeletePress }: ProofProps) => {
|
||||
const confettiRef = useRef<ConfettiMethods>(null);
|
||||
const burstRef = useRef<PIConfettiMethods>(null);
|
||||
const confettiContainerRef = useRef<React.ComponentRef<typeof View>>(null);
|
||||
@@ -317,7 +317,7 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData, onDeletePress }:
|
||||
onRainEnd={onRainEnd}
|
||||
/>
|
||||
<View style={styles.container}>
|
||||
<ScrollView refreshControl={<RefreshControl refreshing={isFetchingUserData} onRefresh={fetchUser} />}>
|
||||
<ScrollView refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={fetchUser} />}>
|
||||
<View style={[theme.card, { paddingVertical: 16, backgroundColor: 'transparent' }]}>
|
||||
<MembershipName user={user} />
|
||||
<Logo user={user} />
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import React from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useEffect } from 'react';
|
||||
import { ActivityIndicator, ScrollView, Text, TextInput, View, StyleSheet } from 'react-native';
|
||||
|
||||
import { useAuth } from '@/auth/AuthContext';
|
||||
import Button from '@/components/Button';
|
||||
import FormErrors from '@/components/FormErrors';
|
||||
import { useAppSelector } from '@/hooks';
|
||||
import { useUser } from '@/queries/useUser';
|
||||
import theme from '@/theme';
|
||||
|
||||
import useUserDelete from './useUserDelete';
|
||||
|
||||
const UserDelete = () => {
|
||||
const [value, onChangeText] = React.useState('');
|
||||
const user = useAppSelector((state) => state.user);
|
||||
const navigation = useNavigation();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { user } = useUser();
|
||||
const { onPress, isDeletingUser, deleteUserError } = useUserDelete();
|
||||
|
||||
// Not a screen for logged-out users; this also leaves after a successful deletion
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
navigation.navigate('UserStack', { screen: 'Membership' });
|
||||
}
|
||||
}, [isAuthenticated, navigation]);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,44 +1,30 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
|
||||
import { logout } from '@/actions';
|
||||
import { requestUserData } from '@/api';
|
||||
import { useAppDispatch, useAppSelector, usePrevious } from '@/hooks';
|
||||
import { useAuth } from '@/auth/AuthContext';
|
||||
import { useUser } from '@/queries/useUser';
|
||||
|
||||
const useMembership = () => {
|
||||
const navigation = useNavigation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isAuthenticated = useAppSelector((store) => store.isAuthenticated);
|
||||
const isFetchingUserData = useAppSelector((store) => store.isFetchingUserData);
|
||||
const userToken = useAppSelector((store) => store.userToken);
|
||||
const prevUserToken = usePrevious(userToken);
|
||||
const { isAuthenticated, signOut } = useAuth();
|
||||
const { isPending, isRefetching, refetch } = useUser();
|
||||
|
||||
const onLogoutPress = () => {
|
||||
dispatch(logout());
|
||||
signOut();
|
||||
navigation.navigate('UserStack', { screen: 'Membership' });
|
||||
};
|
||||
const onRegisterPress = () => navigation.navigate('UserStack', { screen: 'UserRegister' });
|
||||
const onLoginPress = () => navigation.navigate('UserStack', { screen: 'Login' });
|
||||
const onDeletePress = () => navigation.navigate('UserStack', { screen: 'UserDelete' });
|
||||
const fetchUser = useCallback(() => {
|
||||
dispatch(requestUserData(userToken || ''));
|
||||
}, [dispatch, userToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userToken !== prevUserToken && userToken) {
|
||||
fetchUser();
|
||||
}
|
||||
}, [userToken, prevUserToken, fetchUser]);
|
||||
|
||||
return {
|
||||
isAuthenticated,
|
||||
isFetchingUserData,
|
||||
userToken,
|
||||
isPending,
|
||||
isRefetching,
|
||||
onLogoutPress,
|
||||
onRegisterPress,
|
||||
onDeletePress,
|
||||
onLoginPress,
|
||||
fetchUser,
|
||||
fetchUser: refetch,
|
||||
};
|
||||
};
|
||||
export default useMembership;
|
||||
@@ -3,9 +3,9 @@ import { useStripe } from '@stripe/stripe-react-native';
|
||||
import Constants from 'expo-constants';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { requestStripePaymentSheet } from '@/api';
|
||||
import { useAppSelector } from '@/hooks';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import { usePaymentSheetMutation } from '@/queries/usePaymentSheetMutation';
|
||||
import { useUser } from '@/queries/useUser';
|
||||
import { ToastProps } from '@/types/toast';
|
||||
|
||||
const PROOF_QUERY = gql`
|
||||
@@ -22,10 +22,10 @@ interface UseProofProps {
|
||||
}
|
||||
|
||||
const useProof = ({ fetchUser }: UseProofProps) => {
|
||||
const user = useAppSelector((state) => state.user);
|
||||
const userToken = useAppSelector((state) => state.userToken);
|
||||
const { user } = useUser();
|
||||
const { data, loading, error, refetch } = useQuery(PROOF_QUERY);
|
||||
const { initPaymentSheet, presentPaymentSheet } = useStripe();
|
||||
const { mutateAsync: createPaymentSheet } = usePaymentSheetMutation();
|
||||
const { scheduleMembershipExpiryNotification, hasPermission } = useNotifications();
|
||||
const [paymentLoading, setPaymentLoading] = useState(false);
|
||||
// Matches the preselected option in PurchaseButton's radio list
|
||||
@@ -56,10 +56,7 @@ const useProof = ({ fetchUser }: UseProofProps) => {
|
||||
};
|
||||
|
||||
const initializePaymentSheet = async () => {
|
||||
const { paymentIntent, ephemeralKey, customer } = await requestStripePaymentSheet(
|
||||
userToken as string,
|
||||
membershipTypeSlug,
|
||||
);
|
||||
const { paymentIntent, ephemeralKey, customer } = await createPaymentSheet(membershipTypeSlug);
|
||||
|
||||
const { error } = await initPaymentSheet({
|
||||
merchantDisplayName: 'Det Norske Studentersamfund',
|
||||
|
||||
@@ -1,32 +1,12 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { requestDeleteUser } from '@/api';
|
||||
import { useAppDispatch, useAppSelector, usePrevious } from '@/hooks';
|
||||
import { useDeleteUserMutation } from '@/queries/useDeleteUserMutation';
|
||||
|
||||
const useUserDelete = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation();
|
||||
const isAuthenticated = useAppSelector((store) => store.isAuthenticated);
|
||||
const isAuthenticatedPrev = usePrevious(isAuthenticated);
|
||||
const userToken = useAppSelector((store) => store.userToken);
|
||||
const isDeletingUser = useAppSelector((store) => store.isDeletingUser);
|
||||
const deleteUserError = useAppSelector((store) => store.deleteUserError);
|
||||
|
||||
const onPress = () => {
|
||||
dispatch(requestDeleteUser(userToken || ''));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticatedPrev !== isAuthenticated && !isAuthenticated) {
|
||||
navigation.navigate('UserStack', { screen: 'Membership' });
|
||||
}
|
||||
}, [isAuthenticatedPrev, isAuthenticated, navigation]);
|
||||
const { mutate: deleteAccount, isPending, error } = useDeleteUserMutation();
|
||||
|
||||
return {
|
||||
onPress,
|
||||
isDeletingUser,
|
||||
deleteUserError,
|
||||
onPress: () => deleteAccount(),
|
||||
isDeletingUser: isPending,
|
||||
deleteUserError: error,
|
||||
};
|
||||
};
|
||||
export default useUserDelete;
|
||||
@@ -1,29 +1,19 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { requestRegisterUser } from '@/api';
|
||||
import { useAppDispatch, useAppSelector, usePrevious } from '@/hooks';
|
||||
import { ApiError } from '@/errors';
|
||||
import { useRegisterUserMutation } from '@/queries/useRegisterUserMutation';
|
||||
import type { UserRegisterFormValues } from '@/types/forms';
|
||||
import { ToastProps } from '@/types/toast';
|
||||
|
||||
export interface UserRegisterFormValues {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
phone_number: string;
|
||||
password: string;
|
||||
}
|
||||
export type { UserRegisterFormValues };
|
||||
|
||||
const useUserRegister = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation();
|
||||
const isAuthenticated = useAppSelector((store) => store.isAuthenticated);
|
||||
const isRegisteringUser = useAppSelector((store) => store.isRegisteringUser);
|
||||
const registerError = useAppSelector((store) => store.registerError);
|
||||
const { mutate: register, isPending, error } = useRegisterUserMutation();
|
||||
const [toast, setToast] = useState<ToastProps | null>(null);
|
||||
|
||||
const isAuthenticatedPrev = usePrevious(isAuthenticated);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@@ -32,6 +22,7 @@ const useUserRegister = () => {
|
||||
setError,
|
||||
clearErrors,
|
||||
} = useForm<UserRegisterFormValues>();
|
||||
|
||||
const onSubmit = handleSubmit((values) => {
|
||||
clearErrors();
|
||||
if (!isValid) {
|
||||
@@ -44,43 +35,28 @@ const useUserRegister = () => {
|
||||
}
|
||||
setToast(null);
|
||||
|
||||
dispatch(requestRegisterUser(values));
|
||||
|
||||
// Display error messages from API on form fields
|
||||
if (!registerError) return;
|
||||
Object.keys(registerError).forEach((key) => {
|
||||
const errorMessages = registerError[key];
|
||||
if (!errorMessages || !Array.isArray(errorMessages)) {
|
||||
return;
|
||||
}
|
||||
setError(key as keyof UserRegisterFormValues, { type: 'server', message: errorMessages.join('\n') });
|
||||
register(values, {
|
||||
onSuccess: () => navigation.goBack(),
|
||||
onError: (registerError) => {
|
||||
// Map DRF field errors onto the matching form fields
|
||||
if (!(registerError instanceof ApiError)) {
|
||||
return;
|
||||
}
|
||||
Object.entries(registerError.fields).forEach(([field, messages]) => {
|
||||
if (Array.isArray(messages)) {
|
||||
setError(field as keyof UserRegisterFormValues, { type: 'server', message: messages.join('\n') });
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticatedPrev !== isAuthenticated && isAuthenticated) {
|
||||
navigation.goBack();
|
||||
}
|
||||
}, [isAuthenticatedPrev, isAuthenticated, navigation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSubmitted || !registerError) return;
|
||||
clearErrors();
|
||||
Object.keys(registerError).forEach((key) => {
|
||||
const errorMessages = registerError[key];
|
||||
if (!errorMessages || !Array.isArray(errorMessages)) {
|
||||
return;
|
||||
}
|
||||
setError(key as keyof UserRegisterFormValues, { type: 'server', message: errorMessages.join('\n') });
|
||||
});
|
||||
}, [registerError, isSubmitted, clearErrors, setError]);
|
||||
|
||||
return {
|
||||
isRegisteringUser,
|
||||
isRegisteringUser: isPending,
|
||||
onSubmit,
|
||||
isValid,
|
||||
isSubmitted,
|
||||
registerError,
|
||||
registerError: error,
|
||||
control,
|
||||
errors,
|
||||
setFocus,
|
||||
|
||||
Reference in new issue
Block a user