Skip to content
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
1e9d3c2
refactor(card): remove cardGeolocation property and selectors
Brunonascdev Mar 20, 2026
8730edb
feat(card): solve geolocationLocation undefined start
Brunonascdev Mar 20, 2026
801da7a
feat(card): wip - card feature flag removal
Brunonascdev Mar 23, 2026
8432456
merge with main
Brunonascdev Mar 25, 2026
80fed41
feat(card): remove feature flags
Brunonascdev Mar 25, 2026
a080d47
merge with main
Brunonascdev Mar 26, 2026
2a7f2b1
test(card): fix handleLocalAuthentication test file
Brunonascdev Mar 26, 2026
e8ed0bc
feat(card): remove the iscardenabled property from sdk
Brunonascdev Mar 26, 2026
ba1cd03
test(card): fix AccountsMenu failing test
Brunonascdev Mar 26, 2026
ecb8081
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Mar 26, 2026
bce33b8
feat(card): add Card Sign-up Waitlist
Brunonascdev Mar 26, 2026
0e4195c
test(card): refactor EarnRewardsPreview to always display card banner
Brunonascdev Mar 26, 2026
bc9d2e1
test(card): remove ununsed mock
Brunonascdev Mar 26, 2026
64237ac
feat(card): fix UK users Card display on Rewards
Brunonascdev Mar 27, 2026
2565249
[skip ci] Bump version number to 4213
metamaskbot Mar 27, 2026
ef2d3ba
merge with main
Brunonascdev Mar 31, 2026
1c43ade
merge with main
Brunonascdev Mar 31, 2026
84e5789
revert version bump
Brunonascdev Mar 31, 2026
4136f04
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Mar 31, 2026
9bd6ff7
feat(card): wip - migrate unauthenticated mode to card controller
Brunonascdev Mar 31, 2026
21fbab2
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Mar 31, 2026
ca7b77a
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Mar 31, 2026
fe4a2f5
feat(card): migrate unauthenticated state to controller
Brunonascdev Apr 1, 2026
712bfb8
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Apr 1, 2026
2be7308
Merge branch 'main' into feat/card-feature-flag-refactor
Brunonascdev Apr 1, 2026
db153b8
feat(card): fix duplicated calls
Brunonascdev Apr 1, 2026
52576e0
Merge branch 'feat/card-feature-flag-refactor' of github.com:MetaMask…
Brunonascdev Apr 1, 2026
9e86a61
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Apr 1, 2026
b48381b
feat(card): lint issue and failing test
Brunonascdev Apr 1, 2026
963a8cb
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Apr 1, 2026
9a04a86
Merge branch 'feat/card-feature-flag-refactor' into feat/migrate-unau…
Brunonascdev Apr 1, 2026
30a5f3f
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Apr 2, 2026
b81d56b
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Apr 2, 2026
0e7eb17
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Apr 2, 2026
d8c9569
feat(card): migrate user card location to controller
Brunonascdev Apr 2, 2026
c1f25fc
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Apr 3, 2026
7793017
feat(card): merge with main
Brunonascdev Apr 3, 2026
dcbfdd5
feat(card): use selectIsAuthenticatedCard from controller instead of …
Brunonascdev Apr 3, 2026
29c79be
feat(card): remove authenticated references from redux and use contro…
Brunonascdev Apr 3, 2026
d2b481d
fix(card): failing tests and lint issues
Brunonascdev Apr 3, 2026
d0374c3
fix(card): failing SetPhoneNumber.test.tsx test
Brunonascdev Apr 3, 2026
acf5906
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Apr 3, 2026
20afa25
feat(card): remove dispatch on CardHome
Brunonascdev Apr 3, 2026
2445ad4
fix(card): authentication/onboarding loop in some cases
Brunonascdev Apr 4, 2026
30b785f
feat(card): remove redundant user on useEffect
Brunonascdev Apr 4, 2026
cba6ac1
feat(card): fix wrong SDK mention and catch blocks on controller refr…
Brunonascdev Apr 4, 2026
21da1a8
feat(card): update CardAuthentication snapshot
Brunonascdev Apr 4, 2026
0e74d76
feat(card): fix nullable location and unnecessary variable on useEffect
Brunonascdev Apr 4, 2026
cec9eda
feat(card): fix wrong mock and international fallback
Brunonascdev Apr 4, 2026
3befe57
fix(card): use effectiveLocation to avoid SDK reloads
Brunonascdev Apr 4, 2026
22fb767
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Apr 6, 2026
0d0b9b2
feat(card): update CardAuthentication snapshot
Brunonascdev Apr 6, 2026
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
2 changes: 0 additions & 2 deletions app/components/Nav/Main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
} from '../../hooks/useNetworksByNamespace/useNetworksByNamespace';
import { useNetworkSelection } from '../../hooks/useNetworkSelection/useNetworkSelection';
import { useIsOnBridgeRoute } from '../../UI/Bridge/hooks/useIsOnBridgeRoute';
import { CardVerification } from '../../UI/Card/sdk';

