Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
bdb3901
feat(card): wip - baanx integration
Brunonascdev Oct 3, 2025
49d4bb0
feat(card): new login endpoint refactor
Brunonascdev Oct 8, 2025
fc7ae1a
feat(card): merge with main
Brunonascdev Oct 8, 2025
0623f88
feat(card): add test files
Brunonascdev Oct 8, 2025
eca9348
feat(card): fix remaining tests
Brunonascdev Oct 9, 2025
55f5c53
refactor(card): remove logger from cardTokenVault
Brunonascdev Oct 9, 2025
a2d172c
chore: remove changed perps envs on .js.example
Brunonascdev Oct 9, 2025
a18dcec
feat(card): handle 404 status and change logger.error to logger.log
Brunonascdev Oct 9, 2025
4da2553
feat(card): increase test coverage
Brunonascdev Oct 9, 2025
a384023
feat(card): fix sonarcloud issues
Brunonascdev Oct 9, 2025
7b7a03f
Merge branch 'main' of github.com:MetaMask/metamask-mobile into chore…
Brunonascdev Oct 9, 2025
524ec37
Merge branch 'main' into chore/card-api-integration-foundation
Brunonascdev Oct 9, 2025
2ef1abd
fix(card): failure on refresh token exchange
Brunonascdev Oct 9, 2025
984d40f
Merge branch 'chore/card-api-integration-foundation' of github.com:Me…
Brunonascdev Oct 9, 2025
5a144b8
Merge branch 'main' into chore/card-api-integration-foundation
Brunonascdev Oct 9, 2025
e99f7a9
Merge branch 'main' into chore/card-api-integration-foundation
Brunonascdev Oct 10, 2025
b825071
Merge branch 'main' of github.com:MetaMask/metamask-mobile into chore…
Brunonascdev Oct 10, 2025
6358101
feat(card): fix re-renders on cardsdk
Brunonascdev Oct 10, 2025
ae70e03
Merge branch 'main' of github.com:MetaMask/metamask-mobile into chore…
Brunonascdev Oct 12, 2025
85fdfa3
feat(card): modify CardImage component to support metal card
Brunonascdev Oct 12, 2025
2f8d0d3
feat(card): authenticated data wip
Brunonascdev Oct 12, 2025
89341cb
feat(card): merge with main
Brunonascdev Oct 13, 2025
d240dd2
feat(card): authenticated data - need to fix BigNumber issue
Brunonascdev Oct 13, 2025
257b976
fix(card): bigint issue
Brunonascdev Oct 14, 2025
5a72d57
feat(card): performance fixes
Brunonascdev Oct 14, 2025
eb93b2f
feat(card): add warning
Brunonascdev Oct 15, 2025
cc98b7d
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Oct 15, 2025
25cd4c6
feat(card): adapt spending limit progress bar component to use total …
Brunonascdev Oct 15, 2025
9e5385f
feat(card): adapt hooks and components for Solana assets
Brunonascdev Oct 15, 2025
fecd953
feat(card): working version with Solana assets
Brunonascdev Oct 16, 2025
1bb1362
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Oct 16, 2025
0f5ca3a
feat(card): last solana changes, authentication route setup
Brunonascdev Oct 17, 2025
18fb6b2
feat(card): fix infinite loading state on get card failure
Brunonascdev Oct 17, 2025
af88f86
feat(card): merge with main changes
Brunonascdev Oct 17, 2025
2e8a90f
feat(card): fix missing sdk issue
Brunonascdev Oct 17, 2025
c1329ba
feat(card): fix inconsistent sdk behavior
Brunonascdev Oct 17, 2025
51a5053
test(card): add tests for authenticated data changes
Brunonascdev Oct 18, 2025
a240215
lint(card): undo changes on button readme
Brunonascdev Oct 18, 2025
3e659ab
feat(card): fix lint issues and add useCardDetails test file
Brunonascdev Oct 18, 2025
58e611d
Merge branch 'main' of github.com:MetaMask/metamask-mobile into feat/…
Brunonascdev Oct 18, 2025
33fb5fe
test(card): increase coverage
Brunonascdev Oct 18, 2025
36ea7fe
feat(card): fix token parsing issue
Brunonascdev Oct 18, 2025
eb1eb79
feat(card): move card authentication out card context
Brunonascdev Oct 18, 2025
dfab310
feat(card): add card slices test
Brunonascdev Oct 18, 2025
167f22c
lint(card): fix lint issues
Brunonascdev Oct 19, 2025
8e5e95c
feat(card): merge with main
Brunonascdev Oct 20, 2025
1963b32
feat(card): fix typo
Brunonascdev Oct 20, 2025
c5b1a71
Merge branch 'main' into feat/card-authentication-global-context
Brunonascdev Oct 20, 2025
b9104a4
Merge branch 'main' into feat/card-authentication-global-context
Brunonascdev Oct 20, 2025
3aca804
Merge branch 'main' into feat/card-authentication-global-context
Brunonascdev Oct 20, 2025
5ad3283
Merge branch 'main' into feat/card-authentication-global-context
Brunonascdev Oct 20, 2025
175a2a4
Merge branch 'main' into feat/card-authentication-global-context
Brunonascdev Oct 21, 2025
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 @@ -233,7 +233,7 @@ const CardAuthentication = () => {
loading={loading}
style={[styles.button, isDisabled && styles.buttonDisabled]}
width={ButtonWidthTypes.Full}
disabled={isDisabled}
disabled={isDisabled || loading}
/>
</Box>
</Box>
Expand Down
1 change: 0 additions & 1 deletion app/components/UI/Card/hooks/isBaanxLoginEnabled.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const mockCardSDKResponse = (sdk: Partial<CardSDK> | null) => {
sdk: sdk as CardSDK | null,
isLoading: false,
logoutFromProvider: jest.fn(),
userCardLocation: 'international',
});
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { renderHook } from '@testing-library/react-hooks';
import { useSelector } from 'react-redux';
import { useCardAuthenticationVerification } from './useCardAuthenticationVerification';
import useThunkDispatch from '../../../hooks/useThunkDispatch';
import { verifyCardAuthentication } from '../../../../core/redux/slices/card';
import Logger from '../../../../util/Logger';
import { CardFeatureFlag } from '../../../../selectors/featureFlagController/card';

