Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
}

260 changes: 260 additions & 0 deletions app/components/Views/Onboarding/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -132,6 +133,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 @@ -269,19 +271,24 @@ jest.mock(
);

const mockSeedlessOnboardingEnabled = jest.fn();
const mockShouldUseLegacyIosGoogleConfig = jest.fn();
jest.mock('../../../core/OAuthService/OAuthLoginHandlers/constants', () => ({
get SEEDLESS_ONBOARDING_ENABLED() {
return mockSeedlessOnboardingEnabled();
},
shouldUseLegacyIosGoogleConfig: (...args: unknown[]) =>
mockShouldUseLegacyIosGoogleConfig(...args),
}));

const mockNavigate = jest.fn();
const mockReplace = jest.fn();
const mockGoBack = 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);
Expand Down Expand Up @@ -954,12 +961,17 @@ describe('Onboarding', () => {

beforeEach(() => {
mockSeedlessOnboardingEnabled.mockReturnValue(true);
mockShouldUseLegacyIosGoogleConfig.mockReturnValue(false);
(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();
mockShouldUseLegacyIosGoogleConfig.mockReset();
});

it('calls Google OAuth login for create wallet flow on iOS and navigates to SocialLoginSuccessNewUser', async () => {
Expand Down Expand Up @@ -1262,6 +1274,254 @@ 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);
mockShouldUseLegacyIosGoogleConfig.mockReturnValue(false);
mockCreateLoginHandler.mockReturnValue('mockGoogleHandler');
mockOAuthService.handleOAuthLogin.mockResolvedValue({
type: 'success',
existingUser: false,
accountName: '[email protected]',
});

// Simulate user pressing the Continue button — the only dismissal path since
// isInteractable: false prevents closing by tapping outside the sheet.
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?.onPrimaryButtonPress === 'function'
) {
(screenParams.onPrimaryButtonPress 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: 'error',
icon: IconName.Warning,
isInteractable: false,
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 Google login on iOS < 17.4 when the legacy config flag is enabled', async () => {
Platform.OS = 'ios';
(Device.comparePlatformVersionTo as jest.Mock).mockReturnValue(-1);
mockShouldUseLegacyIosGoogleConfig.mockReturnValue(true);
mockCreateLoginHandler.mockReturnValue('mockGoogleHandler');
mockOAuthService.handleOAuthLogin.mockResolvedValue({
type: 'success',
existingUser: false,
accountName: '[email protected]',
});

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();
});

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?.title ===
strings('error_sheet.ios_google_login_not_supported_title'),
);

expect(warningSheetCall).toBeUndefined();
expect(mockCreateLoginHandler).toHaveBeenCalledWith('ios', 'google');
expect(mockOAuthService.handleOAuthLogin).toHaveBeenCalledWith(
'mockGoogleHandler',
false,
);
});

it('does not show iOS version warning for Google login on iOS 17.4 or later when the legacy config flag is disabled', async () => {
Platform.OS = 'ios';
(Device.comparePlatformVersionTo as jest.Mock).mockReturnValue(0);
mockShouldUseLegacyIosGoogleConfig.mockReturnValue(false);
mockCreateLoginHandler.mockReturnValue('mockGoogleHandler');
mockOAuthService.handleOAuthLogin.mockResolvedValue({
type: 'success',
existingUser: false,
accountName: '[email protected]',
});

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();
});

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?.title ===
strings('error_sheet.ios_google_login_not_supported_title'),
);

expect(warningSheetCall).toBeUndefined();
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: '[email protected]',
});

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 Google-specific iOS 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?.title ===
strings('error_sheet.ios_google_login_not_supported_title'),
);
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
Loading
Loading