Skip to content

Commit 03bb01d

Browse files
authored
chore: open What's Happening source articles in the in-app browser (#30126)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** Fixes links opening in `WhatsHappeningSourcesBottomSheet` so article links open in the MetaMask browser instead of the system browser, consistent with the Market Insights feature. <img height="800" alt="Simulator Screenshot - iPhone 17 Pro - 2026-05-13 at 16 52 26" src="https://github.com/user-attachments/assets/d13fe3f7-a5a5-4ec4-aff8-a38b0e50703f" /> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes navigation behavior for external article links and alters analytics emission when URLs are deemed unsafe, which could affect user flow and metrics if routing params or safety checks are misapplied. > > **Overview** > Source article taps in `WhatsHappeningSourcesBottomSheet` now **navigate to the in-app browser** (`Routes.BROWSER.VIEW`) with `newTabUrl`/`timestamp`, instead of calling the OS via `Linking.openURL`. > > A new `fromWhatsHappening` route param is plumbed through `Browser`/`BrowserTab` and used to make the browser close action `goBack()` to the detail view. Analytics/test expectations were updated so **unsafe URLs neither open the browser nor emit the interaction event**. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1c022fe. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 24712bd commit 03bb01d

6 files changed

Lines changed: 51 additions & 14 deletions

File tree

app/components/Views/Browser/Browser.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface BrowserParams {
1010
fromPerps?: boolean;
1111
fromBenefit?: boolean;
1212
fromCard?: boolean;
13+
fromWhatsHappening?: boolean;
1314
linkType?: string;
1415
url?: string;
1516
}

app/components/Views/Browser/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ export const BrowserPure = (props) => {
462462
fromPerps={route.params?.fromPerps}
463463
fromBenefit={route.params?.fromBenefit}
464464
fromCard={route.params?.fromCard}
465+
fromWhatsHappening={route.params?.fromWhatsHappening}
465466
/>
466467
) : (
467468
<DiscoveryTab
@@ -485,6 +486,7 @@ export const BrowserPure = (props) => {
485486
route.params?.fromPerps,
486487
route.params?.fromBenefit,
487488
route.params?.fromCard,
489+
route.params?.fromWhatsHappening,
488490
],
489491
);
490492

app/components/Views/BrowserTab/BrowserTab.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export const BrowserTab: React.FC<BrowserTabProps> = React.memo(
140140
fromPerps,
141141
fromBenefit,
142142
fromCard,
143+
fromWhatsHappening,
143144
}) => {
144145
const navigation = useNavigation();
145146
const { styles } = useStyles(styleSheet, {});
@@ -1294,6 +1295,9 @@ export const BrowserTab: React.FC<BrowserTabProps> = React.memo(
12941295
screen: Routes.CARD.HOME,
12951296
},
12961297
});
1298+
} else if (fromWhatsHappening) {
1299+
// WhatsHappeningDetailView is in the stack navigator so goBack() works correctly.
1300+
navigation.goBack();
12971301
} else {
12981302
// Navigate to TrendingView/TrendingFeed
12991303
// Note: We use explicit navigation instead of goBack() because the browser
@@ -1303,7 +1307,7 @@ export const BrowserTab: React.FC<BrowserTabProps> = React.memo(
13031307
screen: Routes.TRENDING_FEED,
13041308
});
13051309
}
1306-
}, [navigation, fromPerps, fromBenefit, fromCard]);
1310+
}, [navigation, fromPerps, fromBenefit, fromCard, fromWhatsHappening]);
13071311

