From fda3585ce67ce792615f1545659838aa78cca873 Mon Sep 17 00:00:00 2001
From: ieow <4881057+ieow@users.noreply.github.com>
Date: Wed, 25 Mar 2026 18:49:07 +0000
Subject: [PATCH] chore(runway): cherry-pick feat: show legacy ios login
warning prompt cp-7.71.0 (#27875)
## **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
## **Changelog**
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**
### **Before**
### **After**
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.
---
> [!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.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
3b43b83cf7c608c88da4b01bfb67603d840d1582. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.../Views/Onboarding/index.test.tsx | 211 +++++++++++++++++-
app/components/Views/Onboarding/index.tsx | 41 ++++
.../Views/SuccessErrorSheet/utils.test.ts | 113 ++++++++++
.../Views/SuccessErrorSheet/utils.ts | 37 +++
app/core/Analytics/MetaMetrics.events.ts | 4 +
app/util/device/index.js | 16 ++
locales/languages/en.json | 8 +-
7 files changed, 420 insertions(+), 10 deletions(-)
create mode 100644 app/components/Views/SuccessErrorSheet/utils.test.ts
create mode 100644 app/components/Views/SuccessErrorSheet/utils.ts
diff --git a/app/components/Views/Onboarding/index.test.tsx b/app/components/Views/Onboarding/index.test.tsx
index 771c308e15b..8f59eef7433 100644
--- a/app/components/Views/Onboarding/index.test.tsx
+++ b/app/components/Views/Onboarding/index.test.tsx
@@ -71,6 +71,7 @@ import Routes from '../../../constants/navigation/Routes';
import { ONBOARDING, PREVIOUS_SCREEN } from '../../../constants/navigation';
import { strings } from '../../../../locales/i18n';
import { OAuthError, OAuthErrorType } from '../../../core/OAuthService/error';
+import { IconName } from '../../../component-library/components/Icons/Icon';
import { captureException } from '@sentry/react-native';
import Logger from '../../../util/Logger';
import { MIGRATION_ERROR_HAPPENED } from '../../../constants/storage';
@@ -92,9 +93,22 @@ jest.mock('../../../util/test/utils', () => ({
import { fetch as netInfoFetch } from '@react-native-community/netinfo';
const mockNetInfoFetch = netInfoFetch as jest.Mock;
+const mockNavigate = jest.fn();
+const mockReplace = jest.fn();
+const mockGoBack = jest.fn();
// Helper to flush all pending promises
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
+const IOS_GOOGLE_WARNING_TITLE = strings('error_sheet.ios_need_update_title');
+const IOS_GOOGLE_WARNING_BUTTON = strings('error_sheet.ios_need_update_button');
+
+const getIosGoogleWarningSheetCall = () =>
+ mockNavigate.mock.calls.find(
+ ([route, params]) =>
+ route === Routes.MODAL.ROOT_MODAL_FLOW &&
+ params?.screen === Routes.SHEET.SUCCESS_ERROR_SHEET &&
+ params?.params?.title === IOS_GOOGLE_WARNING_TITLE,
+ );
const mockInitialState = {
engine: {
@@ -127,13 +141,22 @@ const mockInitialStateWithExistingUserAndPassword = {
},
};
-jest.mock('../../../util/device', () => ({
- isLargeDevice: jest.fn(),
- isIphoneX: jest.fn(),
- isAndroid: jest.fn(),
- isIos: jest.fn(),
- isMediumDevice: jest.fn(),
-}));
+jest.mock('../../../util/device', () => {
+ const mockDevice = {
+ isLargeDevice: jest.fn(),
+ isIphoneX: jest.fn(),
+ isAndroid: jest.fn(),
+ isIos: jest.fn(),
+ isMediumDevice: jest.fn(),
+ comparePlatformVersionTo: jest.fn().mockReturnValue(1),
+ };
+
+ return {
+ __esModule: true,
+ default: mockDevice,
+ ...mockDevice,
+ };
+});
// expo library are not supported in jest ( unless using jest-expo as preset ), so we need to mock them
jest.mock('../../../core/OAuthService/OAuthLoginHandlers', () => ({
@@ -276,13 +299,12 @@ jest.mock('../../../core/OAuthService/OAuthLoginHandlers/constants', () => ({
},
}));
-const mockNavigate = jest.fn();
-const mockReplace = jest.fn();
const mockNav = {
navigate: mockNavigate,
replace: mockReplace,
reset: jest.fn(),
setOptions: jest.fn(),
+ goBack: mockGoBack,
dispatch: jest.fn((action) => {
if (action.type === 'REPLACE') {
mockReplace(action.payload.name, action.payload.params);
@@ -956,10 +978,13 @@ describe('Onboarding', () => {
beforeEach(() => {
mockSeedlessOnboardingEnabled.mockReturnValue(true);
(StorageWrapper.getItem as jest.Mock).mockResolvedValue(null);
+ (Device.isIos as jest.Mock).mockReturnValue(false);
+ (Device.comparePlatformVersionTo as jest.Mock).mockReturnValue(1);
});
afterEach(() => {
jest.clearAllMocks();
+ mockNavigate.mockReset();
mockSeedlessOnboardingEnabled.mockReset();
});
@@ -1263,6 +1288,174 @@ describe('Onboarding', () => {
);
});
+ it('shows iOS version warning sheet before Google login on iOS < 17.4', async () => {
+ Platform.OS = 'ios';
+ (Device.isIos as jest.Mock).mockReturnValue(true);
+ (Device.comparePlatformVersionTo as jest.Mock).mockReturnValue(-1);
+ (mockAnalytics.isEnabled as jest.Mock).mockReturnValue(true);
+ mockCreateLoginHandler.mockReturnValue('mockGoogleHandler');
+ mockOAuthService.handleOAuthLogin.mockResolvedValue({
+ type: 'success',
+ existingUser: false,
+ accountName: 'test@example.com',
+ });
+
+ const { getByTestId } = renderScreen(
+ Onboarding,
+ { name: 'Onboarding' },
+ {
+ state: mockInitialState,
+ },
+ );
+
+ const createWalletButton = getByTestId(
+ OnboardingSelectorIDs.NEW_WALLET_BUTTON,
+ );
+ await act(async () => {
+ fireEvent.press(createWalletButton);
+ });
+
+ const navCall = mockNavigate.mock.calls.find(
+ (call) =>
+ call[0] === Routes.MODAL.ROOT_MODAL_FLOW &&
+ call[1]?.screen === Routes.SHEET.ONBOARDING_SHEET,
+ );
+
+ const googleOAuthFunction = navCall[1].params.onPressContinueWithGoogle;
+
+ await act(async () => {
+ await googleOAuthFunction(true);
+ await flushPromises();
+ await flushPromises();
+ });
+
+ // Verify the warning sheet was shown with the iOS not-supported message.
+ const warningSheetCall = getIosGoogleWarningSheetCall();
+
+ expect(warningSheetCall).toEqual([
+ Routes.MODAL.ROOT_MODAL_FLOW,
+ expect.objectContaining({
+ screen: Routes.SHEET.SUCCESS_ERROR_SHEET,
+ params: expect.objectContaining({
+ type: 'error',
+ icon: IconName.Warning,
+ isInteractable: false,
+ title: IOS_GOOGLE_WARNING_TITLE,
+ description: expect.anything(),
+ primaryButtonLabel: IOS_GOOGLE_WARNING_BUTTON,
+ onPrimaryButtonPress: expect.any(Function),
+ closeOnPrimaryButtonPress: true,
+ }),
+ }),
+ ]);
+ expect(warningSheetCall?.[1].params.onPrimaryButtonPress).toEqual(
+ expect.any(Function),
+ );
+ expect(Device.comparePlatformVersionTo).toHaveBeenCalledWith('17.4');
+
+ await act(async () => {
+ await warningSheetCall?.[1].params.onPrimaryButtonPress?.();
+ await flushPromises();
+ await flushPromises();
+ });
+
+ expect(mockAnalytics.trackEvent).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'Wallet Google Ios Warning Viewed',
+ properties: expect.objectContaining({
+ account_type: AccountType.MetamaskGoogle,
+ }),
+ }),
+ );
+ expect(mockCreateLoginHandler).toHaveBeenCalledWith('ios', 'google');
+ expect(mockOAuthService.handleOAuthLogin).toHaveBeenCalledWith(
+ 'mockGoogleHandler',
+ false,
+ );
+ });
+
+ it('shows iOS version warning for Google login on iOS < 17.4 during import wallet flow', async () => {
+ Platform.OS = 'ios';
+ (Device.isIos as jest.Mock).mockReturnValue(true);
+ (Device.comparePlatformVersionTo as jest.Mock).mockReturnValue(-1);
+ (mockAnalytics.isEnabled as jest.Mock).mockReturnValue(true);
+ mockCreateLoginHandler.mockReturnValue('mockGoogleHandler');
+ mockOAuthService.handleOAuthLogin.mockResolvedValue({
+ type: 'success',
+ existingUser: false,
+ accountName: 'test@example.com',
+ });
+
+ const { getByTestId } = renderScreen(
+ Onboarding,
+ { name: 'Onboarding' },
+ {
+ state: mockInitialState,
+ },
+ );
+
+ const importWalletButton = getByTestId(
+ OnboardingSelectorIDs.EXISTING_WALLET_BUTTON,
+ );
+ await act(async () => {
+ fireEvent.press(importWalletButton);
+ });
+
+ const navCall = mockNavigate.mock.calls.find(
+ (call) =>
+ call[0] === Routes.MODAL.ROOT_MODAL_FLOW &&
+ call[1]?.screen === Routes.SHEET.ONBOARDING_SHEET,
+ );
+
+ const googleOAuthFunction = navCall[1].params.onPressContinueWithGoogle;
+
+ await act(async () => {
+ await googleOAuthFunction(false);
+ await flushPromises();
+ await flushPromises();
+ });
+
+ const warningSheetCall = getIosGoogleWarningSheetCall();
+
+ expect(warningSheetCall).toBeDefined();
+ expect(warningSheetCall?.[1].params).toEqual(
+ expect.objectContaining({
+ type: 'error',
+ icon: IconName.Warning,
+ title: IOS_GOOGLE_WARNING_TITLE,
+ description: expect.anything(),
+ primaryButtonLabel: IOS_GOOGLE_WARNING_BUTTON,
+ onPrimaryButtonPress: expect.any(Function),
+ closeOnPrimaryButtonPress: true,
+ isInteractable: false,
+ }),
+ );
+ expect(warningSheetCall?.[1].params.onPrimaryButtonPress).toEqual(
+ expect.any(Function),
+ );
+ expect(Device.comparePlatformVersionTo).toHaveBeenCalledWith('17.4');
+
+ await act(async () => {
+ await warningSheetCall?.[1].params.onPrimaryButtonPress?.();
+ await flushPromises();
+ await flushPromises();
+ });
+
+ expect(mockAnalytics.trackEvent).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'Wallet Google Ios Warning Viewed',
+ properties: expect.objectContaining({
+ account_type: AccountType.ImportedGoogle,
+ }),
+ }),
+ );
+ expect(mockCreateLoginHandler).toHaveBeenCalledWith('ios', 'google');
+ expect(mockOAuthService.handleOAuthLogin).toHaveBeenCalledWith(
+ 'mockGoogleHandler',
+ true,
+ );
+ });
+
it('navigates to AccountAlreadyExists for existing user in create wallet flow', async () => {
mockCreateLoginHandler.mockReturnValue('mockGoogleHandler');
mockOAuthService.handleOAuthLogin.mockResolvedValue({
diff --git a/app/components/Views/Onboarding/index.tsx b/app/components/Views/Onboarding/index.tsx
index f2d715f0a34..3c105f2a087 100644
--- a/app/components/Views/Onboarding/index.tsx
+++ b/app/components/Views/Onboarding/index.tsx
@@ -110,6 +110,11 @@ import {
} from '@metamask/design-system-twrnc-preset';
import { getBuildNumber, getVersion } from 'react-native-device-info';
+import { navigateToSuccessErrorSheetPromise } from '../SuccessErrorSheet/utils';
+import {
+ IconColor,
+ IconName,
+} from '../../../component-library/components/Icons/Icon';
interface OnboardingState {
warningModalVisible: boolean;
loading: boolean;
@@ -770,6 +775,41 @@ const Onboarding = () => {
});
const action = async () => {
+ // prompt for ios google login not supported below iOS 17.4
+ if (
+ provider === AuthConnection.Google &&
+ Device.isIos() &&
+ Device.comparePlatformVersionTo('17.4') < 0
+ ) {
+ const description = () => (
+ <>
+
+ {strings(`error_sheet.ios_need_update_description`)}
+
+ {strings(`error_sheet.ios_need_update_description_version`)}
+
+ {strings(`error_sheet.ios_need_update_description_end`)}
+
+
+ {strings(`error_sheet.ios_need_update_description2`)}
+
+ >
+ );
+
+ await navigateToSuccessErrorSheetPromise(navigation, {
+ type: 'error',
+ icon: IconName.Warning,
+ iconColor: IconColor.Warning,
+ title: strings(`error_sheet.ios_need_update_title`),
+ description: description(),
+ primaryButtonLabel: strings(`error_sheet.ios_need_update_button`),
+ closeOnPrimaryButtonPress: true,
+ isInteractable: false,
+ });
+ track(MetaMetricsEvents.WALLET_GOOGLE_IOS_WARNING_VIEWED, {
+ account_type: accountType,
+ });
+ }
setLoading();
const loginHandler = createLoginHandler(Platform.OS, provider);
try {
@@ -799,6 +839,7 @@ const Onboarding = () => {
handleExistingUser(action);
},
[
+ tw,
navigation,
metrics,
track,
diff --git a/app/components/Views/SuccessErrorSheet/utils.test.ts b/app/components/Views/SuccessErrorSheet/utils.test.ts
new file mode 100644
index 00000000000..a0b29825c74
--- /dev/null
+++ b/app/components/Views/SuccessErrorSheet/utils.test.ts
@@ -0,0 +1,113 @@
+import { NavigationProp, ParamListBase } from '@react-navigation/native';
+import Routes from '../../../constants/navigation/Routes';
+import {
+ navigateToSuccessErrorSheet,
+ navigateToSuccessErrorSheetPromise,
+} from './utils';
+
+const mockNavigate = jest.fn();
+const mockNavigation = {
+ navigate: mockNavigate,
+} as unknown as NavigationProp;
+
+const baseParams = {
+ type: 'error' as const,
+ title: 'Error Title',
+ description: 'Error description',
+};
+
+describe('navigateToSuccessErrorSheet', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('forwards params to the success error sheet route', () => {
+ const onClose = jest.fn();
+ const onPrimaryButtonPress = jest.fn();
+ const params = {
+ ...baseParams,
+ type: 'success' as const,
+ onClose,
+ onPrimaryButtonPress,
+ descriptionAlign: 'center' as const,
+ primaryButtonLabel: 'OK',
+ };
+
+ navigateToSuccessErrorSheet(mockNavigation, params);
+
+ expect(mockNavigate).toHaveBeenCalledTimes(1);
+ expect(mockNavigate).toHaveBeenCalledWith(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.SUCCESS_ERROR_SHEET,
+ params: expect.objectContaining({
+ type: 'success',
+ onClose,
+ onPrimaryButtonPress,
+ descriptionAlign: 'center',
+ primaryButtonLabel: 'OK',
+ }),
+ });
+ });
+});
+
+describe('navigateToSuccessErrorSheetPromise', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('resolves and invokes the original onPrimaryButtonPress callback', async () => {
+ const onPrimaryButtonPress = jest.fn();
+
+ mockNavigate.mockImplementation((_route, params) => {
+ params.params.onPrimaryButtonPress();
+ });
+
+ await expect(
+ navigateToSuccessErrorSheetPromise(mockNavigation, {
+ ...baseParams,
+ onPrimaryButtonPress,
+ }),
+ ).resolves.toBeUndefined();
+
+ expect(onPrimaryButtonPress).toHaveBeenCalledTimes(1);
+ expect(mockNavigate).toHaveBeenCalledWith(
+ Routes.MODAL.ROOT_MODAL_FLOW,
+ expect.objectContaining({
+ screen: Routes.SHEET.SUCCESS_ERROR_SHEET,
+ }),
+ );
+ });
+
+ it('resolves and invokes the original onSecondaryButtonPress callback', async () => {
+ const onSecondaryButtonPress = jest.fn();
+
+ mockNavigate.mockImplementation((_route, params) => {
+ params.params.onSecondaryButtonPress();
+ });
+
+ await expect(
+ navigateToSuccessErrorSheetPromise(mockNavigation, {
+ ...baseParams,
+ onSecondaryButtonPress,
+ }),
+ ).resolves.toBeUndefined();
+
+ expect(onSecondaryButtonPress).toHaveBeenCalledTimes(1);
+ });
+
+ it('resolves and invokes the original onClose callback', async () => {
+ const onClose = jest.fn();
+
+ mockNavigate.mockImplementation((_route, params) => {
+ params.params.onClose();
+ });
+
+ await expect(
+ navigateToSuccessErrorSheetPromise(mockNavigation, {
+ ...baseParams,
+ onClose,
+ }),
+ ).resolves.toBeUndefined();
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/app/components/Views/SuccessErrorSheet/utils.ts b/app/components/Views/SuccessErrorSheet/utils.ts
new file mode 100644
index 00000000000..e9c655fa924
--- /dev/null
+++ b/app/components/Views/SuccessErrorSheet/utils.ts
@@ -0,0 +1,37 @@
+import { NavigationProp, ParamListBase } from '@react-navigation/native';
+import { SuccessErrorSheetParams } from './interface';
+import Routes from '../../../constants/navigation/Routes';
+
+export const navigateToSuccessErrorSheet = (
+ navigation: NavigationProp,
+ params: SuccessErrorSheetParams,
+) => {
+ navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.SUCCESS_ERROR_SHEET,
+ params: {
+ ...params,
+ },
+ });
+};
+
+export const navigateToSuccessErrorSheetPromise = async (
+ navigation: NavigationProp,
+ params: SuccessErrorSheetParams,
+) =>
+ new Promise((resolve) => {
+ navigateToSuccessErrorSheet(navigation, {
+ ...params,
+ onPrimaryButtonPress: () => {
+ params.onPrimaryButtonPress?.();
+ resolve();
+ },
+ onSecondaryButtonPress: () => {
+ params.onSecondaryButtonPress?.();
+ resolve();
+ },
+ onClose: () => {
+ params.onClose?.();
+ resolve();
+ },
+ });
+ });
diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts
index 3e0d1d23446..745311015fd 100644
--- a/app/core/Analytics/MetaMetrics.events.ts
+++ b/app/core/Analytics/MetaMetrics.events.ts
@@ -147,6 +147,7 @@ enum EVENT_NAME {
WALLET_CREATION_ATTEMPTED = 'Wallet Creation Attempted',
WALLET_CREATED = 'Wallet Created',
WALLET_SETUP_FAILURE = 'Wallet Setup Failure',
+ WALLET_GOOGLE_IOS_WARNING_VIEWED = 'Wallet Google Ios Warning Viewed',
WALLET_CREATION_ERROR_SCREEN_VIEWED = 'Wallet Creation Error Screen Viewed',
WALLET_CREATION_ERROR_RETRY_CLICKED = 'Wallet Creation Error Retry Clicked',
WALLET_CREATION_ERROR_REPORT_SENT = 'Wallet Creation Error Report Sent',
@@ -859,6 +860,9 @@ const events = {
WALLET_CREATION_ATTEMPTED: generateOpt(EVENT_NAME.WALLET_CREATION_ATTEMPTED),
WALLET_CREATED: generateOpt(EVENT_NAME.WALLET_CREATED),
WALLET_SETUP_FAILURE: generateOpt(EVENT_NAME.WALLET_SETUP_FAILURE),
+ WALLET_GOOGLE_IOS_WARNING_VIEWED: generateOpt(
+ EVENT_NAME.WALLET_GOOGLE_IOS_WARNING_VIEWED,
+ ),
WALLET_CREATION_ERROR_SCREEN_VIEWED: generateOpt(
EVENT_NAME.WALLET_CREATION_ERROR_SCREEN_VIEWED,
),
diff --git a/app/util/device/index.js b/app/util/device/index.js
index df04b68a24d..c3329ec2161 100644
--- a/app/util/device/index.js
+++ b/app/util/device/index.js
@@ -2,6 +2,7 @@
import { Dimensions, Platform } from 'react-native';
import { hasNotch, getApiLevel } from 'react-native-device-info';
+import compareVersions from 'compare-versions';
export default class Device {
static getDeviceWidth() {
@@ -12,6 +13,21 @@ export default class Device {
return Dimensions.get('window').height;
}
+ /**
+ * Compares this device's React Native {@link Platform.Version} to `referenceVersion`
+ * using the shared `compare-versions` package after normalizing both values to strings.
+ *
+ * @param {string|number} referenceVersion - Version to compare against (e.g. `"17.4"`).
+ * @returns {number} `1` if current > reference, `-1` if current < reference, `0` if equal.
+ * @remarks On iOS, `Platform.Version` is usually a string (`"17.3.1"`). On Android it is
+ * typically the API level as a number (for example `34`). Both values are coerced to strings
+ * before comparison so the helper remains safe across platforms while preserving component-wise
+ * numeric comparison semantics.
+ */
+ static comparePlatformVersionTo(referenceVersion) {
+ return compareVersions(String(Platform.Version), String(referenceVersion));
+ }
+
static isIos() {
return Platform.OS === 'ios';
}
diff --git a/locales/languages/en.json b/locales/languages/en.json
index a30519785e2..c4c7879604d 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -6751,7 +6751,13 @@
"oauth_error_button": "Try again",
"no_internet_connection_title": "Unable to connect",
"no_internet_connection_description": "Your internet connection is unstable. Check your connection and try again.",
- "no_internet_connection_button": "Try again"
+ "no_internet_connection_button": "Try again",
+ "ios_need_update_title": "iOS update required",
+ "ios_need_update_description": "MetaMask Google Sign-In will soon require ",
+ "ios_need_update_description_version": "iOS 17.4 or later",
+ "ios_need_update_description_end": ". You can continue using Google Sign-In on this device for now, but it will no longer be supported in an upcoming update.",
+ "ios_need_update_description2": "You can still access your wallet using the same Google account on a supported device or the MetaMask extension. We strongly recommend backing up your Secret Recovery Phrase to ensure uninterrupted access.",
+ "ios_need_update_button": "Continue"
},
"password_hint": {
"title": "Password hint",