Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .yarn/patches/expo-web-browser-npm-14.0.2-98d00ce880.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
diff --git a/ios/WebAuthSession.swift b/ios/WebAuthSession.swift
index 0d8101b01d7c6cd803acf6a359ceaa026993bdd0..c1beeabd962e561bf48392d58c084272247a95cc 100644
--- a/ios/WebAuthSession.swift
+++ b/ios/WebAuthSession.swift
@@ -20,17 +20,34 @@ final internal class WebAuthSession {
private var presentationContextProvider = PresentationContextProvider()

init(authUrl: URL, redirectUrl: URL?, options: AuthSessionOptions) {
- self.authSession = ASWebAuthenticationSession(
- url: authUrl,
- callbackURLScheme: redirectUrl?.scheme,
- completionHandler: { callbackUrl, error in
- self.finish(with: [
- "type": callbackUrl != nil ? "success" : "cancel",
- "url": callbackUrl?.absoluteString,
- "error": error?.localizedDescription
- ])
- }
- )
+ let completionHandler: (URL?, Error?) -> Void = { callbackUrl, error in
+ self.finish(with: [
+ "type": callbackUrl != nil ? "success" : "cancel",
+ "url": callbackUrl?.absoluteString,
+ "error": error?.localizedDescription
+ ])
+ }
+
+ // iOS 17.4+/macOS 14.4+ supports HTTPS callbacks with host/path matching
+ if #available(iOS 17.4, macOS 14.4, *),
+ let redirectUrl,
+ redirectUrl.scheme?.lowercased() == "https",
+ let host = redirectUrl.host(percentEncoded: false),
+ !host.isEmpty {
+ let rawPath = redirectUrl.path
+ let path = (rawPath.isEmpty || rawPath == "/") ? "" : rawPath
+ self.authSession = ASWebAuthenticationSession(
+ url: authUrl,
+ callback: .https(host: host, path: path),
+ completionHandler: completionHandler
+ )
+ } else {
+ self.authSession = ASWebAuthenticationSession(
+ url: authUrl,
+ callbackURLScheme: redirectUrl?.scheme,
+ completionHandler: completionHandler
+ )
+ }
self.authSession?.prefersEphemeralWebBrowserSession = options.preferEphemeralSession
}

137 changes: 137 additions & 0 deletions app/components/Views/Onboarding/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ jest.mock('../../../util/device', () => ({
isAndroid: jest.fn(),
isIos: jest.fn(),
isMediumDevice: jest.fn(),
comparePlatformVersionTo: jest.fn().mockReturnValue(1),
}));

// expo library are not supported in jest ( unless using jest-expo as preset ), so we need to mock them
Expand Down Expand Up @@ -955,10 +956,13 @@ describe('Onboarding', () => {
beforeEach(() => {
mockSeedlessOnboardingEnabled.mockReturnValue(true);
(StorageWrapper.getItem as jest.Mock).mockResolvedValue(null);
// Default to iOS >= 17.4 so the proactive version check is skipped
(Device.comparePlatformVersionTo as jest.Mock).mockReturnValue(1);
});

afterEach(() => {
jest.clearAllMocks();
mockNavigate.mockReset();
mockSeedlessOnboardingEnabled.mockReset();
});

Expand Down Expand Up @@ -1262,6 +1266,139 @@ describe('Onboarding', () => {
);
});

it('shows iOS version warning sheet before Google login on iOS < 17.4', async () => {
Platform.OS = 'ios';
// Simulate iOS version below 17.4
(Device.comparePlatformVersionTo as jest.Mock).mockReturnValue(-1);
mockCreateLoginHandler.mockReturnValue('mockGoogleHandler');
mockOAuthService.handleOAuthLogin.mockResolvedValue({
type: 'success',
existingUser: false,
accountName: 'test@example.com',
});

// Auto-close the error sheet so the promise resolves and the login continues
mockNavigate.mockImplementation(
(route: string, params: Record<string, unknown>) => {
const screenParams = params?.params as Record<string, unknown>;
if (
route === Routes.MODAL.ROOT_MODAL_FLOW &&
params?.screen === Routes.SHEET.SUCCESS_ERROR_SHEET &&
typeof screenParams?.onClose === 'function'
) {
(screenParams.onClose as () => void)();
}
},
);

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
expect(mockNavigate).toHaveBeenCalledWith(
Routes.MODAL.ROOT_MODAL_FLOW,
expect.objectContaining({
screen: Routes.SHEET.SUCCESS_ERROR_SHEET,
params: expect.objectContaining({
type: 'warning',
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
title: strings('error_sheet.ios_google_login_not_supported_title'),
description: strings(
'error_sheet.ios_google_login_not_supported_description',
),
descriptionAlign: 'center',
primaryButtonLabel: strings(
'error_sheet.ios_google_login_not_supported_button',
),
}),
}),
);

// Verify the login flow continued after the sheet was dismissed
expect(mockCreateLoginHandler).toHaveBeenCalledWith('ios', 'google');
expect(mockOAuthService.handleOAuthLogin).toHaveBeenCalledWith(
'mockGoogleHandler',
false,
);
});

it('does not show iOS version warning for Apple login on iOS < 17.4', async () => {
Platform.OS = 'ios';
// Simulate iOS version below 17.4
(Device.comparePlatformVersionTo as jest.Mock).mockReturnValue(-1);
mockCreateLoginHandler.mockReturnValue('mockAppleHandler');
mockOAuthService.handleOAuthLogin.mockResolvedValue({
type: 'success',
existingUser: false,
accountName: 'test@icloud.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 appleOAuthFunction = navCall[1].params.onPressContinueWithApple;

await act(async () => {
await appleOAuthFunction(true);
await flushPromises();
});

// The iOS version warning sheet should NOT be shown for Apple login
const warningSheetCall = mockNavigate.mock.calls.find(
(call) =>
call[0] === Routes.MODAL.ROOT_MODAL_FLOW &&
call[1]?.screen === Routes.SHEET.SUCCESS_ERROR_SHEET &&
call[1]?.params?.type === 'warning',
);
expect(warningSheetCall).toBeUndefined();

// Apple login should proceed normally
expect(mockCreateLoginHandler).toHaveBeenCalledWith('ios', 'apple');
});

it('navigates to AccountAlreadyExists for existing user in create wallet flow', async () => {
mockCreateLoginHandler.mockReturnValue('mockGoogleHandler');
mockOAuthService.handleOAuthLogin.mockResolvedValue({
Expand Down
24 changes: 24 additions & 0 deletions app/components/Views/Onboarding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ import {
} from '@metamask/design-system-twrnc-preset';

import { getBuildNumber, getVersion } from 'react-native-device-info';
import { navigateToSuccessErrorSheetPromise } from '../SuccessErrorSheet/utils';
import { IconName } from '../../../component-library/components/Icons/Icon';
interface OnboardingState {
warningModalVisible: boolean;
loading: boolean;
Expand Down Expand Up @@ -759,6 +761,28 @@ const Onboarding = () => {
});

const action = async () => {
// prompt for ios google login not supported below iOS 17.4
if (
Platform.OS === 'ios' &&
provider === AuthConnection.Google &&
Device.comparePlatformVersionTo('17.4') < 0
) {
await navigateToSuccessErrorSheetPromise(navigation, {
type: 'error',
icon: IconName.Warning,
title: strings(`error_sheet.ios_google_login_not_supported_title`),
description: strings(
`error_sheet.ios_google_login_not_supported_description`,
),
descriptionAlign: 'center',
primaryButtonLabel: strings(
`error_sheet.ios_google_login_not_supported_button`,
),
onPrimaryButtonPress: () => {
navigation.goBack();
},
});
}
setLoading();
const loginHandler = createLoginHandler(Platform.OS, provider);
try {
Expand Down
Loading
Loading