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
6 changes: 3 additions & 3 deletions .claude/commands/unit-test-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ flowchart TD
"failedTests": [{
"file": "usePerps.test.tsx",
"error": "Cannot read property 'data' of undefined",
"command": "npx jest usePerps.test.tsx --no-coverage"
"command": "yarn jest usePerps.test.tsx --no-coverage"
}]
}
```
Expand Down Expand Up @@ -316,11 +316,11 @@ cat scripts/reports/coverage-report-*.json | jq '.failedTests | length'
cat scripts/reports/coverage-report-*.json | jq '.actionableRecommendations.filesNeedingImprovement[0]'

# Debug failing test
npx jest path/to/test.tsx --no-coverage --verbose
yarn jest path/to/test.tsx --no-coverage --verbose

# Type check & lint
yarn lint:tsc
npx eslint path/to/test.tsx --fix
yarn eslint path/to/test.tsx --fix
```

## Skip These Files
Expand Down
30 changes: 24 additions & 6 deletions app.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
const { RUNTIME_VERSION, PROJECT_ID, UPDATE_URL } = require('./ota.config.js');

// Use METAMASK_ENVIRONMENT to select OTA certs:
// - "production" and "rc" use their own certificates
// - all other environments (exp, dev, test, e2e, beta, etc.) fall back to "exp"
const OTA_ENV_MAP = {
production: 'production',
rc: 'rc',
};

const OTA_ENV = OTA_ENV_MAP[process.env.METAMASK_ENVIRONMENT] ?? 'exp';

const CODE_SIGNING_CERTS = {
production: './certs/production.certificate.pem',
exp: './certs/exp.certificate.pem',
rc: './certs/rc.certificate.pem',
};

const CODE_SIGNING_KEYIDS = {
production: 'production',
exp: 'exp',
rc: 'rc',
};

