Skip to content

Commit 6ae9023

Browse files
chore(runway): cherry-pick feat: show legacy ios login warning prompt cp-7.71.0 (#27941)
- feat: show legacy ios login warning prompt cp-7.71.0 (#27875) <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Add warning prompt for ios <17.4 for google login Supports the fix for: https://github.com/MetaMask/MetaMask-planning/issues/7148 Part 1/ 4 - #27741 Part 2/ 4 - #27848 Part 3/ 4 - #27850 (deferred to 7.72.0) Part 4/ 4 - #27875 <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **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: Add warning prompt for ios <17.4 for google login ## **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] --> For < iOS 17.4 https://github.com/user-attachments/assets/f6f3a031-82cc-486d-af5f-e6e1bbc7ed10 For >= iOS 17.4 https://github.com/user-attachments/assets/2cdc0bf3-d59b-4858-be81-baae5e0a4dd2 ## **Pre-merge author checklist** - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] 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. ## **Pre-merge reviewer checklist** - [ ] 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** > Modifies the onboarding social login path by inserting a conditional pre-login warning and new navigation helper, which could affect Google login flow timing/navigation on iOS devices. Changes are localized but touch user authentication entrypoints and analytics tracking. > > **Overview** > Adds an **iOS < 17.4 warning gate** before starting Google OAuth during onboarding (both create and import flows), showing a non-interactable `SuccessErrorSheet` that must be acknowledged before proceeding. > > Introduces `Device.comparePlatformVersionTo()` (using `compare-versions`) and a reusable `navigateToSuccessErrorSheetPromise()` helper to await sheet dismissal, plus a new MetaMetrics event (`WALLET_GOOGLE_IOS_WARNING_VIEWED`) and localized warning copy. > > Updates onboarding tests to mock the new device helper/navigation and to assert the warning sheet + tracking fire before continuing with Google login. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3b43b83. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> [002d91a](002d91a) Co-authored-by: ieow <4881057+ieow@users.noreply.github.com>
1 parent 9c40658 commit 6ae9023

7 files changed

Lines changed: 420 additions & 10 deletions

File tree

app/components/Views/Onboarding/index.test.tsx

Lines changed: 202 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import Routes from '../../../constants/navigation/Routes';
7171
import { ONBOARDING, PREVIOUS_SCREEN } from '../../../constants/navigation';
7272
import { strings } from '../../../../locales/i18n';
7373
import { OAuthError, OAuthErrorType } from '../../../core/OAuthService/error';
74+
import { IconName } from '../../../component-library/components/Icons/Icon';
7475
import { captureException } from '@sentry/react-native';
7576
import Logger from '../../../util/Logger';
7677
import { MIGRATION_ERROR_HAPPENED } from '../../../constants/storage';
@@ -92,9 +93,22 @@ jest.mock('../../../util/test/utils', () => ({
9293
import { fetch as netInfoFetch } from '@react-native-community/netinfo';
9394

9495
const mockNetInfoFetch = netInfoFetch as jest.Mock;
96+
const mockNavigate = jest.fn();
97+
const mockReplace = jest.fn();
98+
const mockGoBack = jest.fn();
9599

96100
// Helper to flush all pending promises
97101
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
102+
const IOS_GOOGLE_WARNING_TITLE = strings('error_sheet.ios_need_update_title');
103+
const IOS_GOOGLE_WARNING_BUTTON = strings('error_sheet.ios_need_update_button');
104+
105+
const getIosGoogleWarningSheetCall = () =>
106+
mockNavigate.mock.calls.find(
107+
([route, params]) =>
108+
route === Routes.MODAL.ROOT_MODAL_FLOW &&
109+
params?.screen === Routes.SHEET.SUCCESS_ERROR_SHEET &&
110+
params?.params?.title === IOS_GOOGLE_WARNING_TITLE,
111+
);
98112

99113
const mockInitialState = {
100114
engine: {
@@ -127,13 +141,22 @@ const mockInitialStateWithExistingUserAndPassword = {
127141
},
128142
};
129143

130-
jest.mock('../../../util/device', () => ({
131-
isLargeDevice: jest.fn(),
132-
isIphoneX: jest.fn(),
133-
isAndroid: jest.fn(),
134-
isIos: jest.fn(),
135-
isMediumDevice: jest.fn(),
136-
}));
144+
jest.mock('../../../util/device', () => {
145+
const mockDevice = {
146+
isLargeDevice: jest.fn(),
147+
isIphoneX: jest.fn(),
148+
isAndroid: jest.fn(),
149+
isIos: jest.fn(),
150+
isMediumDevice: jest.fn(),
151+
comparePlatformVersionTo: jest.fn().mockReturnValue(1),
152+
};
153+
154+
return {
155+
__esModule: true,
156+
default: mockDevice,
157+
...mockDevice,
158+
};
159+
});
137160

138161
// expo library are not supported in jest ( unless using jest-expo as preset ), so we need to mock them
139162
jest.mock('../../../core/OAuthService/OAuthLoginHandlers', () => ({
@@ -276,13 +299,12 @@ jest.mock('../../../core/OAuthService/OAuthLoginHandlers/constants', () => ({
276299
},
277300
}));
278301

279-
const mockNavigate = jest.fn();
280-
const mockReplace = jest.fn();
281302
const mockNav = {
282303
navigate: mockNavigate,
283304
replace: mockReplace,
284305
reset: jest.fn(),
285306
setOptions: jest.fn(),
307+
goBack: mockGoBack,
286308
dispatch: jest.fn((action) => {
287309
if (action.type === 'REPLACE') {
288310
mockReplace(action.payload.name, action.payload.params);
@@ -956,10 +978,13 @@ describe('Onboarding', () => {
956978
beforeEach(() => {
957979
mockSeedlessOnboardingEnabled.mockReturnValue(true);
958980
(StorageWrapper.getItem as jest.Mock).mockResolvedValue(null);
981+
(Device.isIos as jest.Mock).mockReturnValue(false);
982+
(Device.comparePlatformVersionTo as jest.Mock).mockReturnValue(1);
959983
});
960984

961985
afterEach(() => {
962986
jest.clearAllMocks();
987+
mockNavigate.mockReset();
963988
mockSeedlessOnboardingEnabled.mockReset();
964989
});
965990

@@ -1263,6 +1288,174 @@ describe('Onboarding', () => {
12631288
);
12641289
});
12651290

1291+
it('shows iOS version warning sheet before Google login on iOS < 17.4', async () => {
1292+
Platform.OS = 'ios';
1293+
(Device.isIos as jest.Mock).mockReturnValue(true);
1294+
(Device.comparePlatformVersionTo as jest.Mock).mockReturnValue(-1);
1295+
(mockAnalytics.isEnabled as jest.Mock).mockReturnValue(true);
1296+
mockCreateLoginHandler.mockReturnValue('mockGoogleHandler');
1297+
mockOAuthService.handleOAuthLogin.mockResolvedValue({
1298+
type: 'success',
1299+
existingUser: false,
1300+
accountName: 'test@example.com',
1301+
});
1302+
1303+
const { getByTestId } = renderScreen(
1304+
Onboarding,
1305+
{ name: 'Onboarding' },
1306+
{
1307+
state: mockInitialState,
1308+
},
1309+
);
1310+
1311+
const createWalletButton = getByTestId(
1312+
OnboardingSelectorIDs.NEW_WALLET_BUTTON,
1313+
);
1314+
await act(async () => {
1315+
fireEvent.press(createWalletButton);
1316+
});
1317+
1318+
const navCall = mockNavigate.mock.calls.find(
1319+
(call) =>
1320+
call[0] === Routes.MODAL.ROOT_MODAL_FLOW &&
1321+
call[1]?.screen === Routes.SHEET.ONBOARDING_SHEET,
1322+
);
1323+
1324+
const googleOAuthFunction = navCall[1].params.onPressContinueWithGoogle;
1325+
1326+
await act(async () => {
1327+
await googleOAuthFunction(true);
1328+
await flushPromises();
1329+
await flushPromises();
1330+
});
1331+
1332+
// Verify the warning sheet was shown with the iOS not-supported message.
1333+
const warningSheetCall = getIosGoogleWarningSheetCall();
1334+
1335+
expect(warningSheetCall).toEqual([
1336+
Routes.MODAL.ROOT_MODAL_FLOW,
1337+
expect.objectContaining({
1338+
screen: Routes.SHEET.SUCCESS_ERROR_SHEET,
1339+
params: expect.objectContaining({
1340+
type: 'error',
1341+
icon: IconName.Warning,
1342+
isInteractable: false,
1343+
title: IOS_GOOGLE_WARNING_TITLE,
1344+
description: expect.anything(),
1345+
primaryButtonLabel: IOS_GOOGLE_WARNING_BUTTON,
1346+
onPrimaryButtonPress: expect.any(Function),
1347+
closeOnPrimaryButtonPress: true,
1348+
}),
1349+
}),
1350+
]);
1351+
expect(warningSheetCall?.[1].params.onPrimaryButtonPress).toEqual(
1352+
expect.any(Function),
1353+
);
1354+
expect(Device.comparePlatformVersionTo).toHaveBeenCalledWith('17.4');
1355+
1356+
await act(async () => {
1357+
await warningSheetCall?.[1].params.onPrimaryButtonPress?.();
1358+
await flushPromises();
1359+
await flushPromises();
1360+
});
1361+
1362+
expect(mockAnalytics.trackEvent).toHaveBeenCalledWith(
1363+
expect.objectContaining({
1364+
name: 'Wallet Google Ios Warning Viewed',
1365+
properties: expect.objectContaining({
1366+
account_type: AccountType.MetamaskGoogle,
1367+
}),
1368+
}),
1369+
);
1370+
expect(mockCreateLoginHandler).toHaveBeenCalledWith('ios', 'google');
1371+
expect(mockOAuthService.handleOAuthLogin).toHaveBeenCalledWith(
1372+
'mockGoogleHandler',
1373+
false,
1374+
);
1375+
});
1376+
1377+
it('shows iOS version warning for Google login on iOS < 17.4 during import wallet flow', async () => {
1378+
Platform.OS = 'ios';
1379+
(Device.isIos as jest.Mock).mockReturnValue(true);
1380+
(Device.comparePlatformVersionTo as jest.Mock).mockReturnValue(-1);
1381+
(mockAnalytics.isEnabled as jest.Mock).mockReturnValue(true);
1382+
mockCreateLoginHandler.mockReturnValue('mockGoogleHandler');
1383+
mockOAuthService.handleOAuthLogin.mockResolvedValue({
1384+
type: 'success',
1385+
existingUser: false,
1386+
accountName: 'test@example.com',
1387+
});
1388+
1389+
const { getByTestId } = renderScreen(
1390+
Onboarding,
1391+
{ name: 'Onboarding' },
1392+
{
1393+
state: mockInitialState,
1394+
},
1395+
);
1396+
1397+
const importWalletButton = getByTestId(
1398+
OnboardingSelectorIDs.EXISTING_WALLET_BUTTON,
1399+
);
1400+
await act(async () => {
1401+
fireEvent.press(importWalletButton);
1402+
});
1403+
1404+
const navCall = mockNavigate.mock.calls.find(
1405+
(call) =>
1406+
call[0] === Routes.MODAL.ROOT_MODAL_FLOW &&
1407+
call[1]?.screen === Routes.SHEET.ONBOARDING_SHEET,
1408+
);
1409+
1410+
const googleOAuthFunction = navCall[1].params.onPressContinueWithGoogle;
1411+
1412+
await act(async () => {
1413+
await googleOAuthFunction(false);
1414+
await flushPromises();
1415+
await flushPromises();
1416+
});
1417+
1418+
const warningSheetCall = getIosGoogleWarningSheetCall();
1419+
1420+
expect(warningSheetCall).toBeDefined();
1421+
expect(warningSheetCall?.[1].params).toEqual(
1422+
expect.objectContaining({
1423+
type: 'error',
1424+
icon: IconName.Warning,
1425+
title: IOS_GOOGLE_WARNING_TITLE,
1426+
description: expect.anything(),
1427+
primaryButtonLabel: IOS_GOOGLE_WARNING_BUTTON,
1428+
onPrimaryButtonPress: expect.any(Function),
1429+
closeOnPrimaryButtonPress: true,
1430+
isInteractable: false,
1431+
}),
1432+
);
1433+
expect(warningSheetCall?.[1].params.onPrimaryButtonPress).toEqual(
1434+
expect.any(Function),
1435+
);
1436+
expect(Device.comparePlatformVersionTo).toHaveBeenCalledWith('17.4');
1437+
1438+
await act(async () => {
1439+
await warningSheetCall?.[1].params.onPrimaryButtonPress?.();
1440+
await flushPromises();
1441+
await flushPromises();
1442+
});
1443+
1444+
expect(mockAnalytics.trackEvent).toHaveBeenCalledWith(
1445+
expect.objectContaining({
1446+
name: 'Wallet Google Ios Warning Viewed',
1447+
properties: expect.objectContaining({
1448+
account_type: AccountType.ImportedGoogle,
1449+
}),
1450+
}),
1451+
);
1452+
expect(mockCreateLoginHandler).toHaveBeenCalledWith('ios', 'google');
1453+
expect(mockOAuthService.handleOAuthLogin).toHaveBeenCalledWith(
1454+
'mockGoogleHandler',
1455+
true,
1456+
);
1457+
});
1458+
12661459
it('navigates to AccountAlreadyExists for existing user in create wallet flow', async () => {
12671460
mockCreateLoginHandler.mockReturnValue('mockGoogleHandler');
12681461
mockOAuthService.handleOAuthLogin.mockResolvedValue({

app/components/Views/Onboarding/index.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ import {
110110
} from '@metamask/design-system-twrnc-preset';
111111

112112
import { getBuildNumber, getVersion } from 'react-native-device-info';
113+
import { navigateToSuccessErrorSheetPromise } from '../SuccessErrorSheet/utils';
114+
import {
115+
IconColor,
116+
IconName,
117+
} from '../../../component-library/components/Icons/Icon';
113118
interface OnboardingState {
114119
warningModalVisible: boolean;
115120
loading: boolean;
@@ -770,6 +775,41 @@ const Onboarding = () => {
770775
});
771776

772777
const action = async () => {
778+
// prompt for ios google login not supported below iOS 17.4
779+
if (
780+
provider === AuthConnection.Google &&
781+
Device.isIos() &&
782+
Device.comparePlatformVersionTo('17.4') < 0
783+
) {
784+
const description = () => (
785+
<>
786+
<Text style={tw.style('text-pretty')}>
787+
{strings(`error_sheet.ios_need_update_description`)}
788+
<Text twClassName="font-bold">
789+
{strings(`error_sheet.ios_need_update_description_version`)}
790+
</Text>
791+
{strings(`error_sheet.ios_need_update_description_end`)}
792+
</Text>
793+
<Text style={tw.style('text-pretty')}>
794+
{strings(`error_sheet.ios_need_update_description2`)}
795+
</Text>
796+
</>
797+
);
798+
799+
await navigateToSuccessErrorSheetPromise(navigation, {
800+
type: 'error',
801+
icon: IconName.Warning,
802+
iconColor: IconColor.Warning,
803+
title: strings(`error_sheet.ios_need_update_title`),
804+
description: description(),
805+
primaryButtonLabel: strings(`error_sheet.ios_need_update_button`),
806+
closeOnPrimaryButtonPress: true,
807+
isInteractable: false,
808+
});
809+
track(MetaMetricsEvents.WALLET_GOOGLE_IOS_WARNING_VIEWED, {
810+
account_type: accountType,
811+
});
812+
}
773813
setLoading();
774814
const loginHandler = createLoginHandler(Platform.OS, provider);
775815
try {
@@ -799,6 +839,7 @@ const Onboarding = () => {
799839
handleExistingUser(action);
800840
},
801841
[
842+
tw,
802843
navigation,
803844
metrics,
804845
track,

0 commit comments

Comments
 (0)