diff --git a/.js.env.example b/.js.env.example index 6c0d0e327b0..1c11f591536 100644 --- a/.js.env.example +++ b/.js.env.example @@ -98,3 +98,6 @@ export MM_CHAIN_PERMISSIONS="" #Multichain feature flag specific to UI changes export MM_MULTICHAIN_V1_ENABLED="" + +#Permissions Settings feature flag specific to UI changes +export MM_PERMISSIONS_SETTINGS_V1_ENABLED="" diff --git a/README.md b/README.md index 815748fcafd..08173a32791 100644 --- a/README.md +++ b/README.md @@ -39,18 +39,35 @@ git clone git@github.com:MetaMask/metamask-mobile.git && \ cd metamask-mobile ``` -**Firebase Messaging Setup** +### **Firebase Messaging Setup** -Before running the app, keep in mind that MetaMask uses FCM (Firebase Cloud Message) to empower communications. Based on this, as an external contributor you would preferably need to provide your own FREE Firebase project config file with a matching client for package name `io.metamask`, and update your `google-services.json` file in the `android/app` or `GoogleService-Info.plist` file in the `ios` directory. +MetaMask uses Firebase Cloud Messaging (FCM) to enable app communications. To integrate FCM, you’ll need configuration files for both iOS and Android platforms. + +#### **Configuration Files Required** +- **`GoogleService-Info.plist`** (iOS) +- **`google-services.json`** (Android) + +These files are essential for FCM integration and are automatically generated when running: `yarn start:ios` or `yarn start:android` **External Contributors** -In case you don't have FCM account, you can use `./android/app/google-services-example.json` for Android or `./ios/GoogleServices/GoogleService-Info-example.plist` for iOS and follow the steps below to populate the correct environment variables in the `.env` files (`.ios.env`, `.js.env`, `.android.env`), adding `GOOGLE_SERVICES_B64_ANDROID` or `GOOGLE_SERVICES_B64_IOS` variable depending on the environment you are running the app (ios/android). + +As an external contributor, you need to provide your own Firebase project configuration files: + +1. Create a Free Firebase Project + * Set up a Firebase project in the Firebase Console. + * Configure the project with a client package name matching `io.metamask`. +2. Add Configuration Files + * Update the `google-services.json` and `GoogleService-Info.plist` files in: + * `android/app` (for Android) + * `ios` directory (for iOS) + +In case you don't have FCM account, you can reference the instructions below. These instructions will generate the required `GOOGLE_SERVICES_B64_ANDROID` or `GOOGLE_SERVICES_B64_IOS` environment variables found in: `.ios.env`, `.js.env`, `.android.env`. They can be locally generated from examples files. **Internal Contributors** -We should access the Firebase project config file from 1Password. +As an internal contributor, you can access the shared Firebase project config file from 1Password. Ask around for the correct vault. -The value you should provide to `GOOGLE_SERVICES_B64_ANDROID` or `GOOGLE_SERVICES_B64_IOS` is the base64 encoded version of your Firebase project config file, which can be generated as follows: +The values you should provide to `GOOGLE_SERVICES_B64_ANDROID` or `GOOGLE_SERVICES_B64_IOS` are the base64 encoded versions of the example Firebase project config files. These can also be generated locally: **For Android** ```bash diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index 4793a9745f9..91b7ff961ab 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -34,7 +34,6 @@ import SharedDeeplinkManager from '../../../core/DeeplinkManager/SharedDeeplinkM import branch from 'react-native-branch'; import AppConstants from '../../../core/AppConstants'; import Logger from '../../../util/Logger'; -import { routingInstrumentation } from '../../../util/sentry/utils'; import { connect, useDispatch } from 'react-redux'; import { CURRENT_APP_VERSION, @@ -138,7 +137,13 @@ import Engine from '../../../core/Engine'; import { CHAIN_IDS } from '@metamask/transaction-controller'; import { PopularList } from '../../../util/networks/customNetworks'; import { RpcEndpointType } from '@metamask/network-controller'; -import { trace, TraceName, TraceOperation } from '../../../util/trace'; +import { + endTrace, + trace, + TraceName, + TraceOperation, +} from '../../../util/trace'; +import getUIStartupSpan from '../../../core/Performance/UIStartup'; const clearStackNavigatorOptions = { headerShown: false, @@ -584,6 +589,12 @@ const App = (props) => { const sdkInit = useRef(); const [onboarded, setOnboarded] = useState(false); + trace({ + name: TraceName.NavInit, + parentContext: getUIStartupSpan(), + op: TraceOperation.NavInit, + }); + const triggerSetCurrentRoute = (route) => { dispatch(setCurrentRoute(route)); if (route === 'Wallet' || route === 'BrowserView') { @@ -599,9 +610,10 @@ const App = (props) => { setOnboarded(!!existingUser); try { if (existingUser) { + // This should only be called if the auth type is not password, which is not the case so consider removing it await trace( { - name: TraceName.BiometricAuthentication, + name: TraceName.AppStartBiometricAuthentication, op: TraceOperation.BiometricAuthentication, }, async () => { @@ -624,6 +636,7 @@ const App = (props) => { }), ); } + await Authentication.lockApp({ reset: false }); trackErrorAsAnalytics( 'App: Max Attempts Reached', @@ -632,9 +645,15 @@ const App = (props) => { ); } }; - appTriggeredAuth().catch((error) => { - Logger.error(error, 'App: Error in appTriggeredAuth'); - }); + appTriggeredAuth() + .catch((error) => { + Logger.error(error, 'App: Error in appTriggeredAuth'); + }) + .finally(() => { + endTrace({ name: TraceName.NavInit }); + + endTrace({ name: TraceName.UIStartup }); + }); }, [navigator, queueOfHandleDeeplinkFunctions]); const handleDeeplink = useCallback(({ error, params, uri }) => { @@ -684,8 +703,6 @@ const App = (props) => { }); if (!prevNavigator.current) { - // Setup navigator with Sentry instrumentation - routingInstrumentation.registerNavigationContainer(navigator); // Subscribe to incoming deeplinks // Branch.io documentation: https://help.branch.io/developers-hub/docs/react-native branch.subscribe((opts) => { diff --git a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx index 0d8fde99345..506d7e6e37d 100644 --- a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx @@ -16,6 +16,7 @@ import { } from '../../../util/test/accountsControllerTestUtils'; import { mockNetworkState } from '../../../util/test/network'; import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { AccountSelectorListProps } from './AccountSelectorList.types'; const BUSINESS_ACCOUNT = '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'; const PERSONAL_ACCOUNT = '0xd018538C87232FF95acbCe4870629b75640a78E7'; @@ -82,8 +83,9 @@ const initialState = { const onSelectAccount = jest.fn(); const onRemoveImportedAccount = jest.fn(); - -const AccountSelectorListUseAccounts = () => { +const AccountSelectorListUseAccounts: React.FC = ({ + privacyMode = false, +}) => { const { accounts, ensByAccountAddress } = useAccounts(); return ( { accounts={accounts} ensByAccountAddress={ensByAccountAddress} isRemoveAccountEnabled + privacyMode={privacyMode} /> ); }; @@ -118,7 +121,7 @@ const renderComponent = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any state: any = {}, AccountSelectorListTest = AccountSelectorListUseAccounts, -) => renderWithProvider(, { state }); +) => renderWithProvider(, { state }); describe('AccountSelectorList', () => { beforeEach(() => { @@ -238,4 +241,46 @@ describe('AccountSelectorList', () => { expect(snapTag).toBeDefined(); }); }); + it('Text is not hidden when privacy mode is off', async () => { + const state = { + ...initialState, + privacyMode: false, + }; + + const { queryByTestId } = renderComponent(state); + + await waitFor(() => { + const businessAccountItem = queryByTestId( + `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + ); + + expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); + expect( + within(businessAccountItem).getByText(regex.usd(3200)), + ).toBeDefined(); + + expect(within(businessAccountItem).queryByText('••••••')).toBeNull(); + }); + }); + it('Text is hidden when privacy mode is on', async () => { + const state = { + ...initialState, + privacyMode: true, + }; + + const { queryByTestId } = renderComponent(state); + + await waitFor(() => { + const businessAccountItem = queryByTestId( + `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + ); + + expect(within(businessAccountItem).queryByText(regex.eth(1))).toBeNull(); + expect( + within(businessAccountItem).queryByText(regex.usd(3200)), + ).toBeNull(); + + expect(within(businessAccountItem).getByText('••••••')).toBeDefined(); + }); + }); }); diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx index f2364e63402..30b8241836f 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx @@ -14,7 +14,6 @@ import Cell, { } from '../../../component-library/components/Cells/Cell'; import { InternalAccount } from '@metamask/keyring-api'; import { useStyles } from '../../../component-library/hooks'; -import { selectPrivacyMode } from '../../../selectors/preferencesController'; import { TextColor } from '../../../component-library/components/Texts/Text'; import SensitiveText, { SensitiveTextLength, @@ -52,6 +51,7 @@ const AccountSelectorList = ({ isSelectionDisabled, isRemoveAccountEnabled = false, isAutoScrollEnabled = true, + privacyMode = false, ...props }: AccountSelectorListProps) => { const { navigate } = useNavigation(); @@ -72,7 +72,6 @@ const AccountSelectorList = ({ ); const internalAccounts = useSelector(selectInternalAccounts); - const privacyMode = useSelector(selectPrivacyMode); const getKeyExtractor = ({ address }: Account) => address; const renderAccountBalances = useCallback( diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts b/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts index 4059c710cc9..a2f651c718e 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts @@ -56,4 +56,8 @@ export interface AccountSelectorListProps * Optional boolean to enable removing accounts. */ isRemoveAccountEnabled?: boolean; + /** + * Optional boolean to indicate if privacy mode is enabled. + */ + privacyMode?: boolean; } diff --git a/app/components/UI/PermissionsSummary/PermissionsSummary.styles.ts b/app/components/UI/PermissionsSummary/PermissionsSummary.styles.ts index 84e03643ab3..3c25f4830a7 100644 --- a/app/components/UI/PermissionsSummary/PermissionsSummary.styles.ts +++ b/app/components/UI/PermissionsSummary/PermissionsSummary.styles.ts @@ -30,6 +30,9 @@ const createStyles = (params: { marginRight: 24, marginLeft: 24, }, + bottomButtonsContainer: { + marginTop: 16, + }, actionButtonsContainer: { flex: 0, flexDirection: 'row', @@ -118,7 +121,6 @@ const createStyles = (params: { walletIcon: { alignSelf: 'flex-start' }, dataIcon: { alignSelf: 'flex-start' }, disconnectAllContainer: { - marginTop: 16, marginHorizontal: 24, flexDirection: 'row', }, diff --git a/app/components/UI/PermissionsSummary/PermissionsSummary.tsx b/app/components/UI/PermissionsSummary/PermissionsSummary.tsx index 715e887d48a..f48936e74ef 100644 --- a/app/components/UI/PermissionsSummary/PermissionsSummary.tsx +++ b/app/components/UI/PermissionsSummary/PermissionsSummary.tsx @@ -389,7 +389,7 @@ const PermissionsSummary = ({ {!isNetworkSwitch && renderAccountPermissionsRequestInfoCard()} {renderNetworkPermissionsRequestInfoCard()} - + {isAlreadyConnected && isDisconnectAllShown && (