Skip to content
Merged
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 @@ -3,7 +3,6 @@ import { renderScreen } from '../../../../../util/test/renderWithProvider';
import CardAuthentication from './CardAuthentication';
import Routes from '../../../../../constants/navigation/Routes';
import { CardAuthenticationSelectors } from './CardAuthentication.testIds';
import { CardLocation } from '../../types';
import { backgroundState } from '../../../../../util/test/initial-root-state';

// Mock whenEngineReady to prevent async polling after test teardown
Expand Down Expand Up @@ -282,7 +281,7 @@ describe('CardAuthentication Component', () => {
});

describe('Login Step - Login Functionality', () => {
it('calls login with correct parameters for international location', async () => {
it('calls login with correct parameters', async () => {
render();
const emailInput = screen.getByTestId('email-field');
const passwordInput = screen.getByTestId('password-field');
Expand All @@ -296,14 +295,13 @@ describe('CardAuthentication Component', () => {

await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith({
location: 'international',
email: 'test@example.com',
password: 'password123',
});
});
});

it('calls login with US location when selected', async () => {
it('calls login after selecting US location', async () => {
render();
const usBox = screen.getByTestId('us-location-box');
const emailInput = screen.getByTestId('email-field');
Expand All @@ -319,7 +317,6 @@ describe('CardAuthentication Component', () => {

await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith({
location: 'us' as CardLocation,
email: 'test@example.com',
password: 'password123',
});
Expand Down Expand Up @@ -383,7 +380,6 @@ describe('CardAuthentication Component', () => {

await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith({
location: 'international',
email: 'test@example.com',
password: 'password123',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import { useTheme } from '../../../../../util/theme';
import useCardProviderAuthentication from '../../hooks/useCardProviderAuthentication';
import { CardAuthenticationSelectors } from './CardAuthentication.testIds';
import Routes from '../../../../../constants/navigation/Routes';
import { CardLocation } from '../../types';
import { strings } from '../../../../../../locales/i18n';
import Logger from '../../../../../util/Logger';
import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import {
selectUserCardLocation,
setOnboardingId,
setUserCardLocation,
} from '../../../../../core/redux/slices/card';
Expand All @@ -53,7 +53,7 @@ const CardAuthentication = () => {
const [password, setPassword] = useState('');
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [location, setLocation] = useState<CardLocation>('international');
const location = useSelector(selectUserCardLocation);
const [otpData, setOtpData] = useState<{
userId: string;
maskedPhoneNumber?: string;
Expand Down Expand Up @@ -116,7 +116,6 @@ const CardAuthentication = () => {
try {
await sendOtpLogin({
userId: otpData.userId,
location,
});
// Reset countdown when OTP is sent
setResendCooldown(60);
Expand All @@ -127,7 +126,7 @@ const CardAuthentication = () => {

sendOtp();
}
}, [step, otpData?.userId, sendOtpLogin, location]);
}, [step, otpData?.userId, sendOtpLogin]);

// Cooldown timer effect
useEffect(() => {
Expand Down Expand Up @@ -173,7 +172,6 @@ const CardAuthentication = () => {
try {
setLoading(true);
const loginResponse = await login({
location,
email,
password,
...(otpCode ? { otpCode } : {}),
Expand All @@ -190,7 +188,6 @@ const CardAuthentication = () => {
}

if (loginResponse?.phase) {
dispatch(setUserCardLocation(location));
dispatch(setOnboardingId(loginResponse.userId));
navigation.reset({
index: 0,
Expand All @@ -217,7 +214,6 @@ const CardAuthentication = () => {
},
[
email,
location,
login,
password,
step,
Expand Down Expand Up @@ -253,13 +249,12 @@ const CardAuthentication = () => {
try {
await sendOtpLogin({
userId: otpData.userId,
location,
});
setResendCooldown(60);
} catch (err) {
Logger.log('CardAuthentication::Resend OTP failed', err);
}
}, [resendCooldown, otpData?.userId, sendOtpLogin, location, otpLoading]);
}, [resendCooldown, otpData?.userId, sendOtpLogin, otpLoading]);

const handleBackToLogin = useCallback(() => {
setStep('login');
Expand Down Expand Up @@ -367,7 +362,7 @@ const CardAuthentication = () => {
<>
<Box twClassName="flex-row justify-between gap-2">
<TouchableOpacity
onPress={() => setLocation('international')}
onPress={() => dispatch(setUserCardLocation('international'))}
style={tw.style(
`flex flex-col items-center justify-center flex-1 bg-background-muted rounded-lg ${location === 'international' ? 'border border-text-default' : ''}`,
)}
Expand All @@ -386,7 +381,7 @@ const CardAuthentication = () => {
</Box>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setLocation('us')}
onPress={() => dispatch(setUserCardLocation('us'))}
style={tw.style(
`flex flex-col items-center justify-center flex-1 bg-background-muted rounded-lg ${location === 'us' ? 'border border-text-default' : ''}`,
)}
Expand Down Expand Up @@ -465,14 +460,15 @@ const CardAuthentication = () => {
handlePasswordChange,
handleResendOtp,
isPasswordVisible,
location,
otpError,
otpLoading,
password,
performLogin,
resendCooldown,
step,
tw,
dispatch,
location,
],
);
const actions = useMemo(
Expand Down
37 changes: 37 additions & 0 deletions app/components/UI/Card/Views/CardHome/CardHome.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2529,6 +2529,43 @@ describe('CardHome Component', () => {
]);
});
});

it('completes full auth error cleanup flow including toast display', async () => {
// Given: authenticated user with authentication error
setupMockSelectors({ isAuthenticated: true });
mockIsAuthenticationError.mockReturnValue(true);
mockRemoveCardBaanxToken.mockResolvedValue(undefined);
setupLoadCardDataMock({
error: 'Token expired',
isAuthenticated: true,
});

// When: component renders with authentication error
render();

// Then: should complete full cleanup flow:
// 1. Remove token
await waitFor(() => {
expect(mockRemoveCardBaanxToken).toHaveBeenCalled();
});

// 2. Dispatch Redux actions
await waitFor(() => {
expect(mockDispatch).toHaveBeenCalledWith(
expect.objectContaining({ type: 'card/resetAuthenticatedData' }),
);
expect(mockDispatch).toHaveBeenCalledWith(
expect.objectContaining({ type: 'card/clearAllCache' }),
);
});

// 3. Navigate to authentication screen (this happens after toast is shown)
await waitFor(() => {
expect(StackActions.replace).toHaveBeenCalledWith(
Routes.CARD.AUTHENTICATION,
);
});
});
});

describe('KYC Status Verification', () => {
Expand Down
10 changes: 9 additions & 1 deletion app/components/UI/Card/Views/CardHome/CardHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,14 @@ const CardHome = () => {
dispatch(resetAuthenticatedData());
dispatch(clearAllCache());

toastRef?.current?.showToast({
variant: ToastVariants.Icon,
labelOptions: [
{ label: strings('card.card_home.authentication_error') },
],
hasNoTimeout: false,
iconName: IconName.Warning,
});
navigation.dispatch(StackActions.replace(Routes.CARD.AUTHENTICATION));
} catch (error) {
if (!isComponentUnmountedRef.current) {
Expand All @@ -776,7 +784,7 @@ const CardHome = () => {
};

handleAuthenticationError();
}, [cardError, dispatch, isAuthenticated, navigation]);
}, [cardError, dispatch, isAuthenticated, navigation, toastRef]);

useEffect(() => {
if (isSDKLoading) {
Expand Down
128 changes: 127 additions & 1 deletion app/components/UI/Card/components/Onboarding/Complete.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { StackActions, useNavigation } from '@react-navigation/native';
import {
StackActions,
useNavigation,
useRoute,
} from '@react-navigation/native';
import { useDispatch } from 'react-redux';
import Complete from './Complete';
import Routes from '../../../../../constants/navigation/Routes';
Expand All @@ -14,6 +18,7 @@ const mockStackReplace = jest.fn((routeName: string) => ({

jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
useRoute: jest.fn(),
StackActions: {
replace: jest.fn((routeName: string) => ({
type: 'REPLACE',
Expand Down Expand Up @@ -217,6 +222,11 @@ describe('Complete Component', () => {
dispatch: mockNavigationDispatch,
});

// Default: no route params
(useRoute as jest.Mock).mockReturnValue({
params: {},
});

(useDispatch as jest.Mock).mockReturnValue(mockDispatch);

const { useMetrics } = jest.requireMock('../../../../hooks/useMetrics');
Expand Down Expand Up @@ -517,4 +527,120 @@ describe('Complete Component', () => {
});
});
});

describe('Deep Link Navigation (nextDestination param)', () => {
it('navigates to PersonalDetails when nextDestination is personal_details', async () => {
(useRoute as jest.Mock).mockReturnValue({
params: { nextDestination: 'personal_details' },
});

const { getByTestId } = render(<Complete />);
const button = getByTestId('complete-confirm-button');
fireEvent.press(button);

await waitFor(() => {
expect(mockStackReplace).toHaveBeenCalledWith(
Routes.CARD.ONBOARDING.PERSONAL_DETAILS,
);
expect(mockNavigationDispatch).toHaveBeenCalledWith(
expect.objectContaining({
routeName: Routes.CARD.ONBOARDING.PERSONAL_DETAILS,
}),
);
});
});

it('does not reset onboarding state when navigating to PersonalDetails', async () => {
(useRoute as jest.Mock).mockReturnValue({
params: { nextDestination: 'personal_details' },
});

const { resetOnboardingState } = jest.requireMock(
'../../../../../core/redux/slices/card',
);

const { getByTestId } = render(<Complete />);
fireEvent.press(getByTestId('complete-confirm-button'));

await waitFor(() => {
expect(resetOnboardingState).not.toHaveBeenCalled();
});
});

it('navigates to SpendingLimit when nextDestination is card_home', async () => {
(useRoute as jest.Mock).mockReturnValue({
params: { nextDestination: 'card_home' },
});

const { getByTestId } = render(<Complete />);
const button = getByTestId('complete-confirm-button');
fireEvent.press(button);

await waitFor(() => {
expect(mockStackReplace).toHaveBeenCalledWith(
Routes.CARD.SPENDING_LIMIT,
{ flow: 'onboarding' },
);
expect(mockNavigationDispatch).toHaveBeenCalledWith(
expect.objectContaining({ routeName: Routes.CARD.SPENDING_LIMIT }),
);
});
});

it('resets onboarding state when nextDestination is card_home', async () => {
(useRoute as jest.Mock).mockReturnValue({
params: { nextDestination: 'card_home' },
});

const { resetOnboardingState } = jest.requireMock(
'../../../../../core/redux/slices/card',
);

const { getByTestId } = render(<Complete />);
fireEvent.press(getByTestId('complete-confirm-button'));

await waitFor(() => {
expect(mockDispatch).toHaveBeenCalledWith(resetOnboardingState());
});
});

it('does not check token when nextDestination is provided', async () => {
(useRoute as jest.Mock).mockReturnValue({
params: { nextDestination: 'card_home' },
});

const { getCardBaanxToken } = jest.requireMock(
'../../util/cardTokenVault',
);

const { getByTestId } = render(<Complete />);
fireEvent.press(getByTestId('complete-confirm-button'));

await waitFor(() => {
expect(getCardBaanxToken).not.toHaveBeenCalled();
});
});

it('falls back to default behavior when nextDestination is undefined', async () => {
(useRoute as jest.Mock).mockReturnValue({
params: {},
});

const { getCardBaanxToken } = jest.requireMock(
'../../util/cardTokenVault',
);
getCardBaanxToken.mockResolvedValue({
success: true,
tokenData: { accessToken: 'mock-token' },
});

const { getByTestId } = render(<Complete />);
fireEvent.press(getByTestId('complete-confirm-button'));

await waitFor(() => {
expect(getCardBaanxToken).toHaveBeenCalled();
expect(mockStackReplace).toHaveBeenCalledWith(Routes.CARD.HOME);
});
});
});
});
Loading
Loading