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
17 changes: 17 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,14 @@ jobs:
${{ steps.rename.outputs.ios_archive_path }}
if-no-files-found: error

- name: Upload iOS sourcemap
if: matrix.platform == 'ios' && env.IS_SIM_BUILD != 'true'
uses: actions/upload-artifact@v4
with:
name: ios-sourcemaps-${{ inputs.build_name }}
path: ${{ steps.rename.outputs.ios_sourcemap_path }}
if-no-files-found: warn

# Dev builds (CONFIGURATION=Debug): APK only — mirrors Bitrise IS_DEV_BUILD behavior
- name: Upload Android dev artifacts
if: matrix.platform == 'android' && env.CONFIGURATION == 'Debug'
Expand All @@ -349,3 +357,12 @@ jobs:
${{ steps.rename.outputs.android_apk_path }}
${{ steps.rename.outputs.android_aab_path }}
if-no-files-found: error

# Non-Debug builds (prod, RC, beta, test, e2e, exp): sourcemaps — mirrors Bitrise Deploy Android Sourcemaps step
- name: Upload Android sourcemaps
if: matrix.platform == 'android' && env.CONFIGURATION != 'Debug'
uses: actions/upload-artifact@v4
with:
name: android-sourcemaps-${{ inputs.build_name }}
path: ${{ steps.rename.outputs.android_sourcemap_dir }}
if-no-files-found: warn
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,13 @@ const BottomSheet = forwardRef<BottomSheetRef, BottomSheetProps>(
const onCloseCB = useCallback(() => {
if (shouldNavigateBack && !didNavigateBackRef.current) {
didNavigateBackRef.current = true;
navigation.goBack();
if (navigation.isFocused()) {
navigation.goBack();
} else {
Logger.log(
'[BottomSheet] navigation.goBack skipped (screen not focused)',
);
}
} else if (shouldNavigateBack && didNavigateBackRef.current) {
Logger.log('[BottomSheet] navigation.goBack skipped (duplicate close)');
}
Expand All @@ -92,14 +98,17 @@ const BottomSheet = forwardRef<BottomSheetRef, BottomSheetProps>(
// Dismiss the sheet when Android back button is pressed.
useEffect(() => {
const hardwareBackPress = () => {
if (!navigation.isFocused()) {
return false;
}
isInteractable && bottomSheetDialogRef.current?.onCloseDialog();
return true;
};
BackHandler.addEventListener('hardwareBackPress', hardwareBackPress);
return () => {
BackHandler.removeEventListener('hardwareBackPress', hardwareBackPress);
};
}, [onCloseCB, isInteractable]);
}, [onCloseCB, isInteractable, navigation]);

useImperativeHandle(ref, () => ({
onCloseBottomSheet: (callback) => {
Expand Down
4 changes: 4 additions & 0 deletions app/component-library/components/Icons/Icon/Icon.assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ import menuSVG from './assets/menu.svg';
import messagequestionSVG from './assets/message-question.svg';
import messagesSVG from './assets/messages.svg';
import metamaskfoxoutlineSVG from './assets/metamask-fox-outline.svg';
import metamaskfoxfilledSVG from './assets/metamask-fox-filled.svg';
import micSVG from './assets/mic.svg';
import minusboldSVG from './assets/minus-bold.svg';
import minussquareSVG from './assets/minus-square.svg';
Expand Down Expand Up @@ -197,6 +198,7 @@ import scanbarcodeSVG from './assets/scan-barcode.svg';
import scanfocusSVG from './assets/scan-focus.svg';
import scanSVG from './assets/scan.svg';
import searchSVG from './assets/search.svg';
import searchfilledSVG from './assets/search-filled.svg';
import securityalertSVG from './assets/security-alert.svg';
import securitycrossSVG from './assets/security-cross.svg';
import securitykeySVG from './assets/security-key.svg';
Expand Down Expand Up @@ -433,6 +435,7 @@ export const assetByIconName: AssetByIconName = {
[IconName.MessageQuestion]: messagequestionSVG,
[IconName.Messages]: messagesSVG,
[IconName.MetamaskFoxOutline]: metamaskfoxoutlineSVG,
[IconName.MetamaskFoxFilled]: metamaskfoxfilledSVG,
[IconName.Mic]: micSVG,
[IconName.MinusBold]: minusboldSVG,
[IconName.MinusSquare]: minussquareSVG,
Expand Down Expand Up @@ -477,6 +480,7 @@ export const assetByIconName: AssetByIconName = {
[IconName.ScanFocus]: scanfocusSVG,
[IconName.Scan]: scanSVG,
[IconName.Search]: searchSVG,
[IconName.SearchFilled]: searchfilledSVG,
[IconName.SecurityAlert]: securityalertSVG,
[IconName.SecurityCross]: securitycrossSVG,
[IconName.SecurityKey]: securitykeySVG,
Expand Down
2 changes: 2 additions & 0 deletions app/component-library/components/Icons/Icon/Icon.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export enum IconName {
MessageQuestion = 'MessageQuestion',
Messages = 'Messages',
MetamaskFoxOutline = 'MetamaskFoxOutline',
MetamaskFoxFilled = 'MetamaskFoxFilled',
Mic = 'Mic',
MinusBold = 'MinusBold',
MinusSquare = 'MinusSquare',
Expand Down Expand Up @@ -267,6 +268,7 @@ export enum IconName {
ScanFocus = 'ScanFocus',
Scan = 'Scan',
Search = 'Search',
SearchFilled = 'SearchFilled',
SecurityAlert = 'SecurityAlert',
SecurityCross = 'SecurityCross',
SecurityKey = 'SecurityKey',
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 13 additions & 2 deletions app/component-library/components/Navigation/TabBar/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,29 @@ import {
BoxAlignItems,
} from '@metamask/design-system-react-native';
import Routes from '../../../../constants/navigation/Routes';
import { IconName } from '../../Icons/Icon';

import { MetaMetricsEvents } from '../../../../core/Analytics';
import { getDecimalChainId } from '../../../../util/networks';
import { useMetrics } from '../../../../components/hooks/useMetrics';
import { strings } from '../../../../../locales/i18n';

// Internal dependencies.
import { TabBarProps } from './TabBar.types';
import { TabBarProps, TabBarIconKey } from './TabBar.types';
import {
ICON_BY_TAB_BAR_ICON_KEY,
LABEL_BY_TAB_BAR_ICON_KEY,
} from './TabBar.constants';
import { selectChainId } from '../../../../selectors/networkController';
import { useAccountMenuEnabled } from '../../../../selectors/featureFlagController/accountMenu/useAccountMenuEnabled';

const FILLED_ICONS: Partial<Record<TabBarIconKey, IconName>> = {
[TabBarIconKey.Wallet]: IconName.HomeFilled,
[TabBarIconKey.Activity]: IconName.ClockFilled,
[TabBarIconKey.Trending]: IconName.SearchFilled,
[TabBarIconKey.Rewards]: IconName.MetamaskFoxFilled,
};

const TabBar = ({ state, descriptors, navigation }: TabBarProps) => {
const { trackEvent, createEventBuilder } = useMetrics();
const { bottom: bottomInset } = useSafeAreaInsets();
Expand All @@ -49,7 +57,10 @@ const TabBar = ({ state, descriptors, navigation }: TabBarProps) => {
const isSelected = options?.isSelected
? options.isSelected(state.routeNames[state.index])
: state.index === index;
const icon = ICON_BY_TAB_BAR_ICON_KEY[tabBarIconKey];
const baseIcon = ICON_BY_TAB_BAR_ICON_KEY[tabBarIconKey];
const icon = isSelected
? (FILLED_ICONS[tabBarIconKey] ?? baseIcon)
: baseIcon;
const labelKey = LABEL_BY_TAB_BAR_ICON_KEY[tabBarIconKey];
const labelText = labelKey ? strings(labelKey) : '';
const onPress = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ exports[`TabBar renders correctly 1`] = `
color="#131416"
fill="currentColor"
height={24}
name="Home"
name="HomeFilled"
style={
{
"height": 24,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ import renderWithProvider from '../../../../../util/test/renderWithProvider';
import { backgroundState } from '../../../../../util/test/initial-root-state';

jest.mock('../../../../../selectors/assets/balances', () => ({
// This selector is used directly with useSelector, so it must accept (state) and return a value
selectBalanceBySelectedAccountGroup: jest.fn(() => null),
// This one is a factory: selectBalanceChangeBySelectedAccountGroup(period) -> (state) => value
// Factory: selectBalanceBySelectedAccountGroup(popularChainIds?) -> (state) => value
selectBalanceBySelectedAccountGroup: jest.fn(() => () => null),
// Factory: selectBalanceChangeBySelectedAccountGroup(period, popularChainIds?) -> (state) => value
selectBalanceChangeBySelectedAccountGroup: jest.fn(() => () => null),
// This selector is used to display the BalanceEmptyState
selectAccountGroupBalanceForEmptyState: jest.fn(() => null),
}));

// Mock homepage redesign feature flag for BalanceEmptyState
// Mock homepage feature flags (BalanceEmptyState and AccountGroupBalance use these)
jest.mock('../../../../../selectors/featureFlagController/homepage', () => ({
selectHomepageRedesignV1Enabled: jest.fn(() => true),
selectHomepageSectionsV1Enabled: jest.fn(() => true),
}));

// This selector is used to determine if the current network is a testnet for BalanceEmptyState display logic
Expand Down Expand Up @@ -44,6 +45,16 @@ jest.mock('../../../../../components/hooks/useAnalytics/useAnalytics', () => ({
}),
}));

// AccountGroupBalance uses listPopularNetworks for balance selectors
jest.mock(
'../../../../hooks/useNetworkEnablement/useNetworkEnablement',
() => ({
useNetworkEnablement: () => ({
listPopularNetworks: () => [],
}),
}),
);

const testState = {
engine: {
backgroundState: {
Expand All @@ -66,7 +77,7 @@ describe('AccountGroupBalance', () => {
selectBalanceChangeBySelectedAccountGroup,
} = jest.requireMock('../../../../../selectors/assets/balances');
(selectBalanceBySelectedAccountGroup as jest.Mock).mockImplementation(
() => null,
() => () => null,
);
(selectAccountGroupBalanceForEmptyState as jest.Mock).mockImplementation(
() => null,
Expand Down Expand Up @@ -96,7 +107,7 @@ describe('AccountGroupBalance', () => {
'../../../../../selectors/assets/balances',
);
(selectBalanceBySelectedAccountGroup as jest.Mock).mockImplementation(
() => ({
() => () => ({
walletId: 'wallet-1',
groupId: 'wallet-1/group-1',
totalBalanceInUserCurrency: 123.45,
Expand Down Expand Up @@ -125,7 +136,7 @@ describe('AccountGroupBalance', () => {

// Mock the regular balance selector to return zero balance data
(selectBalanceBySelectedAccountGroup as jest.Mock).mockImplementation(
() => ({
() => () => ({
walletId: 'wallet-1',
groupId: 'wallet-1/group-1',
totalBalanceInUserCurrency: 0, // Zero on current network
Expand Down Expand Up @@ -172,7 +183,7 @@ describe('AccountGroupBalance', () => {

// Start with zero balance
(selectBalanceBySelectedAccountGroup as jest.Mock).mockImplementation(
() => ({
() => () => ({
walletId: 'wallet-1',
groupId: 'wallet-1/group-1',
totalBalanceInUserCurrency: 0,
Expand All @@ -196,7 +207,7 @@ describe('AccountGroupBalance', () => {

// Update mocks to return non-zero balance (simulating balance fetch completing)
(selectBalanceBySelectedAccountGroup as jest.Mock).mockImplementation(
() => ({
() => () => ({
walletId: 'wallet-1',
groupId: 'wallet-1/group-1',
totalBalanceInUserCurrency: 123.45,
Expand Down Expand Up @@ -227,7 +238,7 @@ describe('AccountGroupBalance', () => {

// Start with zero balance (simulates account with no funds or just switched)
(selectBalanceBySelectedAccountGroup as jest.Mock).mockImplementation(
() => ({
() => () => ({
walletId: 'wallet-1',
groupId: 'wallet-1/group-1',
totalBalanceInUserCurrency: 0,
Expand Down Expand Up @@ -256,7 +267,7 @@ describe('AccountGroupBalance', () => {

// Update mocks to show balance has loaded with funds
(selectBalanceBySelectedAccountGroup as jest.Mock).mockImplementation(
() => ({
() => () => ({
walletId: 'wallet-1',
groupId: 'wallet-1/group-1',
totalBalanceInUserCurrency: 150,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { useCallback, useState, useRef, useEffect } from 'react';
import React, {
useCallback,
useMemo,
useState,
useRef,
useEffect,
} from 'react';
import { View, TouchableOpacity } from 'react-native';
import { useSelector } from 'react-redux';
import Engine from '../../../../../core/Engine';
Expand All @@ -9,8 +15,12 @@ import {
selectBalanceChangeBySelectedAccountGroup,
selectAccountGroupBalanceForEmptyState,
} from '../../../../../selectors/assets/balances';
import { selectHomepageRedesignV1Enabled } from '../../../../../selectors/featureFlagController/homepage';
import {
selectHomepageRedesignV1Enabled,
selectHomepageSectionsV1Enabled,
} from '../../../../../selectors/featureFlagController/homepage';
import { selectEvmChainId } from '../../../../../selectors/networkController';
import { useNetworkEnablement } from '../../../../hooks/useNetworkEnablement/useNetworkEnablement';
import { TEST_NETWORK_IDS } from '../../../../../constants/network';
import SensitiveText, {
SensitiveTextLength,
Expand All @@ -33,14 +43,41 @@ const AccountGroupBalance = () => {
const { PreferencesController } = Engine.context;
const styles = createStyles();
const { formatCurrency } = useFormatters();
const isHomepageSectionsV1Enabled = useSelector(
selectHomepageSectionsV1Enabled,
);
const { popularNetworks } = useNetworkEnablement();

// Stabilize chain IDs by content so selector identity doesn't change every render (avoids max depth / infinite loop).
// FF on: balance for all popular networks; FF off: balance for enabled networks only (selector uses state when undefined).
const popularChainIdsKey = (popularNetworks ?? []).join(',');
const chainIdsForBalance = useMemo(
() =>
isHomepageSectionsV1Enabled ? [...(popularNetworks ?? [])] : undefined,
// popularChainIdsKey stabilizes by content; popularNetworks is a new array ref every render from the hook
// eslint-disable-next-line react-hooks/exhaustive-deps
[isHomepageSectionsV1Enabled, popularChainIdsKey],
);

const groupBalanceSelector = useMemo(
() => selectBalanceBySelectedAccountGroup(chainIdsForBalance),
[chainIdsForBalance],
);
const balanceChange1dSelector = useMemo(
() => selectBalanceChangeBySelectedAccountGroup('1d', chainIdsForBalance),
[chainIdsForBalance],
);
const privacyMode = useSelector(selectPrivacyMode);
const groupBalance = useSelector(selectBalanceBySelectedAccountGroup);
const groupBalance = useSelector(groupBalanceSelector) as {
groupId: string;
totalBalanceInUserCurrency: number;
userCurrency: string;
walletId: string;
} | null;
const accountGroupBalance = useSelector(
selectAccountGroupBalanceForEmptyState,
);
const balanceChange1d = useSelector(
selectBalanceChangeBySelectedAccountGroup('1d'),
);
const balanceChange1d = useSelector(balanceChange1dSelector);
const isHomepageRedesignV1Enabled = useSelector(
selectHomepageRedesignV1Enabled,
);
Expand Down Expand Up @@ -129,10 +166,11 @@ const AccountGroupBalance = () => {
// Check if current network is a testnet
const isCurrentNetworkTestnet = TEST_NETWORK_IDS.includes(selectedChainId);

// Show empty state on accounts with an aggregated mainnet balance of zero
// Show empty state on accounts with an aggregated mainnet balance of zero (redesign + sections v1)
const shouldShowEmptyState =
hasZeroAccountGroupBalance &&
isHomepageRedesignV1Enabled &&
isHomepageSectionsV1Enabled &&
!isCurrentNetworkTestnet;

// Show skeleton while loading: either no groupBalance OR balance not fetched yet
Expand Down
Loading
Loading