13081312
const onCancelUrlBar = useCallback(() => {
13091313
hideAutocomplete();

app/components/Views/BrowserTab/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ export type BrowserTabProps = SharedTabProps & {
118118
* Whether browser was opened from Card (e.g. manage card / travel in-app)
119119
*/
120120
fromCard?: boolean;
121+
/**
122+
* Whether browser was opened from the What's Happening detail view
123+
*/
124+
fromWhatsHappening?: boolean;
121125

122126
/**
123127
* Boolean indicating if browser is in fullscreen mode

app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningSourcesBottomSheet.test.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from 'react';
2-
import { Linking } from 'react-native';
32
import { screen, fireEvent } from '@testing-library/react-native';
43
import renderWithProvider from '../../../../util/test/renderWithProvider';
54
import WhatsHappeningSourcesBottomSheet from './WhatsHappeningSourcesBottomSheet';
@@ -13,6 +12,12 @@ const mockCreateEventBuilder = jest.fn((eventName: string) => ({
1312
})),
1413
build: jest.fn(() => ({ category: eventName })),
1514
}));
15+
const mockNavigate = jest.fn();
16+
17+
jest.mock('@react-navigation/native', () => ({
18+
...jest.requireActual('@react-navigation/native'),
19+
useNavigation: () => ({ navigate: mockNavigate }),
20+
}));
1621

1722
jest.mock('../../../hooks/useAnalytics/useAnalytics', () => ({
1823
useAnalytics: () => ({
@@ -61,7 +66,6 @@ jest.mock('../../../UI/MarketInsights/utils/marketInsightsFormatting', () => ({
6166
getFaviconUrl: jest.fn((url: string) => `https://favicon/${url}`),
6267
}));
6368

64-
const mockOpenURL = jest.spyOn(Linking, 'openURL').mockResolvedValue(undefined);
6569
const mockIsSafeUrl = jest.requireMock(
6670
'../../../UI/MarketInsights/utils/marketInsightsFormatting',
6771
).isSafeUrl;
@@ -119,7 +123,7 @@ describe('WhatsHappeningSourcesBottomSheet', () => {
119123
expect(screen.getByText('cointelegraph.com')).toBeOnTheScreen();
120124
});
121125

122-
it('opens the article URL when a row is pressed and URL is safe', () => {
126+
it('opens the article URL in the in-app browser when a row is pressed and URL is safe', () => {
123127
renderWithProvider(
124128
<WhatsHappeningSourcesBottomSheet
125129
onClose={jest.fn()}
@@ -130,10 +134,20 @@ describe('WhatsHappeningSourcesBottomSheet', () => {
130134
/>,
131135
);
132136
fireEvent.press(screen.getByText('coindesk.com'));
133-
expect(mockOpenURL).toHaveBeenCalledWith('https://coindesk.com/fed-pauses');
137+
expect(mockNavigate).toHaveBeenCalledWith(
138+
'BrowserTabHome',
139+
expect.objectContaining({
140+
screen: 'BrowserView',
141+
params: expect.objectContaining({
142+
newTabUrl: 'https://coindesk.com/fed-pauses',
143+
fromTrending: true,
144+
fromWhatsHappening: true,
145+
}),
146+
}),
147+
);
134148
});
135149

136-
it('does not open the URL when isSafeUrl returns false', () => {
150+
it('does not open the browser when isSafeUrl returns false', () => {
137151
mockIsSafeUrl.mockReturnValue(false);
138152
renderWithProvider(
139153
<WhatsHappeningSourcesBottomSheet
@@ -145,7 +159,7 @@ describe('WhatsHappeningSourcesBottomSheet', () => {
145159
/>,
146160
);
147161
fireEvent.press(screen.getByText('coindesk.com'));
148-
expect(mockOpenURL).not.toHaveBeenCalled();
162+
expect(mockNavigate).not.toHaveBeenCalled();
149163
});
150164

151165
it('renders the sheet title', () => {
@@ -205,7 +219,7 @@ describe('WhatsHappeningSourcesBottomSheet', () => {
205219
);
206220
});
207221

208-
it('still tracks the source_click interaction even when the URL is unsafe', () => {
222+
it('does not track the interaction when the URL is unsafe', () => {
209223
mockIsSafeUrl.mockReturnValue(false);
210224
renderWithProvider(
211225
<WhatsHappeningSourcesBottomSheet
@@ -217,7 +231,7 @@ describe('WhatsHappeningSourcesBottomSheet', () => {
217231
/>,
218232
);
219233
fireEvent.press(screen.getByText('coindesk.com'));
220-
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
234+
expect(mockCreateEventBuilder).not.toHaveBeenCalledWith(
221235
MetaMetricsEvents.WHATS_HAPPENING_INTERACTED,
222236
);
223237
});

app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningSourcesBottomSheet.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useCallback, useRef } from 'react';
2-
import { Linking } from 'react-native';
32
import { ScrollView } from 'react-native-gesture-handler';
3+
import { useNavigation, NavigationProp } from '@react-navigation/native';
4+
import type { RootStackParamList } from '../../../../core/NavigationService/types';
45
import { useTailwind } from '@metamask/design-system-twrnc-preset';
56
import {
67
FontWeight,
@@ -17,6 +18,7 @@ import ArticleRow from '../../../UI/MarketInsights/components/ArticleRow';
1718
import { isSafeUrl } from '../../../UI/MarketInsights/utils/marketInsightsFormatting';
1819
import { MetaMetricsEvents } from '../../../../core/Analytics';
1920
import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
21+
import Routes from '../../../../constants/navigation/Routes';
2022
import {
2123
WhatsHappeningInteractionType,
2224
type WhatsHappeningSourceValue,
@@ -38,9 +40,13 @@ const WhatsHappeningSourcesBottomSheet: React.FC<
3840
const tw = useTailwind();
3941
const bottomSheetRef = useRef<BottomSheetRef>(null);
4042
const { trackEvent, createEventBuilder } = useAnalytics();
43+
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
4144

4245
const handleSourcePress = useCallback(
4346
(url: string) => {
47+
if (!isSafeUrl(url)) {
48+
return;
49+
}
4450
trackEvent(
4551
createEventBuilder(MetaMetricsEvents.WHATS_HAPPENING_INTERACTED)
4652
.addProperties({
@@ -50,11 +56,17 @@ const WhatsHappeningSourcesBottomSheet: React.FC<
5056
})
5157
.build(),
5258
);
53-
if (isSafeUrl(url)) {
54-
Linking.openURL(url);
55-
}
59+
navigation.navigate(Routes.BROWSER.HOME, {
60+
screen: Routes.BROWSER.VIEW,
61+
params: {
62+
newTabUrl: url,
63+
timestamp: Date.now(),
64+
fromTrending: true,
65+
fromWhatsHappening: true,
66+
},
67+
});
5668
},
57-
[item, cardIndex, source, trackEvent, createEventBuilder],
69+
[item, cardIndex, source, trackEvent, createEventBuilder, navigation],
5870
);
5971

6072
return (

0 commit comments

Comments
 (0)