jest.mock('react-redux');
jest.mock('../../../hooks/useThunkDispatch');
jest.mock('../../../../core/redux/slices/card');
jest.mock('../../../../util/Logger');

const mockUseSelector = useSelector as jest.MockedFunction<typeof useSelector>;
const mockUseThunkDispatch = useThunkDispatch as jest.MockedFunction<
typeof useThunkDispatch
>;
const mockLogger = Logger as jest.Mocked<typeof Logger>;

describe('useCardAuthenticationVerification', () => {
const mockDispatch = jest.fn();

const mockCardFeatureFlag: CardFeatureFlag = {
isBaanxLoginEnabled: true,
constants: {
onRampApiUrl: 'https://api.onramp.metamask.io',
accountsApiUrl: 'https://api.accounts.metamask.io',
},
chains: {
'1': {
enabled: true,
tokens: [],
},
},
};

beforeEach(() => {
jest.clearAllMocks();
mockUseThunkDispatch.mockReturnValue(mockDispatch);
});

const defaultMockState = {
userLoggedIn: true,
cardFeatureFlag: mockCardFeatureFlag,
};

const setupMockSelectors = (overrides = {}) => {
const state = { ...defaultMockState, ...overrides };

mockUseSelector.mockReset();

const selectorValues = [state.userLoggedIn, state.cardFeatureFlag];

let callIndex = 0;
mockUseSelector.mockImplementation(() => {
const value = selectorValues[callIndex % selectorValues.length];
callIndex++;
return value;
});
};

it('dispatches verification when all conditions are met', () => {
setupMockSelectors();

renderHook(() => useCardAuthenticationVerification());

expect(mockDispatch).toHaveBeenCalledWith(
verifyCardAuthentication({
isBaanxLoginEnabled: true,
}),
);
});

it('dispatches verification with isBaanxLoginEnabled false', () => {
setupMockSelectors({
cardFeatureFlag: { ...mockCardFeatureFlag, isBaanxLoginEnabled: false },
});

renderHook(() => useCardAuthenticationVerification());

expect(mockDispatch).toHaveBeenCalledWith(
verifyCardAuthentication({
isBaanxLoginEnabled: false,
}),
);
});

it('dispatches verification when isBaanxLoginEnabled is undefined', () => {
const featureFlagWithoutBaanx = { ...mockCardFeatureFlag };
delete (featureFlagWithoutBaanx as Partial<CardFeatureFlag>)
.isBaanxLoginEnabled;
setupMockSelectors({
cardFeatureFlag: featureFlagWithoutBaanx,
});

renderHook(() => useCardAuthenticationVerification());

expect(mockDispatch).toHaveBeenCalledWith(
verifyCardAuthentication({
isBaanxLoginEnabled: false,
}),
);
});

it('does not dispatch when user is not logged in', () => {
setupMockSelectors({ userLoggedIn: false });

renderHook(() => useCardAuthenticationVerification());

expect(mockDispatch).not.toHaveBeenCalled();
});

it('does not dispatch when card feature flag is null', () => {
setupMockSelectors({ cardFeatureFlag: null });

renderHook(() => useCardAuthenticationVerification());

expect(mockDispatch).not.toHaveBeenCalled();
});

it('does not dispatch when card feature flag is undefined', () => {
setupMockSelectors({ cardFeatureFlag: undefined });

renderHook(() => useCardAuthenticationVerification());

expect(mockDispatch).not.toHaveBeenCalled();
});

it('does not dispatch when both user is not logged in and card feature flag is not enabled', () => {
setupMockSelectors({ userLoggedIn: false, cardFeatureFlag: null });

renderHook(() => useCardAuthenticationVerification());

expect(mockDispatch).not.toHaveBeenCalled();
});

it('logs error when dispatch throws Error instance', () => {
const testError = new Error('Test dispatch error');
setupMockSelectors();
mockDispatch.mockImplementation(() => {
throw testError;
});

renderHook(() => useCardAuthenticationVerification());

expect(mockLogger.error).toHaveBeenCalledWith(
testError,
'useCardAuthenticationVerification::Error verifying authentication',
);
});

it('logs error when dispatch throws non-Error value', () => {
setupMockSelectors();
mockDispatch.mockImplementation(() => {
throw 'String error';
});

renderHook(() => useCardAuthenticationVerification());

expect(mockLogger.error).toHaveBeenCalledWith(
new Error('String error'),
'useCardAuthenticationVerification::Error verifying authentication',
);
});

it('dispatches verification when user logs in', () => {
setupMockSelectors({ userLoggedIn: false });
const { rerender } = renderHook(() => useCardAuthenticationVerification());

expect(mockDispatch).not.toHaveBeenCalled();

setupMockSelectors({ userLoggedIn: true });
rerender();

expect(mockDispatch).toHaveBeenCalledWith(
verifyCardAuthentication({
isBaanxLoginEnabled: true,
}),
);
});

it('dispatches verification when card feature flag is enabled', () => {
setupMockSelectors({ cardFeatureFlag: null });
const { rerender } = renderHook(() => useCardAuthenticationVerification());

expect(mockDispatch).not.toHaveBeenCalled();

setupMockSelectors({ cardFeatureFlag: mockCardFeatureFlag });
rerender();

expect(mockDispatch).toHaveBeenCalledWith(
verifyCardAuthentication({
isBaanxLoginEnabled: true,
}),
);
});

it('dispatches verification again when isBaanxLoginEnabled flag changes', () => {
setupMockSelectors({
cardFeatureFlag: { ...mockCardFeatureFlag, isBaanxLoginEnabled: false },
});
const { rerender } = renderHook(() => useCardAuthenticationVerification());

expect(mockDispatch).toHaveBeenCalledWith(
verifyCardAuthentication({
isBaanxLoginEnabled: false,
}),
);
expect(mockDispatch).toHaveBeenCalledTimes(1);

mockDispatch.mockClear();

setupMockSelectors({
cardFeatureFlag: { ...mockCardFeatureFlag, isBaanxLoginEnabled: true },
});
rerender();

expect(mockDispatch).toHaveBeenCalledWith(
verifyCardAuthentication({
isBaanxLoginEnabled: true,
}),
);
expect(mockDispatch).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import useThunkDispatch from '../../../hooks/useThunkDispatch';
import { verifyCardAuthentication } from '../../../../core/redux/slices/card';
import { selectUserLoggedIn } from '../../../../reducers/user';
import {
CardFeatureFlag,
selectCardFeatureFlag,
} from '../../../../selectors/featureFlagController/card';
import Logger from '../../../../util/Logger';

/**
* Hook that automatically verifies card authentication status when the app loads.
* This hook should be used at the app entry level to ensure authentication state
* is always up-to-date without needing to open the Card screen.
*/
export const useCardAuthenticationVerification = () => {
const dispatch = useThunkDispatch();
const userLoggedIn = useSelector(selectUserLoggedIn);
const cardFeatureFlag = useSelector(selectCardFeatureFlag) as CardFeatureFlag;

const checkAuthentication = useCallback(() => {
const isBaanxLoginEnabled = cardFeatureFlag?.isBaanxLoginEnabled ?? false;

dispatch(
verifyCardAuthentication({
isBaanxLoginEnabled,
}),
);
}, [cardFeatureFlag, dispatch]);

useEffect(() => {
// Only run authentication check when:
// 1. User is logged in
// 2. Card feature flag is enabled
if (userLoggedIn && cardFeatureFlag) {
try {
checkAuthentication();
} catch (error) {
Logger.error(
error instanceof Error ? error : new Error(String(error)),
'useCardAuthenticationVerification::Error verifying authentication',
);
}
}
}, [userLoggedIn, cardFeatureFlag, checkAuthentication]);
};
6 changes: 0 additions & 6 deletions app/components/UI/Card/hooks/useCardDetails.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ describe('useCardDetails', () => {
sdk: mockSDK,
isLoading: false,
logoutFromProvider: mockLogoutFromProvider,
userCardLocation: 'us',
});
});

Expand Down Expand Up @@ -240,7 +239,6 @@ describe('useCardDetails', () => {
sdk: null,
isLoading: true,
logoutFromProvider: mockLogoutFromProvider,
userCardLocation: 'us',
});
mockGetCardDetails.mockResolvedValue(mockCardDetailsResponse);

Expand All @@ -257,7 +255,6 @@ describe('useCardDetails', () => {
sdk: null,
isLoading: false,
logoutFromProvider: mockLogoutFromProvider,
userCardLocation: 'us',
});
mockGetCardDetails.mockResolvedValue(mockCardDetailsResponse);

Expand Down Expand Up @@ -295,7 +292,6 @@ describe('useCardDetails', () => {
sdk: null,
isLoading: true,
logoutFromProvider: mockLogoutFromProvider,
userCardLocation: 'us',
});
mockGetCardDetails.mockResolvedValue(mockCardDetailsResponse);

Expand All @@ -310,7 +306,6 @@ describe('useCardDetails', () => {
sdk: mockSDK,
isLoading: false,
logoutFromProvider: mockLogoutFromProvider,
userCardLocation: 'us',
});
rerender();

Expand All @@ -328,7 +323,6 @@ describe('useCardDetails', () => {
sdk: null,
isLoading: false,
logoutFromProvider: mockLogoutFromProvider,
userCardLocation: 'us',
});

const { result } = renderHook(() => useCardDetails());
Expand Down
Loading
Loading