const Stack = createStackNavigator();

Expand Down Expand Up @@ -168,7 +167,7 @@
} else {
props.setInfuraAvailabilityNotBlocked();
}
// eslint-disable-next-line react-hooks/exhaustive-deps

Check warning on line 170 in app/components/Nav/Main/index.js

View workflow job for this annotation

GitHub Actions / Run `lint`

React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior
}, [
props.navigation,
props.providerType,
Expand Down Expand Up @@ -379,7 +378,7 @@
removeConnectionStatusListener.current &&
removeConnectionStatusListener.current();
};
// eslint-disable-next-line react-hooks/exhaustive-deps

Check warning on line 381 in app/components/Nav/Main/index.js

View workflow job for this annotation

GitHub Actions / Run `lint`

React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior
}, [connectionChangeHandler]);

const openDeprecatedNetworksArticle = () => {
Expand Down Expand Up @@ -420,7 +419,6 @@
<FadeOutOverlay />
<Notification navigation={props.navigation} />
<RampOrders />
<CardVerification />
<EarnTransactionMonitor />
{renderDeprecatedNetworkAlert(
props.chainId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ jest.mock('../../../../../util/analytics/whenEngineReady', () => ({
default: jest.fn().mockResolvedValue(undefined),
}));

jest.mock('../../../../../core/Engine', () => ({
__esModule: true,
default: {
context: {
CardController: {
setUserLocation: jest.fn(),
},
},
},
}));

const mockNavigate = jest.fn();
const mockGoBack = jest.fn();
const mockReset = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,9 @@ import Logger from '../../../../../util/Logger';
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
import { MetaMetricsEvents } from '../../../../../core/Analytics';
import { useDispatch, useSelector } from 'react-redux';
import {
selectUserCardLocation,
setOnboardingId,
setUserCardLocation,
} from '../../../../../core/redux/slices/card';
import { setOnboardingId } from '../../../../../core/redux/slices/card';
import { selectCardUserLocation } from '../../../../../selectors/cardController';
import Engine from '../../../../../core/Engine';
import { CardActions, CardScreens } from '../../util/metrics';
import OnboardingStep from '../../components/Onboarding/OnboardingStep';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
Expand All @@ -49,7 +47,7 @@ const CardAuthentication = () => {
const [password, setPassword] = useState('');
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const [loading, setLoading] = useState(false);
const location = useSelector(selectUserCardLocation);
const location = useSelector(selectCardUserLocation);
const [otpData, setOtpData] = useState<{
userId: string;
maskedPhoneNumber?: string;
Expand Down Expand Up @@ -357,7 +355,9 @@ const CardAuthentication = () => {
<>
<Box twClassName="flex-row justify-between gap-2">
<TouchableOpacity
onPress={() => dispatch(setUserCardLocation('international'))}
onPress={() =>
Engine.context.CardController.setUserLocation('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 @@ -376,7 +376,9 @@ const CardAuthentication = () => {
</Box>
</TouchableOpacity>
<TouchableOpacity
onPress={() => dispatch(setUserCardLocation('us'))}
onPress={() =>
Engine.context.CardController.setUserLocation('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 @@ -460,7 +462,6 @@ const CardAuthentication = () => {
resendCooldown,
step,
tw,
dispatch,
location,
],
);
Expand Down
49 changes: 10 additions & 39 deletions app/components/UI/Card/Views/CardHome/CardHome.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ import {
} from '../../../../../selectors/featureFlagController/deposit';
import { selectMetalCardCheckoutFeatureFlag } from '../../../../../selectors/featureFlagController/card';
import {
selectIsCardAuthenticated,
selectCardholderAccounts,
selectIsAuthenticatedCard,
selectUserCardLocation,
} from '../../../../../core/redux/slices/card';
selectCardUserLocation,
} from '../../../../../selectors/cardController';
import { useIsSwapEnabledForPriorityToken } from '../../hooks/useIsSwapEnabledForPriorityToken';
import useCardDetailsToken from '../../hooks/useCardDetailsToken';
import useCardPinToken from '../../hooks/useCardPinToken';
Expand Down Expand Up @@ -376,9 +376,6 @@ jest.mock('../../util/cardTokenVault', () => ({
}));

// Mock Redux card actions
const mockResetAuthenticatedData = jest.fn(() => ({
type: 'card/resetAuthenticatedData',
}));
const mockClearAllCache = jest.fn(() => ({
type: 'card/clearAllCache',
}));
Expand All @@ -388,7 +385,6 @@ jest.mock('../../../../../core/redux/slices/card', () => {
);
return {
...actualModule,
resetAuthenticatedData: () => mockResetAuthenticatedData(),
clearAllCache: () => mockClearAllCache(),
};
});
Expand Down Expand Up @@ -422,6 +418,9 @@ jest.mock('../../../../../core/Engine', () => ({
getAccountByAddress: jest.fn().mockReturnValue({ id: 'account-id' }),
setSelectedAccount: jest.fn(),
},
CardController: {
validateAndRefreshSession: jest.fn().mockResolvedValue(undefined),
},
},
},
}));
Expand Down Expand Up @@ -601,8 +600,8 @@ function setupMockSelectors(
if (selector === selectDepositMinimumVersionFlag)
return config.depositMinVersion;
if (selector === selectCardholderAccounts) return config.cardholderAccounts;
if (selector === selectIsAuthenticatedCard) return config.isAuthenticated;
if (selector === selectUserCardLocation) return config.userLocation;
if (selector === selectIsCardAuthenticated) return config.isAuthenticated;
if (selector === selectCardUserLocation) return config.userLocation;
if (selector === selectMetalCardCheckoutFeatureFlag)
return config.isMetalCardCheckoutEnabled;

Expand Down Expand Up @@ -725,7 +724,6 @@ describe('CardHome Component', () => {
mockIsAuthenticationError.mockReturnValue(false); // Default to no auth error
mockRemoveCardBaanxToken.mockClear();
mockRemoveCardBaanxToken.mockResolvedValue(undefined);
mockResetAuthenticatedData.mockClear();
mockClearAllCache.mockClear();
mockNavigationDispatch.mockClear();

Expand Down Expand Up @@ -2619,9 +2617,6 @@ describe('CardHome Component', () => {
});

await waitFor(() => {
expect(mockDispatch).toHaveBeenCalledWith(
expect.objectContaining({ type: 'card/resetAuthenticatedData' }),
);
expect(mockRemoveQueries).toHaveBeenCalled();
});

Expand Down Expand Up @@ -2652,7 +2647,6 @@ describe('CardHome Component', () => {

// Then: should not trigger authentication error handling
expect(mockRemoveCardBaanxToken).not.toHaveBeenCalled();
expect(mockResetAuthenticatedData).not.toHaveBeenCalled();
expect(mockRemoveQueries).not.toHaveBeenCalled();
expect(mockNavigationDispatch).not.toHaveBeenCalledWith(
expect.objectContaining({ type: 'REPLACE' }),
Expand All @@ -2673,7 +2667,6 @@ describe('CardHome Component', () => {

// Then: should not trigger authentication error handling
expect(mockRemoveCardBaanxToken).not.toHaveBeenCalled();
expect(mockResetAuthenticatedData).not.toHaveBeenCalled();
expect(mockRemoveQueries).not.toHaveBeenCalled();
});

Expand All @@ -2691,7 +2684,6 @@ describe('CardHome Component', () => {

// Then: should not trigger authentication error handling
expect(mockRemoveCardBaanxToken).not.toHaveBeenCalled();
expect(mockResetAuthenticatedData).not.toHaveBeenCalled();
expect(mockRemoveQueries).not.toHaveBeenCalled();
expect(mockNavigationDispatch).not.toHaveBeenCalledWith(
expect.objectContaining({ type: 'REPLACE' }),
Expand Down Expand Up @@ -2750,9 +2742,6 @@ describe('CardHome Component', () => {
});

await waitFor(() => {
expect(mockDispatch).toHaveBeenCalledWith(
expect.objectContaining({ type: 'card/resetAuthenticatedData' }),
);
expect(mockRemoveQueries).toHaveBeenCalled();
});
});
Expand Down Expand Up @@ -2857,9 +2846,6 @@ describe('CardHome Component', () => {
// Then: should clear auth state and navigate to welcome
await waitFor(() => {
expect(mockRemoveCardBaanxToken).toHaveBeenCalled();
expect(mockDispatch).toHaveBeenCalledWith(
expect.objectContaining({ type: 'card/resetAuthenticatedData' }),
);
expect(mockRemoveQueries).toHaveBeenCalled();
expect(StackActions.replace).toHaveBeenCalledWith(
Routes.CARD.AUTHENTICATION,
Expand All @@ -2877,13 +2863,6 @@ describe('CardHome Component', () => {
callOrder.push('removeToken');
});

mockDispatch.mockImplementation((action) => {
if (action.type === 'card/resetAuthenticatedData') {
callOrder.push('resetAuth');
}
return action;
});

mockRemoveQueries.mockImplementation(() => {
callOrder.push('removeQueries');
});
Expand All @@ -2902,12 +2881,7 @@ describe('CardHome Component', () => {

// Then: operations should execute in correct order
await waitFor(() => {
expect(callOrder).toEqual([
'removeToken',
'resetAuth',
'removeQueries',
'navigate',
]);
expect(callOrder).toEqual(['removeToken', 'removeQueries', 'navigate']);
});
});

Expand All @@ -2930,11 +2904,8 @@ describe('CardHome Component', () => {
expect(mockRemoveCardBaanxToken).toHaveBeenCalled();
});

// 2. Dispatch Redux actions and clear query cache
// 2. Clear query cache
await waitFor(() => {
expect(mockDispatch).toHaveBeenCalledWith(
expect.objectContaining({ type: 'card/resetAuthenticatedData' }),
);
expect(mockRemoveQueries).toHaveBeenCalled();
});

Expand Down
15 changes: 8 additions & 7 deletions app/components/UI/Card/Views/CardHome/CardHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,7 @@ import {
} from '../../constants';
import { useCardSDK } from '../../sdk';
import Routes from '../../../../../constants/navigation/Routes';
import {
resetAuthenticatedData,
selectUserCardLocation,
} from '../../../../../core/redux/slices/card';
import { selectCardUserLocation } from '../../../../../selectors/cardController';
import { cardQueries } from '../../queries';
import { selectMetalCardCheckoutFeatureFlag } from '../../../../../selectors/featureFlagController/card';
import CardMessageBox from '../../components/CardMessageBox/CardMessageBox';
Expand Down Expand Up @@ -144,7 +141,7 @@ const CardHome = () => {
const [isHandlingAuthError, setIsHandlingAuthError] = useState(false);
const { toastRef } = useContext(ToastContext);
const { logoutFromProvider, isLoading: isSDKLoading } = useCardSDK();
const userLocation = useSelector(selectUserCardLocation);
const userLocation = useSelector(selectCardUserLocation);
const isMetalCardCheckoutEnabled = useSelector(
selectMetalCardCheckoutFeatureFlag,
);
Expand Down Expand Up @@ -1130,12 +1127,16 @@ const CardHome = () => {

try {
await removeCardBaanxToken();
// Sync controller state: token is now gone so validateAndRefreshSession
// will mark CardController.isAuthenticated = false without an API call.
await Engine.context.CardController.validateAndRefreshSession().catch(
() => undefined,
);

if (isComponentUnmountedRef.current) {
return;
}

dispatch(resetAuthenticatedData());
queryClient.removeQueries({ queryKey: cardQueries.keys.all() });

toastRef?.current?.showToast({
Expand All @@ -1159,7 +1160,7 @@ const CardHome = () => {
};

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

useEffect(() => {
if (isSDKLoading) {
Expand Down
14 changes: 12 additions & 2 deletions app/components/UI/Card/Views/CardWelcome/CardWelcome.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,20 @@ jest.mock('../../../../../util/theme', () => {
};
});

const createTestStore = (initialState = {}) =>
const createTestStore = (
initialState: { cardholderAccounts?: string[] } = {},
) =>
configureStore({
reducer: {
card: (state = { cardholderAccounts: [], ...initialState }) => state,
engine: (
state = {
backgroundState: {
CardController: {
cardholderAccounts: initialState.cardholderAccounts ?? [],
},
},
},
) => state,
},
});

Expand Down
2 changes: 1 addition & 1 deletion app/components/UI/Card/Views/CardWelcome/CardWelcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Routes from '../../../../../constants/navigation/Routes';
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
import { MetaMetricsEvents } from '../../../../../core/Analytics';
import { CardActions, CardScreens } from '../../util/metrics';
import { selectHasCardholderAccounts } from '../../../../../core/redux/slices/card';
import { selectHasCardholderAccounts } from '../../../../../selectors/cardController';
import { useSelector } from 'react-redux';

const CardWelcome = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,7 @@ describe('AssetSelectionBottomSheet', () => {
beforeEach(() => {
jest.clearAllMocks();

mockUseSelector.mockImplementation((selector) => {
if (selector.toString().includes('selectUserCardLocation')) {
return 'international';
}
return undefined;
});
mockUseSelector.mockReturnValue(undefined);

(useAssetBalances as jest.Mock).mockReturnValue(new Map());

Expand Down Expand Up @@ -393,12 +388,7 @@ describe('AssetSelectionBottomSheet', () => {

describe('token sorting', () => {
it('sorts tokens by priority', () => {
mockUseSelector.mockImplementation((selector) => {
if (selector.toString().includes('selectUserCardLocation')) {
return 'international';
}
return undefined;
});
mockUseSelector.mockReturnValue(undefined);
const token1 = createMockToken({
symbol: 'USDC',
priority: 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ function renderWithProvider(
},
},
card: {
cardholderAccounts: [],
hasViewedCardButton: false,
isLoaded: false,
...cardState,
},
},
Expand Down
4 changes: 0 additions & 4 deletions app/components/UI/Card/components/CardButton/CardButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
CARD_BUTTON_BADGE_AB_KEY,
CARD_BUTTON_BADGE_VARIANTS,
} from './abTestConfig';
import Logger from '../../../../../util/Logger';

interface CardButtonProps {
onPress: () => void;
Expand Down Expand Up @@ -56,9 +55,6 @@ const CardButton: React.FC<CardButtonProps> = ({ onPress, touchAreaSlop }) => {
useEffect(() => {
if (hasTrackedViewedEvent.current || !flagsResolved) return;
hasTrackedViewedEvent.current = true;
Logger.log({
active_ab_tests: [{ key: CARD_BUTTON_BADGE_AB_KEY, value: variantName }],
});

trackEvent(
createEventBuilder(MetaMetricsEvents.CARD_BUTTON_VIEWED)
Expand Down
Loading
Loading