diff --git a/.github/workflows/update-latest-build-version.yml b/.github/workflows/update-latest-build-version.yml index 4d43acd8881d..ffcf5ebc7a4c 100644 --- a/.github/workflows/update-latest-build-version.yml +++ b/.github/workflows/update-latest-build-version.yml @@ -7,7 +7,6 @@ ############################################################################################## name: Update Latest Build Version - on: workflow_dispatch: inputs: @@ -15,37 +14,32 @@ on: description: 'The base branch, tag, or SHA for git operations and the pull request.' required: true jobs: - generate-build-version: - uses: MetaMask/metamask-mobile-build-version/.github/workflows/metamask-mobile-build-version.yml@v0.2.0 - permissions: - id-token: write + generate-build-version: + uses: MetaMask/metamask-mobile-build-version/.github/workflows/metamask-mobile-build-version.yml@v0.2.0 + permissions: + id-token: write + + bump-version: + runs-on: ubuntu-latest + needs: generate-build-version + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ inputs.base-branch }} + token: ${{ secrets.PR_TOKEN }} - bump-version: - runs-on: ubuntu-latest - needs: generate-build-version - permissions: - contents: write - id-token: write - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - ref: ${{ inputs.base-branch }} - token: ${{ secrets.PR_TOKEN }} - - - name: Bump script - env: - HEAD_REF: ${{ inputs.base-branch }} - run: | - ./scripts/set-build-version.sh ${{ needs.generate-build-version.outputs.build-version }} - git diff - git config user.name metamaskbot - git config user.email metamaskbot@users.noreply.github.com - git add bitrise.yml - git add package.json - git add ios/MetaMask.xcodeproj/project.pbxproj - git add android/app/build.gradle - git commit -m "Bump version number to ${{ needs.generate-build-version.outputs.build-version }}" - git push origin HEAD:"$HEAD_REF" --force - - \ No newline at end of file + - name: Bump script + env: + HEAD_REF: ${{ inputs.base-branch }} + run: | + ./scripts/set-build-version.sh ${{ needs.generate-build-version.outputs.build-version }} + git diff + git config user.name metamaskbot + git config user.email metamaskbot@users.noreply.github.com + git add bitrise.yml + git add package.json + git add ios/MetaMask.xcodeproj/project.pbxproj + git add android/app/build.gradle + git commit -m "Bump version number to ${{ needs.generate-build-version.outputs.build-version }}" + git push origin HEAD:"$HEAD_REF" --force-with-lease diff --git a/ReactotronConfig.js b/ReactotronConfig.js new file mode 100644 index 000000000000..059efc3ee332 --- /dev/null +++ b/ReactotronConfig.js @@ -0,0 +1,3 @@ +import Reactotron, { networking } from 'reactotron-react-native'; + +Reactotron.configure().use(networking()).connect(); diff --git a/app/components/Approvals/InstallSnapApproval/InstallSnapApproval.tsx b/app/components/Approvals/InstallSnapApproval/InstallSnapApproval.tsx index c50ec70f59a4..93cbca44abc0 100644 --- a/app/components/Approvals/InstallSnapApproval/InstallSnapApproval.tsx +++ b/app/components/Approvals/InstallSnapApproval/InstallSnapApproval.tsx @@ -141,6 +141,11 @@ const InstallSnapApproval = () => { const connectSnapIds = getConnectSnapIds(approvalRequest, currentPermissions); const snapId = installSnapId ?? connectSnapIds[0]; + + if (!snapId) { + return null; + } + const snapName = getSnapMetadata(snapId).name; // TODO: This component should support connecting to multiple Snaps at once. diff --git a/app/core/AppStateEventListener.test.ts b/app/core/AppStateEventListener.test.ts index 9009366e5f58..ca88f23b7b53 100644 --- a/app/core/AppStateEventListener.test.ts +++ b/app/core/AppStateEventListener.test.ts @@ -76,7 +76,6 @@ describe('AppStateEventListener', () => { .mockReturnValue({} as unknown as ReduxStore); const mockAttribution = { attributionId: 'test123', - utm: 'test_utm', utm_source: 'source', utm_medium: 'medium', utm_campaign: 'campaign', diff --git a/app/core/AppStateEventListener.ts b/app/core/AppStateEventListener.ts index 565c82a5c377..12d10440d80f 100644 --- a/app/core/AppStateEventListener.ts +++ b/app/core/AppStateEventListener.ts @@ -73,12 +73,12 @@ export class AppStateEventListener { MetaMetricsEvents.APP_OPENED, ); if (attribution) { - const { attributionId, utm, ...utmParams } = attribution; + const { attributionId, ...utmParams } = attribution; DevLogger.log( - `AppStateManager:: processAppStateChange:: sending event 'APP_OPENED' attributionId=${attribution.attributionId} utm=${attribution.utm}`, + `AppStateManager:: processAppStateChange:: sending event 'APP_OPENED' attributionId=${attribution.attributionId}`, utmParams, ); - appOpenedEventBuilder.addProperties({ attributionId, ...utmParams }); + appOpenedEventBuilder.addProperties({ ...attribution }); } metrics.trackEvent(appOpenedEventBuilder.build()); } catch (error) { diff --git a/app/core/DeeplinkManager/ParseManager/extractURLParams.test.ts b/app/core/DeeplinkManager/ParseManager/extractURLParams.test.ts index 423f78628a47..b29bda54ae75 100644 --- a/app/core/DeeplinkManager/ParseManager/extractURLParams.test.ts +++ b/app/core/DeeplinkManager/ParseManager/extractURLParams.test.ts @@ -1,6 +1,5 @@ import qs from 'qs'; import { Alert } from 'react-native'; -import UrlParser from 'url-parse'; import { strings } from '../../../../locales/i18n'; import { PROTOCOLS } from '../../../constants/deeplinks'; import extractURLParams from './extractURLParams'; @@ -9,15 +8,6 @@ jest.mock('qs', () => ({ parse: jest.fn(), })); -jest.mock('url-parse', () => { - const mockUrlParser = jest.fn(); - - return { - __esModule: true, - default: mockUrlParser, - }; -}); - jest.mock('react-native', () => ({ Alert: { alert: jest.fn(), @@ -25,13 +15,14 @@ jest.mock('react-native', () => ({ })); describe('extractURLParams', () => { - const mockUrlParser = UrlParser as jest.MockedClass; + // const mockUrlParser = UrlParser as jest.MockedClass; const mockQs = qs as jest.Mocked; beforeEach(() => { jest.clearAllMocks(); }); - it('should correctly extract parameters from a valid URL with query parameters', () => { + + it('extracts parameters from a valid URL with query parameters', () => { const url = `${PROTOCOLS.DAPP}/https://example.com?uri=test&redirect=true&channelId=123&comm=test&pubkey=abc&v=2`; const expectedParams = { uri: 'test', @@ -41,18 +32,42 @@ describe('extractURLParams', () => { sdkVersion: '', channelId: '123', comm: 'test', + pubkey: 'abc', v: '2', attributionId: '', - utm: '', + utm_source: '', + utm_medium: '', + utm_campaign: '', + utm_term: '', + utm_content: '', }; - mockUrlParser.mockImplementation( - () => - ({ - query: - '?uri=test&redirect=true&channelId=123&comm=test&pubkey=abc&v=2', - } as unknown as UrlParser), - ); + mockQs.parse.mockReturnValue(expectedParams); + + const { params } = extractURLParams(url); + + expect(params).toEqual(expectedParams); + }); + + it('extracts UTM parameters from a URL', () => { + const url = `${PROTOCOLS.DAPP}/https://example.com?utm_source=facebook&utm_medium=social&utm_campaign=summer_sale&utm_term=wallet&utm_content=banner`; + const expectedParams = { + uri: '', + redirect: '', + originatorInfo: '', + rpc: '', + sdkVersion: '', + channelId: '', + comm: '', + v: '', + pubkey: '', + attributionId: '', + utm_source: 'facebook', + utm_medium: 'social', + utm_campaign: 'summer_sale', + utm_term: 'wallet', + utm_content: 'banner', + }; mockQs.parse.mockReturnValue(expectedParams); @@ -61,16 +76,9 @@ describe('extractURLParams', () => { expect(params).toEqual(expectedParams); }); - it('should return an empty params object when the URL has no query parameters', () => { + it('returns an empty params object when the URL has no query parameters', () => { const url = `${PROTOCOLS.DAPP}/https://example.com`; - mockUrlParser.mockImplementation( - () => - ({ - query: '', - } as unknown as UrlParser), - ); - const { params } = extractURLParams(url); expect(params).toEqual({ @@ -84,21 +92,18 @@ describe('extractURLParams', () => { pubkey: '', v: '', attributionId: '', - utm: '', + utm_source: '', + utm_medium: '', + utm_campaign: '', + utm_term: '', + utm_content: '', }); }); - it('should handle invalid query parameters and show an alert when parsing fails', () => { + it('handles invalid query parameters and shows an alert when parsing fails', () => { const url = `${PROTOCOLS.DAPP}/https://example.com?invalid=param`; const errorMessage = 'Invalid query parameter'; - mockUrlParser.mockImplementation( - () => - ({ - query: '?invalid=param', - } as unknown as UrlParser), - ); - mockQs.parse.mockImplementation(() => { throw new Error(errorMessage); }); @@ -118,7 +123,11 @@ describe('extractURLParams', () => { pubkey: '', v: '', attributionId: '', - utm: '', + utm_source: '', + utm_medium: '', + utm_campaign: '', + utm_term: '', + utm_content: '', }); expect(alertSpy).toHaveBeenCalledWith( @@ -127,7 +136,7 @@ describe('extractURLParams', () => { ); }); - it('should correctly parse and extract parameters from a URL with valid query parameters', () => { + it('parses and extracts parameters from a URL with valid query parameters', () => { const url = `${PROTOCOLS.DAPP}/https://example.com?uri=test&redirect=false&channelId=456&comm=other&pubkey=xyz`; const expectedParams = { uri: 'test', @@ -140,15 +149,40 @@ describe('extractURLParams', () => { sdkVersion: '', pubkey: 'xyz', attributionId: '', - utm: '', + utm_source: '', + utm_medium: '', + utm_campaign: '', + utm_term: '', + utm_content: '', }; - mockUrlParser.mockImplementation( - () => - ({ - query: '?uri=test&redirect=false&channelId=456&comm=other&pubkey=xyz', - } as unknown as UrlParser), - ); + mockQs.parse.mockReturnValue(expectedParams); + + const { params } = extractURLParams(url); + + expect(params).toEqual(expectedParams); + }); + + it('extracts parameters from a valid URL with duplicate query parameters', () => { + // pubkey and comm is duplicated + const url = `${PROTOCOLS.DAPP}/https://example.com?uri=test&redirect=true&channelId=123&comm=test&comm=test&pubkey=abc&v=2&pubkey=abc`; + const expectedParams = { + uri: 'test', + redirect: 'true', + originatorInfo: '', + rpc: '', + sdkVersion: '', + channelId: '123', + comm: 'test', + pubkey: 'abc', + v: '2', + attributionId: '', + utm_source: '', + utm_medium: '', + utm_campaign: '', + utm_term: '', + utm_content: '', + }; mockQs.parse.mockReturnValue(expectedParams); diff --git a/app/core/DeeplinkManager/ParseManager/extractURLParams.ts b/app/core/DeeplinkManager/ParseManager/extractURLParams.ts index 4d992b6c3e4c..bb782154987b 100644 --- a/app/core/DeeplinkManager/ParseManager/extractURLParams.ts +++ b/app/core/DeeplinkManager/ParseManager/extractURLParams.ts @@ -3,8 +3,6 @@ import { Alert } from 'react-native'; import UrlParser from 'url-parse'; import { strings } from '../../../../locales/i18n'; import { PROTOCOLS } from '../../../constants/deeplinks'; -import Logger from '../../../util/Logger'; -import DevLogger from '../../SDKConnect/utils/DevLogger'; export interface DeeplinkUrlParams { uri: string; @@ -20,7 +18,11 @@ export interface DeeplinkUrlParams { originatorInfo?: string; request?: string; attributionId?: string; - utm?: string; + utm_source?: string; + utm_medium?: string; + utm_campaign?: string; + utm_term?: string; + utm_content?: string; account?: string; // This is the format => "address@chainId" } @@ -42,21 +44,23 @@ function extractURLParams(url: string) { channelId: '', comm: '', attributionId: '', - utm: '', + utm_source: '', + utm_medium: '', + utm_campaign: '', + utm_term: '', + utm_content: '', }; - DevLogger.log(`extractParams:: urlObj`, urlObj); - if (urlObj.query.length) { try { - params = qs.parse( - urlObj.query.substring(1), - ) as unknown as DeeplinkUrlParams; + // Use arrayLimit: 1 to prevent arrays from being returned for duplicate parameters + const parsedParams = qs.parse(urlObj.query.substring(1), { + arrayLimit: 99, + }); + params = { ...params, ...parsedParams }; if (params.message) { - Logger.log('extractParams:: message before...: ', params.message); params.message = params.message?.replace(/ /g, '+'); - Logger.log('extractParams:: message after: ', params.message); } } catch (e) { if (e) Alert.alert(strings('deeplink.invalid'), e.toString()); diff --git a/app/core/processAttribution.test.tsx b/app/core/processAttribution.test.tsx index 16a0c2307955..15bffbf59cb5 100644 --- a/app/core/processAttribution.test.tsx +++ b/app/core/processAttribution.test.tsx @@ -1,7 +1,6 @@ import { store } from '../store'; import extractURLParams from './DeeplinkManager/ParseManager/extractURLParams'; import { processAttribution } from './processAttribution'; -import Logger from '../util/Logger'; jest.mock('../store', () => ({ store: { @@ -9,9 +8,9 @@ jest.mock('../store', () => ({ }, })); -jest.mock('./DeeplinkManager/ParseManager/extractURLParams', () => jest.fn()); -jest.mock('../util/Logger', () => ({ - error: jest.fn(), +jest.mock('./DeeplinkManager/ParseManager/extractURLParams', () => ({ + __esModule: true, + default: jest.fn(), })); describe('processAttribution', () => { @@ -26,28 +25,26 @@ describe('processAttribution', () => { (extractURLParams as jest.Mock).mockReturnValue({ params: { attributionId: 'test123', - utm: JSON.stringify({ - source: 'twitter', - medium: 'social', - campaign: 'cmp-57731027-afbf09/', - term: null, - content: null, - }), + utm_source: 'facebook', + utm_medium: 'social', + utm_campaign: 'summer_sale', + utm_term: 'wallet', + utm_content: 'banner', }, }); const result = processAttribution({ - currentDeeplink: 'metamask://connect?attributionId=test123&utm=...', + currentDeeplink: + 'metamask://connect?attributionId=test123&utm_source=facebook&utm_medium=social&utm_campaign=summer_sale&utm_term=wallet&utm_content=banner', store, }); expect(result).toEqual({ attributionId: 'test123', - utm: expect.any(String), - utm_source: 'twitter', + utm_source: 'facebook', utm_medium: 'social', - utm_campaign: 'cmp-57731027-afbf09/', - utm_term: null, - utm_content: null, + utm_campaign: 'summer_sale', + utm_term: 'wallet', + utm_content: 'banner', }); }); @@ -57,7 +54,8 @@ describe('processAttribution', () => { }); const result = processAttribution({ - currentDeeplink: 'metamask://connect?attributionId=test123&utm=...', + currentDeeplink: + 'metamask://connect?attributionId=test123&utm_source=facebook&utm_medium=social&utm_campaign=summer_sale', store, }); expect(result).toBeUndefined(); @@ -79,56 +77,55 @@ describe('processAttribution', () => { (extractURLParams as jest.Mock).mockReturnValue({ params: { attributionId: 'test123', - utm: JSON.stringify({ - source: 'twitter', - medium: 'social', - }), + utm_source: 'facebook', + utm_medium: 'social', + utm_campaign: '', + utm_term: '', + utm_content: '', }, }); const result = processAttribution({ - currentDeeplink: 'metamask://connect?attributionId=test123&utm=...', + currentDeeplink: + 'metamask://connect?attributionId=test123&utm_source=facebook&utm_medium=social', store, }); expect(result).toEqual({ attributionId: 'test123', - utm: expect.any(String), - utm_source: 'twitter', + utm_source: 'facebook', utm_medium: 'social', - utm_campaign: undefined, - utm_term: undefined, - utm_content: undefined, + utm_campaign: '', + utm_term: '', + utm_content: '', }); }); - it('handles JSON parsing errors gracefully', () => { + it('handles empty UTM parameters gracefully', () => { (store.getState as jest.Mock).mockReturnValue({ security: { dataCollectionForMarketing: true }, }); (extractURLParams as jest.Mock).mockReturnValue({ params: { attributionId: 'test123', - utm: 'invalid-json', + utm_source: '', + utm_medium: '', + utm_campaign: '', + utm_term: '', + utm_content: '', }, }); const result = processAttribution({ - currentDeeplink: - 'metamask://connect?attributionId=test123&utm=invalid-json', + currentDeeplink: 'metamask://connect?attributionId=test123', store, }); expect(result).toEqual({ attributionId: 'test123', - utm: 'invalid-json', - utm_source: undefined, - utm_medium: undefined, - utm_campaign: undefined, - utm_term: undefined, - utm_content: undefined, + utm_source: '', + utm_medium: '', + utm_campaign: '', + utm_term: '', + utm_content: '', }); - expect(Logger.error).toHaveBeenCalledWith( - expect.any(Error), - expect.any(Error), - ); }); }); diff --git a/app/core/processAttribution.tsx b/app/core/processAttribution.tsx index c665f4f049bb..437edb565cc2 100644 --- a/app/core/processAttribution.tsx +++ b/app/core/processAttribution.tsx @@ -2,7 +2,6 @@ import extractURLParams from './DeeplinkManager/ParseManager/extractURLParams'; import { RootState } from '../reducers'; import { Store } from 'redux'; import Logger from '../util/Logger'; -import DevLogger from './SDKConnect/utils/DevLogger'; interface ProcessAttributionParams { currentDeeplink: string | null; @@ -12,7 +11,7 @@ interface ProcessAttributionParams { interface AttributionResult { attributionId?: string; - utm?: string; + utm_source?: string; utm_medium?: string; utm_campaign?: string; @@ -26,32 +25,25 @@ export function processAttribution({ }: ProcessAttributionParams): AttributionResult | undefined { const { security } = store.getState(); if (!security.dataCollectionForMarketing) { + Logger.log( + 'processAttribution:: dataCollectionForMarketing is false, returning undefined', + ); return undefined; } if (currentDeeplink) { const { params } = extractURLParams(currentDeeplink); - const attributionId = params.attributionId || undefined; - const utm = params.utm || undefined; - let utm_source, utm_medium, utm_campaign, utm_term, utm_content; - - if (utm) { - try { - const utmParams = JSON.parse(utm); - DevLogger.log('processAttribution:: UTM params', utmParams); - utm_source = utmParams.source; - utm_medium = utmParams.medium; - utm_campaign = utmParams.campaign; - utm_term = utmParams.term; - utm_content = utmParams.content; - } catch (error) { - Logger.error(new Error('Error parsing UTM params'), error); - } - } + const { + attributionId, + utm_source, + utm_medium, + utm_campaign, + utm_term, + utm_content, + } = params; return { attributionId, - utm, utm_source, utm_medium, utm_campaign, diff --git a/babel.config.js b/babel.config.js index 7c6d70335d9f..ff37d2bcfabd 100644 --- a/babel.config.js +++ b/babel.config.js @@ -24,6 +24,7 @@ module.exports = { ], 'transform-inline-environment-variables', 'react-native-reanimated/plugin', + 'react-native-worklets-core/plugin', ], overrides: [ { diff --git a/docs/readme/api-logging.md b/docs/readme/api-logging.md index 208f7ffcc956..fc10601610c4 100644 --- a/docs/readme/api-logging.md +++ b/docs/readme/api-logging.md @@ -21,11 +21,13 @@ LOG_API_CALLS=true yarn start:android 1. Open a new terminal window 2. Run the following command: + ```bash yarn start:api-logging-server ``` The monitor will start and display: + - The port it's running on - Detailed logs for all API calls including: - Request method and URL @@ -53,11 +55,13 @@ The API call logging functionality is implemented using the following components 4. The API monitor script uses `mockttp` to intercept and log all API calls When using the API monitor, you'll see detailed logs in the console showing: + - All API requests with their methods, URLs, headers, and bodies - All API responses with their status codes, headers, and bodies - Pretty-formatted JSON for better readability This is particularly useful for: + - Debugging API-related issues - Understanding the network traffic of the app -- Verifying that API calls are being made correctly \ No newline at end of file +- Verifying that API calls are being made correctly diff --git a/docs/readme/debugging.md b/docs/readme/debugging.md index cda2936cb613..7d8e0878426f 100644 --- a/docs/readme/debugging.md +++ b/docs/readme/debugging.md @@ -31,3 +31,29 @@ For reference on how to use `Redux DevTools`, refer to [Redux DevTools](https:// - Make sure the value of `webviewDebuggingEnabled` prop on the Webview component is `true` - Open Safari on your desktop - Go to the menu Develop -> [Your device] -> [Website] + +## Debugging Network Requests using Reactotron + +Reactotron is already configured in the app and can be used to monitor API calls + +### Install the Reactotron Desktop App + +```bash +brew install --cask reactotron +``` + +### Start monitoring + +- Open Reactotron +- Refresh simulator +- API logs should appear under Timeline tab in Reactotron. + +### Troubleshooting (Android) + +If Reactotron isn’t picking up connections on Android, run: + +```bash +adb reverse tcp:9090 tcp:9090 +``` + +Then refresh the app again. diff --git a/docs/readme/performance.md b/docs/readme/performance.md index ff965e91a7cd..7fc28e1951d7 100644 --- a/docs/readme/performance.md +++ b/docs/readme/performance.md @@ -7,6 +7,7 @@ This guide aims to help you navigate this complex landscape by providing best pr - [Re-renders and component optimization](#re-renders-and-component-optimization) - [Tool(s) for identifying re-renders](#tools-for-identifying-re-renders) - [Automatically optimizing React](#automatically-optimizing-react) +- [Worklets (Background JS Threads)](#worklets-background-js-threads) By understanding these areas and applying the recommended practices, you can create a more efficient and responsive application that provides a better user experience. @@ -201,3 +202,345 @@ module.exports = { ### Troubleshooting React Compiler By default, Metro may cache parts of the compiled code especially when using the `yarn watch` command. To perform a clean build, run `yarn watch:clean` instead. You can also verify that the file is picked up by React Compiler by adding a log in the `sources` function to confirm the match. + +## Worklets (background JS threads) + +[Worklets](https://github.com/margelo/react-native-worklets-core) are small JavaScript functions that can be executed on a separate JavaScript Thread so that they don't clog the main JS Thread. Perfect for heavy computations and processing. Find out how to use them below. + +> **Note on Usage**: Worklets are meant to unclog the main JS thread, not to speed up the computational process itself. By unlcoging the main JS thread, the intent is to improve responsiveness (reduce FPS drops). + +## runAsync + +Purpose: `runAsync` is used to execute code on a different thread without blocking the main JS thread. + +When to use: Use it to offload functions to a background thread. + +Example: + +``` +import { Worklets } from 'react-native-worklets-core'; +... +const fibonacci = (num: number): number => { + 'worklet' + if (num <= 1) return num; + let prev = 0, curr = 1; + for (let i = 2; i <= num; i++) { + let next = prev + curr; + prev = curr; + curr = next; + } + return curr; +} + +const context = Worklets.defaultContext +const result = await context.runAsync(() => { + 'worklet' + return fibonacci(50) +}) +console.log(`Fibonacci of 50 is ${result}`) +``` + +## runOnJS +Purpose: `runOnJS` is used to call a JavaScript function from within a worklet. Since worklets run on a separate thread, they cannot directly call JavaScript functions. `runOnJS` bridges this gap by allowing you to invoke JavaScript functions on the JavaScript thread. + +When to use: Use it when you need to communicate from the worklet thread back to the JavaScript thread. + +Example: +``` +import { runOnJS } from 'react-native-worklets-core'; +... +const [age, setAge] = useState(30) + +function something() { + 'worklet' + runOnJS(() => setAge(50)) +} +``` + +## useWorklet + +Purpose: `useWorklet` is a hook that allows you to define a worklet function directly within your React component. It ensures that the function is properly marked as a worklet and can run on the worklet thread. + +When to use: Use this hook on a component when you need to define a function that will execute on the worklet thread. + +Example: +``` +import { useWorklet } from 'react-native-worklets-core'; + +function MyComponent() { + const myWorklet = useWorklet(() => { + 'worklet'; + console.log('This is running on the worklet thread'); + }, []); + + return ( +