module.exports = {
name: 'MetaMask',
displayName: 'MetaMask',
Expand Down Expand Up @@ -75,16 +97,12 @@ module.exports = {
owner: 'metamask',
runtimeVersion: RUNTIME_VERSION,
updates: {
codeSigningCertificate: './certs/certificate.pem',
codeSigningCertificate: CODE_SIGNING_CERTS[OTA_ENV],
codeSigningMetadata: {
keyid: 'main',
keyid: CODE_SIGNING_KEYIDS[OTA_ENV],
alg: 'rsa-v1_5-sha256',
},
url: UPDATE_URL,
// Channel is set by requestHeaders, will be overridden with build script
requestHeaders: {
'expo-channel-name': 'preview',
},
},
extra: {
eas: {
Expand Down
32 changes: 4 additions & 28 deletions app/components/Nav/App/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { KeyringTypes } from '@metamask/keyring-controller';
import { AccountDetailsIds } from '../../../../e2e/selectors/MultichainAccounts/AccountDetails.selectors';
import { AvatarAccountType } from '../../../component-library/components/Avatars/Avatar';
import AUTHENTICATION_TYPE from '../../../constants/userProperties';
import { useOTAUpdates } from '../../hooks/useOTAUpdates';

const initialState: DeepPartial<RootState> = {
user: {
Expand Down Expand Up @@ -83,16 +82,6 @@ jest.mock('../../hooks/useMetrics/useMetrics', () => ({
}),
}));

jest.mock('../../hooks/useOTAUpdates', () => ({
useOTAUpdates: jest.fn().mockReturnValue({
isCheckingUpdates: false,
}),
}));

const mockUseOTAUpdates = useOTAUpdates as jest.MockedFunction<
typeof useOTAUpdates
>;

jest.mock(
'../../UI/FoxLoader',
() =>
Expand Down Expand Up @@ -123,6 +112,10 @@ jest.mock('../../../core/OAuthService/OAuthLoginHandlers', () => ({
createLoginHandler: jest.fn(),
}));

jest.mock('../../hooks/useOTAUpdates', () => ({
useOTAUpdates: jest.fn(),
}));

// Mock the navigation hook
const mockNavigate = jest.fn();
const mockReset = jest.fn();
Expand Down Expand Up @@ -269,9 +262,6 @@ describe('App', () => {

beforeEach(() => {
jest.clearAllMocks();
mockUseOTAUpdates.mockReturnValue({
isCheckingUpdates: false,
});
mockNavigate.mockClear();
});

Expand All @@ -284,20 +274,6 @@ describe('App', () => {
jest.useRealTimers();
});

it('renders FoxLoader when OTA update check runs', () => {
mockUseOTAUpdates.mockReturnValue({
isCheckingUpdates: true,
});

const { getByTestId } = renderScreen(
App,
{ name: 'App' },
{ state: initialState },
);

expect(getByTestId(MOCK_FOX_LOADER_ID)).toBeTruthy();
});

it('configures MetaMetrics instance and identifies user on startup', async () => {
renderScreen(App, { name: 'App' }, { state: initialState });
await waitFor(() => {
Expand Down
21 changes: 9 additions & 12 deletions app/components/Nav/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import ConnectQRHardware from '../../Views/ConnectQRHardware';
import SelectHardwareWallet from '../../Views/ConnectHardware/SelectHardware';
import { AUTHENTICATION_APP_TRIGGERED_AUTH_NO_CREDENTIALS } from '../../../constants/error';
import { UpdateNeeded } from '../../../components/UI/UpdateNeeded';
import { OTAUpdatesModal } from '../../UI/OTAUpdatesModal';
import NetworkSettings from '../../Views/Settings/NetworksSettings/NetworkSettings';
import ModalMandatory from '../../../component-library/components/Modals/ModalMandatory';
import { RestoreWallet } from '../../Views/RestoreWallet';
Expand Down Expand Up @@ -149,7 +150,6 @@ import MultichainAccountActions from '../../Views/MultichainAccounts/sheets/Mult
import useInterval from '../../hooks/useInterval';
import { Duration } from '@metamask/utils';
import { selectSeedlessOnboardingLoginFlow } from '../../../selectors/seedlessOnboardingController';
import { useOTAUpdates } from '../../hooks/useOTAUpdates';
import { SmartAccountUpdateModal } from '../../Views/confirmations/components/smart-account-update-modal';
import { PayWithModal } from '../../Views/confirmations/components/modals/pay-with-modal/pay-with-modal';
import { useMetrics } from '../../hooks/useMetrics';
Expand All @@ -161,6 +161,7 @@ import { useEmptyNavHeaderForConfirmations } from '../../Views/confirmations/hoo
import { trackVaultCorruption } from '../../../util/analytics/vaultCorruptionTracking';
import SocialLoginIosUser from '../../Views/SocialLoginIosUser';
import AUTHENTICATION_TYPE from '../../../constants/userProperties';
import { useOTAUpdates } from '../../hooks/useOTAUpdates';

const clearStackNavigatorOptions = {
headerShown: false,
Expand Down Expand Up @@ -516,6 +517,10 @@ const RootModalFlow = (props: RootModalFlowProps) => (
<Stack.Screen name={'AssetOptions'} component={AssetOptions} />
<Stack.Screen name={'NftOptions'} component={NftOptions} />
<Stack.Screen name={Routes.MODAL.UPDATE_NEEDED} component={UpdateNeeded} />
<Stack.Screen
name={Routes.MODAL.OTA_UPDATES_MODAL}
component={OTAUpdatesModal}
/>
{
<Stack.Screen
name={Routes.SHEET.SELECT_SRP}
Expand Down Expand Up @@ -1088,7 +1093,7 @@ const AppFlow = () => {
);
};

const AppContent: React.FC = () => {
const App: React.FC = () => {
const navigation = useNavigation();
const routes = useNavigationState((state) => state.routes);
const { toastRef } = useContext(ToastContext);
Expand All @@ -1098,6 +1103,8 @@ const AppContent: React.FC = () => {
selectSeedlessOnboardingLoginFlow,
);

useOTAUpdates();

if (isFirstRender.current) {
trace({
name: TraceName.NavInit,
Expand Down Expand Up @@ -1280,14 +1287,4 @@ const AppContent: React.FC = () => {
);
};

const App: React.FC = () => {
const { isCheckingUpdates } = useOTAUpdates();

if (isCheckingUpdates) {
return <FoxLoader />;
}

return <AppContent />;
};

export default App;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`AssetElement should render correctly 1`] = `
exports[`AssetElement renders correctly 1`] = `
<TouchableOpacity
disabled={false}
onLongPress={[Function]}
Expand Down
2 changes: 2 additions & 0 deletions app/components/UI/AssetElement/index.constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const BALANCE_TEST_ID = 'balance-test-id';
export const SECONDARY_BALANCE_TEST_ID = 'secondary-balance-test-id';
export const SECONDARY_BALANCE_BUTTON_TEST_ID =
'secondary-balance-button-test-id';
export const TOKEN_AMOUNT_BALANCE_TEST_ID = 'token-amount-balance-test-id';
61 changes: 59 additions & 2 deletions app/components/UI/AssetElement/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import { shallow } from 'enzyme';
import { render, fireEvent } from '@testing-library/react-native';
import AssetElement from './';
import { getAssetTestId } from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds';
import { BALANCE_TEST_ID, SECONDARY_BALANCE_TEST_ID } from './index.constants';
import {
BALANCE_TEST_ID,
SECONDARY_BALANCE_BUTTON_TEST_ID,
SECONDARY_BALANCE_TEST_ID,
} from './index.constants';
import { TOKEN_BALANCE_LOADING } from '../Tokens/constants';
import { TextColor } from '../../../component-library/components/Texts/Text';
import { mockTheme } from '../../../util/theme';

describe('AssetElement', () => {
const onPressMock = jest.fn();
const onLongPressMock = jest.fn();
const onSecondaryBalancePressMock = jest.fn();

const erc20Token = {
name: 'Dai',
Expand All @@ -26,7 +31,11 @@ describe('AssetElement', () => {
image: '',
};

it('should render correctly', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders correctly', () => {
const wrapper = shallow(<AssetElement asset={erc20Token} />);
expect(wrapper).toMatchSnapshot();
});
Expand Down Expand Up @@ -205,4 +214,52 @@ describe('AssetElement', () => {
});
expect(secondaryBalance.props.children).toBe('0.00%');
});

describe('onSecondaryBalancePress', () => {
it('calls onSecondaryBalancePress with asset when secondary balance is pressed', () => {
const { getByTestId } = render(
<AssetElement
asset={erc20Token}
balance="$100.00"
secondaryBalance="Convert to mUSD"
onSecondaryBalancePress={onSecondaryBalancePressMock}
/>,
);

fireEvent.press(getByTestId(SECONDARY_BALANCE_BUTTON_TEST_ID));

expect(onSecondaryBalancePressMock).toHaveBeenCalledTimes(1);
expect(onSecondaryBalancePressMock).toHaveBeenCalledWith(erc20Token);
});

it('does not call onSecondaryBalancePress when handler is undefined', () => {
const { getByTestId } = render(
<AssetElement
asset={erc20Token}
balance="$100.00"
secondaryBalance="+5.67%"
/>,
);

fireEvent.press(getByTestId(SECONDARY_BALANCE_BUTTON_TEST_ID));

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

it('does not call onSecondaryBalancePress when disabled prop is true', () => {
const { getByTestId } = render(
<AssetElement
asset={erc20Token}
balance="$100.00"
secondaryBalance="Convert to mUSD"
onSecondaryBalancePress={onSecondaryBalancePressMock}
disabled
/>,
);

fireEvent.press(getByTestId(SECONDARY_BALANCE_BUTTON_TEST_ID));

expect(onSecondaryBalancePressMock).not.toHaveBeenCalled();
});
});
});
58 changes: 39 additions & 19 deletions app/components/UI/AssetElement/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import {
TOKEN_BALANCE_LOADING_UPPERCASE,
TOKEN_RATE_UNDEFINED,
} from '../Tokens/constants';
import { BALANCE_TEST_ID, SECONDARY_BALANCE_TEST_ID } from './index.constants';
import {
BALANCE_TEST_ID,
SECONDARY_BALANCE_BUTTON_TEST_ID,
SECONDARY_BALANCE_TEST_ID,
} from './index.constants';

interface AssetElementProps {
children?: React.ReactNode;
Expand All @@ -35,6 +39,7 @@ interface AssetElementProps {
privacyMode?: boolean;
hideSecondaryBalanceInPrivacyMode?: boolean;
disabled?: boolean;
onSecondaryBalancePress?: (asset: TokenI) => void;
}

const createStyles = (colors: Colors) =>
Expand Down Expand Up @@ -74,6 +79,7 @@ const AssetElement: React.FC<AssetElementProps> = ({
privacyMode = false,
hideSecondaryBalanceInPrivacyMode = true,
disabled = false,
onSecondaryBalancePress,
}) => {
const { colors } = useTheme();
const styles = createStyles(colors);
Expand All @@ -86,6 +92,13 @@ const AssetElement: React.FC<AssetElementProps> = ({
onLongPress?.(asset);
};

const isSecondaryDisabled = disabled || !onSecondaryBalancePress;

const handleOnSecondaryBalancePress = () => {
if (isSecondaryDisabled) return;
onSecondaryBalancePress?.(asset);
};

// TODO: Use the SensitiveText component when it's available
// when privacyMode is true, we should hide the balance and the fiat
return (
Expand Down Expand Up @@ -119,25 +132,32 @@ const AssetElement: React.FC<AssetElementProps> = ({
</SensitiveText>
)}
{secondaryBalance ? (
<SensitiveText
variant={TextVariant.BodySMMedium}
style={
secondaryBalanceColor
? styles.secondaryBalanceCustomColor
: styles.secondaryBalance
}
color={secondaryBalanceColor}
isHidden={privacyMode && hideSecondaryBalanceInPrivacyMode}
length={SensitiveTextLength.Short}
testID={SECONDARY_BALANCE_TEST_ID}
<TouchableOpacity
onPress={handleOnSecondaryBalancePress}
disabled={isSecondaryDisabled}
testID={SECONDARY_BALANCE_BUTTON_TEST_ID}
>
{secondaryBalance === TOKEN_BALANCE_LOADING ||
secondaryBalance === TOKEN_BALANCE_LOADING_UPPERCASE ? (
<SkeletonText thin style={styles.skeleton} />
) : (
secondaryBalance
)}
</SensitiveText>
<SensitiveText
variant={TextVariant.BodySMMedium}
style={
secondaryBalanceColor
? styles.secondaryBalanceCustomColor
: styles.secondaryBalance
}
color={secondaryBalanceColor}
isHidden={privacyMode && hideSecondaryBalanceInPrivacyMode}
length={SensitiveTextLength.Short}
testID={SECONDARY_BALANCE_TEST_ID}
// Remove onPress from here since it's on Pressable now
>
{secondaryBalance === TOKEN_BALANCE_LOADING ||
secondaryBalance === TOKEN_BALANCE_LOADING_UPPERCASE ? (
<SkeletonText thin style={styles.skeleton} />
) : (
secondaryBalance
)}
</SensitiveText>
</TouchableOpacity>
) : null}
</View>
</TouchableOpacity>
Expand Down
Loading
Loading