Skip to content

Commit b118813

Browse files
chore(runway): cherry-pick fix(card): cp-7.62.0 general UI issues (#24801)
1 parent a83bf8a commit b118813

20 files changed

Lines changed: 225 additions & 3910 deletions

app/components/UI/Card/Views/CardHome/CardHome.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ import CardWarningBox from '../../components/CardWarningBox/CardWarningBox';
7979
import { useIsSwapEnabledForPriorityToken } from '../../hooks/useIsSwapEnabledForPriorityToken';
8080
import { isAuthenticationError } from '../../util/isAuthenticationError';
8181
import { removeCardBaanxToken } from '../../util/cardTokenVault';
82-
import Logger from '../../../../../util/Logger';
8382
import useLoadCardData from '../../hooks/useLoadCardData';
8483
import { CardActions } from '../../util/metrics';
8584
import { isSolanaChainId } from '@metamask/bridge-controller';
@@ -456,10 +455,14 @@ const CardHome = () => {
456455
changeAssetAction();
457456
}
458457
} catch (error) {
459-
Logger.log('enableCardAction error', error);
458+
const errorMessage =
459+
error instanceof Error && error.message
460+
? error.message
461+
: strings('card.card_home.enable_card_error');
462+
460463
toastRef?.current?.showToast({
461464
variant: ToastVariants.Icon,
462-
labelOptions: [{ label: strings('card.card_home.enable_card_error') }],
465+
labelOptions: [{ label: errorMessage }],
463466
iconName: IconName.Danger,
464467
iconColor: theme.colors.error.default,
465468
backgroundColor: theme.colors.error.muted,

app/components/UI/Card/Views/CardWelcome/CardWelcome.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ const CardWelcome = () => {
123123
variant={TextVariant.BodyMDMedium}
124124
style={styles.notNowButtonText}
125125
>
126-
{strings('predict.gtm_content.not_now')}
126+
{strings('card.card_onboarding.not_now_button')}
127127
</Text>
128128
}
129129
/>

app/components/UI/Card/components/Onboarding/ConfirmEmail.test.tsx

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ const createTestStore = (initialState = {}) =>
387387

388388
describe('ConfirmEmail Component', () => {
389389
const mockNavigate = jest.fn();
390+
const mockReset = jest.fn();
390391
const mockUseNavigation = useNavigation as jest.MockedFunction<
391392
typeof useNavigation
392393
>;
@@ -404,6 +405,7 @@ describe('ConfirmEmail Component', () => {
404405

405406
mockUseNavigation.mockReturnValue({
406407
navigate: mockNavigate,
408+
reset: mockReset,
407409
} as never);
408410
mockUseParams.mockReturnValue({
409411
email: 'test@example.com',
@@ -1026,6 +1028,7 @@ describe('ConfirmEmail Component', () => {
10261028
confirmAction: expect.objectContaining({
10271029
label: 'Log in',
10281030
}),
1031+
onClose: expect.any(Function),
10291032
}),
10301033
});
10311034
});
@@ -1106,11 +1109,62 @@ describe('ConfirmEmail Component', () => {
11061109

11071110
expect(capturedOnPress).toEqual(expect.any(Function));
11081111

1109-
mockNavigate.mockClear();
1112+
mockReset.mockClear();
11101113

11111114
capturedOnPress?.();
11121115

1113-
expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.AUTHENTICATION);
1116+
expect(mockReset).toHaveBeenCalledWith({
1117+
index: 0,
1118+
routes: [{ name: Routes.CARD.AUTHENTICATION }],
1119+
});
1120+
});
1121+
1122+
it('navigates to authentication screen when modal is closed', async () => {
1123+
const store = createTestStore();
1124+
1125+
let capturedOnClose: (() => void) | undefined;
1126+
1127+
const mockVerifyEmailVerification = jest.fn().mockResolvedValue({
1128+
onboardingId: null,
1129+
hasAccount: true,
1130+
});
1131+
1132+
mockUseEmailVerificationVerify.mockReturnValue({
1133+
verifyEmailVerification: mockVerifyEmailVerification,
1134+
isLoading: false,
1135+
isError: false,
1136+
error: null,
1137+
reset: jest.fn(),
1138+
});
1139+
1140+
mockNavigate.mockImplementation((_route, params) => {
1141+
if (params?.params?.onClose) {
1142+
capturedOnClose = params.params.onClose;
1143+
}
1144+
});
1145+
1146+
const { getByTestId } = render(
1147+
<Provider store={store}>
1148+
<ConfirmEmail />
1149+
</Provider>,
1150+
);
1151+
1152+
const codeFieldInput = getByTestId('confirm-email-code-field');
1153+
1154+
await act(async () => {
1155+
fireEvent.changeText(codeFieldInput, '123456');
1156+
});
1157+
1158+
expect(capturedOnClose).toEqual(expect.any(Function));
1159+
1160+
mockReset.mockClear();
1161+
1162+
capturedOnClose?.();
1163+
1164+
expect(mockReset).toHaveBeenCalledWith({
1165+
index: 0,
1166+
routes: [{ name: Routes.CARD.AUTHENTICATION }],
1167+
});
11141168
});
11151169

11161170
it('does not navigate to confirm modal when onboardingId is returned', async () => {

app/components/UI/Card/components/Onboarding/ConfirmEmail.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ const ConfirmEmail = () => {
148148
dispatch(setOnboardingId(onboardingId));
149149
navigation.navigate(Routes.CARD.ONBOARDING.SET_PHONE_NUMBER);
150150
} else if (hasAccount) {
151+
const navigateToAuthentication = () => {
152+
navigation.reset({
153+
index: 0,
154+
routes: [{ name: Routes.CARD.AUTHENTICATION }],
155+
});
156+
};
157+
151158
navigation.navigate(Routes.CARD.MODALS.ID, {
152159
screen: Routes.CARD.MODALS.CONFIRM_MODAL,
153160
params: {
@@ -162,10 +169,9 @@ const ConfirmEmail = () => {
162169
label: strings(
163170
'card.card_onboarding.account_exists.confirm_button',
164171
),
165-
onPress: () => {
166-
navigation.navigate(Routes.CARD.AUTHENTICATION);
167-
},
172+
onPress: navigateToAuthentication,
168173
},
174+
onClose: navigateToAuthentication,
169175
icon: IconName.UserCheck,
170176
},
171177
});

app/components/UI/Card/components/Onboarding/ConfirmModal.test.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ const mockOnCloseBottomSheet = jest.fn((callback?: () => void) => {
2121
}
2222
});
2323

24+
// Store the onClose prop passed to BottomSheet
25+
let capturedOnCloseProp: (() => void) | undefined;
26+
const getCapturedOnCloseProp = () => capturedOnCloseProp;
27+
const setCapturedOnCloseProp = (fn: (() => void) | undefined) => {
28+
capturedOnCloseProp = fn;
29+
};
30+
2431
// Store reference for the mock to use (hoisting workaround)
2532
const getMockOnCloseBottomSheet = () => mockOnCloseBottomSheet;
2633

@@ -35,12 +42,19 @@ jest.mock(
3542
{
3643
children,
3744
testID,
45+
onClose,
3846
}: {
3947
children: React.ReactNode;
4048
testID?: string;
49+
onClose?: () => void;
4150
},
4251
ref: React.Ref<{ onCloseBottomSheet: (callback?: () => void) => void }>,
4352
) => {
53+
// Capture onClose prop for testing
54+
React.useEffect(() => {
55+
setCapturedOnCloseProp(onClose);
56+
}, [onClose]);
57+
4458
React.useImperativeHandle(ref, () => ({
4559
onCloseBottomSheet: (callback?: () => void) => {
4660
// Get the mock function at runtime to avoid hoisting issues
@@ -136,6 +150,7 @@ interface MockConfirmModalParams {
136150
onPress: jest.Mock;
137151
variant?: string;
138152
};
153+
onClose?: jest.Mock;
139154
}
140155

141156
const createMockParams = (
@@ -156,6 +171,7 @@ describe('ConfirmModal', () => {
156171
jest.clearAllMocks();
157172
mockStrings.mockClear();
158173
mockUseParams.mockReturnValue(createMockParams());
174+
setCapturedOnCloseProp(undefined);
159175
// Restore mock implementation since jest.resetAllMocks() clears it
160176
mockOnCloseBottomSheet.mockImplementation((callback?: () => void) => {
161177
if (callback) {
@@ -266,6 +282,46 @@ describe('ConfirmModal', () => {
266282

267283
expect(mockOnCloseBottomSheet).toHaveBeenCalledTimes(1);
268284
});
285+
286+
it('closes bottom sheet without callback when close button is pressed', () => {
287+
const mockOnClose = jest.fn();
288+
mockUseParams.mockReturnValue(
289+
createMockParams({
290+
onClose: mockOnClose,
291+
}),
292+
);
293+
294+
const { getByTestId } = render(<ConfirmModal />);
295+
const closeButton = getByTestId('confirm-modal-close-button');
296+
297+
fireEvent.press(closeButton);
298+
299+
// handleCancel should close without passing callback (onClose is handled by BottomSheet prop)
300+
expect(mockOnCloseBottomSheet).toHaveBeenCalledTimes(1);
301+
expect(mockOnCloseBottomSheet).toHaveBeenCalledWith(undefined);
302+
});
303+
304+
it('passes onClose callback to BottomSheet component', () => {
305+
const mockOnClose = jest.fn();
306+
mockUseParams.mockReturnValue(
307+
createMockParams({
308+
onClose: mockOnClose,
309+
}),
310+
);
311+
312+
render(<ConfirmModal />);
313+
314+
// Verify onClose prop was passed to BottomSheet
315+
expect(getCapturedOnCloseProp()).toBe(mockOnClose);
316+
});
317+
318+
it('does not pass onClose to BottomSheet when not provided', () => {
319+
mockUseParams.mockReturnValue(createMockParams());
320+
321+
render(<ConfirmModal />);
322+
323+
expect(getCapturedOnCloseProp()).toBeUndefined();
324+
});
269325
});
270326

271327
describe('Edge Cases', () => {

app/components/UI/Card/components/Onboarding/ConfirmModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ interface ConfirmModalParams {
2727
description: string;
2828
icon: IconName;
2929
confirmAction: ModalAction;
30+
onClose?: () => void;
3031
}
3132

3233
const ConfirmModal = () => {
33-
const { title, description, icon, confirmAction } =
34+
const { title, description, icon, confirmAction, onClose } =
3435
useParams<ConfirmModalParams>();
3536
const sheetRef = useRef<BottomSheetRef>(null);
3637

@@ -85,7 +86,7 @@ const ConfirmModal = () => {
8586
);
8687

8788
return (
88-
<BottomSheet ref={sheetRef} testID="confirm-modal">
89+
<BottomSheet ref={sheetRef} onClose={onClose} testID="confirm-modal">
8990
<Box
9091
flexDirection={BoxFlexDirection.Column}
9192
alignItems={BoxAlignItems.Center}

0 commit comments

Comments
 (0)