diff --git a/.eslintrc.js b/.eslintrc.js index 8bf712b2dcd6..09cca7bd6b86 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -37,6 +37,7 @@ module.exports = { '@typescript-eslint/no-explicit-any': 'error', // Under discussion '@typescript-eslint/no-duplicate-enum-values': 'off', + '@typescript-eslint/no-parameter-properties': 'off', }, }, { diff --git a/app.config.js b/app.config.js index f1f87b695a49..b7806b051ae7 100644 --- a/app.config.js +++ b/app.config.js @@ -18,6 +18,10 @@ module.exports = { { subdomains: '*' } - ] - ] + ], + 'expo-apple-authentication', + ], + ios: { + usesAppleSignIn: true + } }; diff --git a/app/constants/deeplinks.ts b/app/constants/deeplinks.ts index 439416c211b8..4f9359c669fa 100644 --- a/app/constants/deeplinks.ts +++ b/app/constants/deeplinks.ts @@ -27,6 +27,7 @@ export enum ACTIONS { SELL = 'sell', SELL_CRYPTO = 'sell-crypto', EMPTY = '', + OAUTH_REDIRECT = 'oauth-redirect', } export const PREFIXES = { @@ -43,5 +44,6 @@ export const PREFIXES = { [ACTIONS.SELL]: '', [ACTIONS.BUY_CRYPTO]: '', [ACTIONS.SELL_CRYPTO]: '', + [ACTIONS.OAUTH_REDIRECT]: '', METAMASK: 'metamask://', }; diff --git a/app/core/AppConstants.ts b/app/core/AppConstants.ts index 4fddd8f3cd85..be173e81f1a3 100644 --- a/app/core/AppConstants.ts +++ b/app/core/AppConstants.ts @@ -1,6 +1,7 @@ import { CoreTypes } from '@walletconnect/types'; import Device from '../util/device'; import { DEFAULT_SERVER_URL } from '@metamask/sdk-communication-layer'; +import { getBundleId } from 'react-native-device-info'; const DEVELOPMENT = 'development'; const PORTFOLIO_URL = @@ -140,7 +141,9 @@ export default { STAKING_RISK_DISCLOSURE: 'https://consensys.io/staking-risk-disclosures', ADD_SOLANA_ACCOUNT_PRIVACY_POLICY: 'https://support.metamask.io/configure/accounts/how-to-add-accounts-in-your-wallet/#solana-accounts' }, - DECODING_API_URL: process.env.DECODING_API_URL || 'https://signature-insights.api.cx.metamask.io/v1', + DECODING_API_URL: + process.env.DECODING_API_URL || + 'https://signature-insights.api.cx.metamask.io/v1', ERRORS: { INFURA_BLOCKED_MESSAGE: 'EthQuery - RPC Error - This service is not available in your country', @@ -228,4 +231,17 @@ export default { VERSION: 'v1', DEFAULT_FETCH_INTERVAL: 15 * 60 * 1000, // 15 minutes }, + SEEDLESS_ONBOARDING: { + AUTH_SERVER_URL: process.env.AUTH_SERVER_URL, + IOS_APPLE_CLIENT_ID: getBundleId + ? getBundleId() + : process.env.IOS_APPLE_CLIENT_ID, + IOS_GOOGLE_CLIENT_ID: process.env.IOS_GOOGLE_CLIENT_ID, + IOS_GOOGLE_REDIRECT_URI: process.env.IOS_GOOGLE_REDIRECT_URI, + ANDROID_WEB_GOOGLE_CLIENT_ID: process.env.ANDROID_WEB_GOOGLE_CLIENT_ID, + ANDROID_WEB_APPLE_CLIENT_ID: process.env.ANDROID_WEB_APPLE_CLIENT_ID, + AUTH_CONNECTION_ID: process.env.AUTH_CONNECTION_ID, + GROUPED_AUTH_CONNECTION_ID: process.env.GROUPED_AUTH_CONNECTION_ID, + WEB3AUTH_NETWORK: process.env.WEB3AUTH_NETWORK, + }, } as const; diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts index 1bb06b13f645..e6199cee0b48 100644 --- a/app/core/Engine/Engine.ts +++ b/app/core/Engine/Engine.ts @@ -203,6 +203,10 @@ import { InternalAccount } from '@metamask/keyring-internal-api'; import { toFormattedAddress } from '../../util/address'; import { BRIDGE_API_BASE_URL } from '../../constants/bridge'; +///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) +import { seedlessOnboardingControllerInit } from './controllers/seedless-onboarding-controller'; +///: END:ONLY_INCLUDE_IF + const NON_EMPTY = 'NON_EMPTY'; const encryptor = new Encryptor({ @@ -1050,6 +1054,9 @@ export class Engine { MultichainBalancesController: multichainBalancesControllerInit, MultichainTransactionsController: multichainTransactionsControllerInit, ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) + SeedlessOnboardingController: seedlessOnboardingControllerInit, + ///: END:ONLY_INCLUDE_IF }, persistedState: initialState as EngineState, existingControllersByName, @@ -1061,7 +1068,10 @@ export class Engine { const gasFeeController = controllersByName.GasFeeController; const signatureController = controllersByName.SignatureController; const transactionController = controllersByName.TransactionController; - + ///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) + const seedlessOnboardingController = + controllersByName.SeedlessOnboardingController; + ///: END:ONLY_INCLUDE_IF // Backwards compatibility for existing references this.accountsController = accountsController; this.gasFeeController = gasFeeController; @@ -1402,6 +1412,9 @@ export class Engine { BridgeController: bridgeController, BridgeStatusController: bridgeStatusController, EarnController: earnController, + ///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) + SeedlessOnboardingController: seedlessOnboardingController, + ///: END:ONLY_INCLUDE_IF }; const childControllers = Object.assign({}, this.context); @@ -2030,6 +2043,9 @@ export default { BridgeController, BridgeStatusController, EarnController, + ///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) + SeedlessOnboardingController, + ///: END:ONLY_INCLUDE_IF } = instance.datamodel.state; return { @@ -2080,6 +2096,9 @@ export default { BridgeController, BridgeStatusController, EarnController, + ///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) + SeedlessOnboardingController, + ///: END:ONLY_INCLUDE_IF }; }, diff --git a/app/core/Engine/constants.ts b/app/core/Engine/constants.ts index 255dd4c823ad..583a17ea744e 100644 --- a/app/core/Engine/constants.ts +++ b/app/core/Engine/constants.ts @@ -70,6 +70,9 @@ export const BACKGROUND_STATE_CHANGE_EVENT_NAMES = [ 'BridgeController:stateChange', 'BridgeStatusController:stateChange', 'EarnController:stateChange', + ///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) + 'SeedlessOnboardingController:stateChange', + ///: END:ONLY_INCLUDE_IF ] as const; export const swapsSupportedChainIds = [ diff --git a/app/core/Engine/controllers/seedless-onboarding-controller/index.test.ts b/app/core/Engine/controllers/seedless-onboarding-controller/index.test.ts new file mode 100644 index 000000000000..e7919dce50e7 --- /dev/null +++ b/app/core/Engine/controllers/seedless-onboarding-controller/index.test.ts @@ -0,0 +1,81 @@ +import { seedlessOnboardingControllerInit } from '.'; +import { ExtendedControllerMessenger } from '../../../ExtendedControllerMessenger'; +import { buildControllerInitRequestMock } from '../../utils/test-utils'; +import { ControllerInitRequest } from '../../types'; +import { + SeedlessOnboardingController, + SeedlessOnboardingControllerMessenger, + SeedlessOnboardingControllerState, +} from '@metamask/seedless-onboarding-controller'; + +jest.mock('@metamask/seedless-onboarding-controller', () => { + const actualSeedlessOnboardingController = jest.requireActual( + '@metamask/seedless-onboarding-controller', + ); + return { + controllerName: actualSeedlessOnboardingController.controllerName, + getDefaultSeedlessOnboardingControllerState: + actualSeedlessOnboardingController.getDefaultSeedlessOnboardingControllerState, + SeedlessOnboardingController: jest.fn(), + Web3AuthNetwork: actualSeedlessOnboardingController.Web3AuthNetwork, + }; +}); + +describe('seedless onboarding controller init', () => { + const seedlessOnboardingControllerClassMock = jest.mocked( + SeedlessOnboardingController, + ); + let initRequestMock: jest.Mocked< + ControllerInitRequest + >; + + beforeEach(() => { + jest.resetAllMocks(); + const baseControllerMessenger = new ExtendedControllerMessenger(); + // Create controller init request mock + initRequestMock = buildControllerInitRequestMock(baseControllerMessenger); + }); + + it('returns controller instance', () => { + expect( + seedlessOnboardingControllerInit(initRequestMock).controller, + ).toBeInstanceOf(SeedlessOnboardingController); + }); + + it('controller state should be default state when no initial state is passed in', () => { + const defaultSeedlessOnboardingControllerState = jest + .requireActual('@metamask/seedless-onboarding-controller') + .getDefaultSeedlessOnboardingControllerState(); + + seedlessOnboardingControllerInit(initRequestMock); + + const seedlessOnboardingControllerState = + seedlessOnboardingControllerClassMock.mock.calls[0][0].state; + + expect(seedlessOnboardingControllerState).toEqual( + defaultSeedlessOnboardingControllerState, + ); + }); + + it('controller state should be initial state when initial state is passed in', () => { + const initialSeedlessOnboardingControllerState: Partial = + { + vault: undefined, + nodeAuthTokens: undefined, + }; + + initRequestMock.persistedState = { + ...initRequestMock.persistedState, + SeedlessOnboardingController: initialSeedlessOnboardingControllerState, + }; + + seedlessOnboardingControllerInit(initRequestMock); + + const seedlessOnboardingControllerState = + seedlessOnboardingControllerClassMock.mock.calls[0][0].state; + + expect(seedlessOnboardingControllerState).toStrictEqual( + initialSeedlessOnboardingControllerState, + ); + }); +}); diff --git a/app/core/Engine/controllers/seedless-onboarding-controller/index.ts b/app/core/Engine/controllers/seedless-onboarding-controller/index.ts new file mode 100644 index 000000000000..2a4a7e53c15e --- /dev/null +++ b/app/core/Engine/controllers/seedless-onboarding-controller/index.ts @@ -0,0 +1,59 @@ +import './shim'; +import type { ControllerInitFunction } from '../../types'; +import { + SeedlessOnboardingController, + SeedlessOnboardingControllerState, + Web3AuthNetwork, + getDefaultSeedlessOnboardingControllerState, + type SeedlessOnboardingControllerMessenger, +} from '@metamask/seedless-onboarding-controller'; +import AppConstants from '../../../AppConstants'; +import { Encryptor, LEGACY_DERIVATION_OPTIONS } from '../../../Encryptor'; +import { EncryptionKey, EncryptionResult } from '../../../Encryptor/types'; + +const web3AuthNetwork = AppConstants.SEEDLESS_ONBOARDING.WEB3AUTH_NETWORK; + +if (!web3AuthNetwork) { + throw new Error( + `Missing environment variables for SeedlessOnboardingController\n + WEB3AUTH_NETWORK: ${web3AuthNetwork}\n`, + ); +} + +const encryptor = new Encryptor({ + keyDerivationOptions: LEGACY_DERIVATION_OPTIONS, +}); + +/** + * Initialize the SeedlessOnboardingController. + * + * @param request - The request object. + * @returns The SeedlessOnboardingController. + */ +export const seedlessOnboardingControllerInit: ControllerInitFunction< + SeedlessOnboardingController, + SeedlessOnboardingControllerMessenger +> = (request) => { + const { controllerMessenger, persistedState } = request; + + const seedlessOnboardingControllerState = + persistedState.SeedlessOnboardingController ?? + getDefaultSeedlessOnboardingControllerState(); + + const controller = new SeedlessOnboardingController({ + messenger: controllerMessenger, + state: + seedlessOnboardingControllerState as SeedlessOnboardingControllerState, + encryptor: { + ...encryptor, + decryptWithKey: async (key: EncryptionKey, encryptedString: string) => + encryptor.decryptWithKey( + key, + encryptedString as unknown as EncryptionResult, + ), + }, + network: web3AuthNetwork as Web3AuthNetwork, + }); + + return { controller }; +}; diff --git a/app/core/Engine/controllers/seedless-onboarding-controller/shim.js b/app/core/Engine/controllers/seedless-onboarding-controller/shim.js new file mode 100644 index 000000000000..cf0b80ac4b39 --- /dev/null +++ b/app/core/Engine/controllers/seedless-onboarding-controller/shim.js @@ -0,0 +1,7 @@ +// eslint-disable-next-line import/no-nodejs-modules +import Crypto from 'crypto'; + +global.crypto = { + ...Crypto, + ...global.crypto, +}; diff --git a/app/core/Engine/messengers/index.ts b/app/core/Engine/messengers/index.ts index a2843e272a95..0fa3a23b4330 100644 --- a/app/core/Engine/messengers/index.ts +++ b/app/core/Engine/messengers/index.ts @@ -27,6 +27,10 @@ import { getNotificationServicesControllerMessenger } from './notifications/noti import { getNotificationServicesPushControllerMessenger } from './notifications/notification-services-push-controller-messenger'; import { getGasFeeControllerMessenger } from './gas-fee-controller-messenger/gas-fee-controller-messenger'; import { getSignatureControllerMessenger } from './signature-controller-messenger'; +///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) +import { getSeedlessOnboardingControllerMessenger } from './seedless-onboarding-controller-messenger'; +///: END:ONLY_INCLUDE_IF + /** * The messengers for the controllers that have been. */ @@ -107,4 +111,10 @@ export const CONTROLLER_MESSENGERS = { getInitMessenger: noop, }, ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) + SeedlessOnboardingController: { + getMessenger: getSeedlessOnboardingControllerMessenger, + getInitMessenger: noop, + }, + ///: END:ONLY_INCLUDE_IF } as const; diff --git a/app/core/Engine/messengers/seedless-onboarding-controller-messenger/index.ts b/app/core/Engine/messengers/seedless-onboarding-controller-messenger/index.ts new file mode 100644 index 000000000000..622cf570a5af --- /dev/null +++ b/app/core/Engine/messengers/seedless-onboarding-controller-messenger/index.ts @@ -0,0 +1,21 @@ +import { BaseControllerMessenger } from '../../types'; + +export type SeedlessOnboardingControllerMessenger = ReturnType< + typeof getSeedlessOnboardingControllerMessenger +>; + +/** + * Get the SeedlessOnboardingControllerMessenger for the SeedlessOnboardingController. + * + * @param baseControllerMessenger - The base controller messenger. + * @returns The SeedlessOnboardingControllerMessenger. + */ +export function getSeedlessOnboardingControllerMessenger( + baseControllerMessenger: BaseControllerMessenger, +) { + return baseControllerMessenger.getRestricted({ + name: 'SeedlessOnboardingController', + allowedEvents: ['KeyringController:lock', 'KeyringController:unlock'], + allowedActions: [], + }); +} diff --git a/app/core/Engine/types.ts b/app/core/Engine/types.ts index 2d2bd500ab61..58e58ca74bd4 100644 --- a/app/core/Engine/types.ts +++ b/app/core/Engine/types.ts @@ -259,6 +259,14 @@ import { EarnControllerEvents, EarnControllerState, } from '@metamask/earn-controller'; +///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) +import { + SeedlessOnboardingController, + SeedlessOnboardingControllerState, + SeedlessOnboardingControllerEvents, +} from '@metamask/seedless-onboarding-controller'; +///: END:ONLY_INCLUDE_IF + import { Hex } from '@metamask/utils'; import { CONTROLLER_MESSENGERS } from './messengers'; @@ -269,6 +277,9 @@ import { AppMetadataControllerEvents, AppMetadataControllerState, } from '@metamask/app-metadata-controller'; +///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) +import { EncryptionKey } from '../Encryptor/types'; +///: END:ONLY_INCLUDE_IF(seedless-onboarding) /** * Controllers that area always instantiated @@ -405,6 +416,9 @@ type GlobalEvents = | BridgeControllerEvents | BridgeStatusControllerEvents | EarnControllerEvents + ///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) + | SeedlessOnboardingControllerEvents + ///: END:ONLY_INCLUDE_IF | AppMetadataControllerEvents; /** @@ -479,6 +493,9 @@ export type Controllers = { BridgeController: BridgeController; BridgeStatusController: BridgeStatusController; EarnController: EarnController; + ///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) + SeedlessOnboardingController: SeedlessOnboardingController; + ///: END:ONLY_INCLUDE_IF }; /** @@ -542,6 +559,9 @@ export type EngineState = { BridgeController: BridgeControllerState; BridgeStatusController: BridgeStatusControllerState; EarnController: EarnControllerState; + ///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) + SeedlessOnboardingController: SeedlessOnboardingControllerState; + ///: END:ONLY_INCLUDE_IF }; /** Controller names */ @@ -593,7 +613,11 @@ export type ControllersToInitialize = | 'MultichainNetworkController' | 'TransactionController' | 'GasFeeController' - | 'SignatureController'; + | 'SignatureController' + ///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding) + | 'SeedlessOnboardingController' + ///: END:ONLY_INCLUDE_IF + | 'AppMetadataController'; /** * Callback that returns a controller messenger for a specific controller. diff --git a/app/core/OAuthService/OAuthInterface.ts b/app/core/OAuthService/OAuthInterface.ts new file mode 100644 index 000000000000..bda467a1fc6d --- /dev/null +++ b/app/core/OAuthService/OAuthInterface.ts @@ -0,0 +1,86 @@ +import { Web3AuthNetwork } from '@metamask/seedless-onboarding-controller'; + +export enum OAuthLoginResultType { + SUCCESS = 'success', + ERROR = 'error', +} + +export interface HandleOAuthLoginResult { + type: OAuthLoginResultType; + existingUser: boolean; + accountName?: string; + error?: string; +} + +export enum AuthConnection { + Google = 'google', + Apple = 'apple', +} + +export interface LoginHandlerCodeResult { + authConnection: AuthConnection; + code: string; + clientId: string; + redirectUri?: string; + codeVerifier?: string; +} + +export interface LoginHandlerIdTokenResult { + authConnection: AuthConnection; + idToken: string; + clientId: string; + redirectUri?: string; + codeVerifier?: string; +} + +export type LoginHandlerResult = + | LoginHandlerCodeResult + | LoginHandlerIdTokenResult; + +export type HandleFlowParams = LoginHandlerResult & { + web3AuthNetwork: Web3AuthNetwork; +}; + +export interface OAuthUserInfo { + email: string; + sub: string; +} + +export interface AuthRequestCodeParams { + code: string; + client_id: string; + login_provider: AuthConnection; + network: Web3AuthNetwork; + redirect_uri?: string; + code_verifier?: string; +} + +export interface AuthRequestIdTokenParams { + id_token: string; + client_id: string; + login_provider: AuthConnection; + network: Web3AuthNetwork; + redirect_uri?: string; + code_verifier?: string; +} + +export type AuthRequestParams = + | AuthRequestCodeParams + | AuthRequestIdTokenParams; + +export interface AuthResponse { + id_token: string; + refresh_token?: string; + indexes: number[]; + endpoints: Record; + success: boolean; + message: string; + jwt_tokens: Record; +} + +export interface LoginHandler { + get authConnection(): AuthConnection; + get scope(): string[]; + get authServerPath(): string; + login(): Promise; +} diff --git a/app/core/OAuthService/OAuthLoginHandlers/androidHandlers/apple.ts b/app/core/OAuthService/OAuthLoginHandlers/androidHandlers/apple.ts new file mode 100644 index 000000000000..48e5bd912bad --- /dev/null +++ b/app/core/OAuthService/OAuthLoginHandlers/androidHandlers/apple.ts @@ -0,0 +1,148 @@ +import { + CodeChallengeMethod, + ResponseType, + AuthRequest, +} from 'expo-auth-session'; +import { + AuthConnection, + LoginHandler, + LoginHandlerCodeResult, +} from '../../OAuthInterface'; +import { BaseLoginHandler } from '../baseHandler'; +import { OAuthError, OAuthErrorType } from '../../error'; +export interface AndroidAppleLoginHandlerParams { + clientId: string; + redirectUri: string; + appRedirectUri: string; +} + +/** + * AndroidAppleLoginHandler is the login handler for the Apple login on android. + */ +export class AndroidAppleLoginHandler + extends BaseLoginHandler + implements LoginHandler +{ + public readonly OAUTH_SERVER_URL = 'https://appleid.apple.com/auth/authorize'; + + readonly #scope = ['name', 'email']; + + protected clientId: string; + protected redirectUri: string; + protected appRedirectUri: string; + + get authConnection() { + return AuthConnection.Apple; + } + + get scope() { + return this.#scope; + } + + get authServerPath() { + return 'api/v1/oauth/token'; + } + + /** + * AndroidAppleLoginHandler constructor. + * + * @param params.clientId - The Service ID from the apple developer account for the app. + * @param params.redirectUri - The server redirectUri for the Apple login to handle rest api login. + * @param params.appRedirectUri - The Android App redirectUri for the customChromeTab to handle auth-session login. + */ + constructor(params: AndroidAppleLoginHandlerParams) { + super(); + const { appRedirectUri, redirectUri, clientId } = params; + this.clientId = clientId; + this.redirectUri = redirectUri; + this.appRedirectUri = appRedirectUri; + } + + /** + * This method is used to login with apple via customChromeTab via expo-auth-session. + * It generates the auth url with server redirect uri and state. + * It creates a client auth request instance so that the auth-session can return result on appRedirectUrl. + * It then prompts the auth request via the client auth request instance with auth url generated with server redirect uri and state. + * + * Data flow: + * App -> Apple -> AuthServer -> App + * + * @returns LoginHandlerCodeResult + */ + async login(): Promise { + const state = JSON.stringify({ + provider: this.authConnection, + client_redirect_back_uri: this.appRedirectUri, + redirectUri: this.redirectUri, + clientId: this.clientId, + random: this.nonce, + }); + const authRequest = new AuthRequest({ + clientId: this.clientId, + redirectUri: this.redirectUri, + scopes: this.#scope, + responseType: ResponseType.Code, + codeChallengeMethod: CodeChallengeMethod.S256, + usePKCE: true, + state, + extraParams: { + response_mode: 'form_post', + }, + }); + // generate the auth url + const authUrl = await authRequest.makeAuthUrlAsync({ + authorizationEndpoint: this.OAUTH_SERVER_URL, + }); + + // create a client auth request instance so that the auth-session can return result on appRedirectUrl + const authRequestClient = new AuthRequest({ + clientId: this.clientId, + redirectUri: this.appRedirectUri, + state, + }); + + // prompt the auth request using generated auth url instead of the client auth request instance + const result = await authRequestClient.promptAsync( + { + authorizationEndpoint: this.OAUTH_SERVER_URL, + }, + { + url: authUrl, + }, + ); + if (result.type === 'success') { + return { + authConnection: AuthConnection.Apple, + code: result.params.code, + clientId: this.clientId, + redirectUri: this.redirectUri, + codeVerifier: authRequest.codeVerifier, + }; + } + if (result.type === 'error') { + if (result.error) { + throw new OAuthError(result.error.message, OAuthErrorType.LoginError); + } + throw new OAuthError( + 'handleAndroidAppleLogin: Unknown error', + OAuthErrorType.UnknownError, + ); + } + if (result.type === 'cancel') { + throw new OAuthError( + 'handleAndroidAppleLogin: User cancelled the login process', + OAuthErrorType.UserCancelled, + ); + } + if (result.type === 'dismiss') { + throw new OAuthError( + 'handleAndroidAppleLogin: User dismissed the login process', + OAuthErrorType.UserDismissed, + ); + } + throw new OAuthError( + 'handleAndroidAppleLogin: Unknown error', + OAuthErrorType.UnknownError, + ); + } +} diff --git a/app/core/OAuthService/OAuthLoginHandlers/androidHandlers/google.ts b/app/core/OAuthService/OAuthLoginHandlers/androidHandlers/google.ts new file mode 100644 index 000000000000..ab2fab50763d --- /dev/null +++ b/app/core/OAuthService/OAuthLoginHandlers/androidHandlers/google.ts @@ -0,0 +1,86 @@ +import { + LoginHandlerIdTokenResult, + AuthConnection, +} from '../../OAuthInterface'; +import { signInWithGoogle } from 'react-native-google-acm'; +import { BaseLoginHandler } from '../baseHandler'; +import { OAuthErrorType, OAuthError } from '../../error'; + +/** + * AndroidGoogleLoginHandler is the login handler for the Google login on android. + */ +export class AndroidGoogleLoginHandler extends BaseLoginHandler { + readonly #scope = ['email', 'profile']; + + protected clientId: string; + + get authConnection() { + return AuthConnection.Google; + } + + get scope() { + return this.#scope; + } + + get authServerPath() { + return 'api/v1/oauth/id_token'; + } + + /** + * This constructor is used to initialize the clientId. + * + * @param params.clientId - The web clientId for the Google login. + * Note: The android clientId must be created from the same OAuth clientId in the web. + */ + constructor(params: { clientId: string }) { + super(); + this.clientId = params.clientId; + } + + /** + * This method is used to login with google seemsless via react-native-google-acm. + * + * @returns LoginHandlerIdTokenResult + */ + async login(): Promise { + try { + const result = await signInWithGoogle({ + serverClientId: this.clientId, + nonce: this.nonce, + autoSelectEnabled: true, + filterByAuthorizedAccounts: false, + }); + + if (result?.type === 'google-signin') { + return { + authConnection: this.authConnection, + idToken: result.idToken, + clientId: this.clientId, + }; + } + + throw new OAuthError( + 'handleGoogleLogin: Unknown error', + OAuthErrorType.UnknownError, + ); + } catch (error) { + if (error instanceof OAuthError) { + throw error; + } else if (error instanceof Error) { + if (error.message.includes('cancelled')) { + throw new OAuthError( + 'handleGoogleLogin: User cancelled the login process', + OAuthErrorType.UserCancelled, + ); + } else { + throw new OAuthError(error, OAuthErrorType.UnknownError); + } + } else { + throw new OAuthError( + 'handleGoogleLogin: Unknown error', + OAuthErrorType.UnknownError, + ); + } + } + } +} diff --git a/app/core/OAuthService/OAuthLoginHandlers/baseHandler.ts b/app/core/OAuthService/OAuthLoginHandlers/baseHandler.ts new file mode 100644 index 000000000000..420a598618f2 --- /dev/null +++ b/app/core/OAuthService/OAuthLoginHandlers/baseHandler.ts @@ -0,0 +1,156 @@ +import { + AuthConnection, + AuthRequestParams, + AuthResponse, + HandleFlowParams, + LoginHandlerResult, +} from '../OAuthInterface'; +import { OAuthError, OAuthErrorType } from '../error'; + +/** + * Pads a string to a length of 4 characters + * + * @param input - The base64 encoded string to pad + * @returns The padded string + */ +function padBase64String(input: string) { + const segmentLength = 4; + const stringLength = input.length; + const diff = stringLength % segmentLength; + if (!diff) { + return input; + } + let position = stringLength; + let padLength = segmentLength - diff; + const paddedStringLength = stringLength + padLength; + const buffer = Buffer.alloc(paddedStringLength); + buffer.write(input); + while (padLength > 0) { + buffer.write('=', position); + position += 1; + padLength -= 1; + } + return buffer.toString(); +} + +/** + * Get the auth tokens from the auth server + * + * @param params - The params required to get the auth tokens + * @param params.authConnection - The auth connection type (Google, Apple, etc.) + * @param params.clientId - The client id of the app ( clientId for Google, Service ID or Bundle ID for Apple) + * @param params.redirectUri - The redirect uri of the app used for the login + * @param params.codeVerifier - The PKCE code verifier if PKCE is used + * @param params.web3AuthNetwork - The web3 auth network (sapphire_mainnet, sapphire_devnet, etc.) + * + * @param pathname - The pathname(endpoint) of the auth server + * @param authServerUrl - The url of the auth server + */ +export async function getAuthTokens( + params: HandleFlowParams, + pathname: string, + authServerUrl: string, +): Promise { + const { + authConnection, + clientId, + redirectUri, + codeVerifier, + web3AuthNetwork, + } = params; + + let body: AuthRequestParams; + + if ('code' in params) { + body = { + code: params.code, + client_id: clientId, + login_provider: authConnection, + network: web3AuthNetwork, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + }; + } else { + body = { + id_token: params.idToken, + client_id: clientId, + login_provider: authConnection, + network: web3AuthNetwork, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + }; + } + + const res = await fetch(`${authServerUrl}/${pathname}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (res.status === 200) { + const data = (await res.json()) satisfies AuthResponse; + if (data.success) { + return data; + } + throw new OAuthError(data.message, OAuthErrorType.AuthServerError); + } + + throw new OAuthError( + `AuthServer Error, request failed with status: [${ + res.status + }]: ${await res.text()}`, + OAuthErrorType.AuthServerError, + ); +} + +/** + * Base class for the login handlers + */ +export abstract class BaseLoginHandler { + public nonce: string; + + abstract get authConnection(): AuthConnection; + + abstract get scope(): string[]; + + abstract get authServerPath(): string; + + abstract login(): Promise; + + constructor() { + this.nonce = this.#generateNonce(); + } + + /** + * Get the auth tokens from the auth server + * + * @param params - The params from the login handler + * @param authServerUrl - The url of the auth server + */ + getAuthTokens(params: HandleFlowParams, authServerUrl: string) { + return getAuthTokens(params, this.authServerPath, authServerUrl); + } + + /** + * Decode the JWT Token to get the user's information. + * + * @param idToken - The JWT Token from the Web3Auth Authentication Server. + * @returns The user's information from the JWT Token. + */ + decodeIdToken(idToken: string): string { + const [, idTokenPayload] = idToken.split('.'); + const base64String = padBase64String(idTokenPayload) + .replace(/-/u, '+') + .replace(/_/u, '/'); + // Using buffer here instead of atob because userinfo can contain emojis which are not supported by atob + // the browser replacement for atob is https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64 + // which is not supported in all chrome yet + return Buffer.from(base64String, 'base64').toString('utf-8'); + } + + #generateNonce(): string { + return Math.random().toString(36).substring(2, 15); + } +} diff --git a/app/core/OAuthService/OAuthLoginHandlers/constants.ts b/app/core/OAuthService/OAuthLoginHandlers/constants.ts new file mode 100644 index 000000000000..49a4426faeb8 --- /dev/null +++ b/app/core/OAuthService/OAuthLoginHandlers/constants.ts @@ -0,0 +1,49 @@ +import { ACTIONS, PROTOCOLS } from '../../../constants/deeplinks'; +import AppConstants from '../../AppConstants'; + +export const AppRedirectUri = `${PROTOCOLS.HTTPS}://${AppConstants.MM_UNIVERSAL_LINK_HOST}/${ACTIONS.OAUTH_REDIRECT}`; + +if ( + !AppConstants.SEEDLESS_ONBOARDING.WEB3AUTH_NETWORK || + !AppConstants.SEEDLESS_ONBOARDING.AUTH_SERVER_URL || + !AppConstants.SEEDLESS_ONBOARDING.IOS_GOOGLE_CLIENT_ID || + !AppConstants.SEEDLESS_ONBOARDING.IOS_GOOGLE_REDIRECT_URI || + !AppConstants.SEEDLESS_ONBOARDING.IOS_APPLE_CLIENT_ID || + !AppConstants.SEEDLESS_ONBOARDING.ANDROID_WEB_GOOGLE_CLIENT_ID || + !AppConstants.SEEDLESS_ONBOARDING.ANDROID_WEB_APPLE_CLIENT_ID || + !AppConstants.SEEDLESS_ONBOARDING.AUTH_CONNECTION_ID || + !AppConstants.SEEDLESS_ONBOARDING.GROUPED_AUTH_CONNECTION_ID +) { + throw new Error( + `Missing environment variables for OAuthLoginHandlers\n + WEB3AUTH_NETWORK: ${AppConstants.SEEDLESS_ONBOARDING.WEB3AUTH_NETWORK}\n + AUTH_SERVER_URL: ${AppConstants.SEEDLESS_ONBOARDING.AUTH_SERVER_URL}\n + IOS_GOOGLE_CLIENT_ID: ${AppConstants.SEEDLESS_ONBOARDING.IOS_GOOGLE_CLIENT_ID}\n + IOS_GOOGLE_REDIRECT_URI: ${AppConstants.SEEDLESS_ONBOARDING.IOS_GOOGLE_REDIRECT_URI}\n + IOS_APPLE_CLIENT_ID: ${AppConstants.SEEDLESS_ONBOARDING.IOS_APPLE_CLIENT_ID}\n + ANDROID_WEB_GOOGLE_CLIENT_ID: ${AppConstants.SEEDLESS_ONBOARDING.ANDROID_WEB_GOOGLE_CLIENT_ID}\n + ANDROID_WEB_APPLE_CLIENT_ID: ${AppConstants.SEEDLESS_ONBOARDING.ANDROID_WEB_APPLE_CLIENT_ID}\n`, + ); +} + +export const web3AuthNetwork = + AppConstants.SEEDLESS_ONBOARDING.WEB3AUTH_NETWORK; +export const AuthServerUrl = AppConstants.SEEDLESS_ONBOARDING.AUTH_SERVER_URL; + +export const IosGID = AppConstants.SEEDLESS_ONBOARDING.IOS_GOOGLE_CLIENT_ID; +export const IosGoogleRedirectUri = + AppConstants.SEEDLESS_ONBOARDING.IOS_GOOGLE_REDIRECT_URI; +export const IosAppleClientId = + AppConstants.SEEDLESS_ONBOARDING.IOS_APPLE_CLIENT_ID; + +export const AndroidGoogleWebGID = + AppConstants.SEEDLESS_ONBOARDING.ANDROID_WEB_GOOGLE_CLIENT_ID; +export const AppleWebClientId = + AppConstants.SEEDLESS_ONBOARDING.ANDROID_WEB_APPLE_CLIENT_ID; + +export const AuthConnectionId = + AppConstants.SEEDLESS_ONBOARDING.AUTH_CONNECTION_ID; +export const GroupedAuthConnectionId = + AppConstants.SEEDLESS_ONBOARDING.GROUPED_AUTH_CONNECTION_ID; + +export const AppleServerRedirectUri = `${AuthServerUrl}/api/v1/oauth/callback`; diff --git a/app/core/OAuthService/OAuthLoginHandlers/index.test.ts b/app/core/OAuthService/OAuthLoginHandlers/index.test.ts new file mode 100644 index 000000000000..17c797b7ec07 --- /dev/null +++ b/app/core/OAuthService/OAuthLoginHandlers/index.test.ts @@ -0,0 +1,85 @@ +import { Platform } from 'react-native'; +import { AuthConnection } from '../OAuthInterface'; +import { createLoginHandler } from './index'; + +const mockExpoAuthSessionPromptAsync = jest.fn().mockResolvedValue({ + type: 'success', + params: { + code: 'googleCode', + }, +}); +jest.mock('expo-auth-session', () => ({ + AuthRequest: () => ({ + promptAsync: mockExpoAuthSessionPromptAsync, + makeAuthUrlAsync: jest.fn().mockResolvedValue({ + url: 'https://example.com', + }), + }), + CodeChallengeMethod: jest.fn(), + ResponseType: jest.fn(), +})); + +const mockSignInAsync = jest.fn().mockResolvedValue({ + identityToken: 'appleIdToken', +}); +jest.mock('expo-apple-authentication', () => ({ + signInAsync: () => mockSignInAsync(), + AppleAuthenticationScope: jest.fn(), +})); + +const mockSignInWithGoogle = jest.fn().mockResolvedValue({ + type: 'google-signin', + idToken: 'googleIdToken', +}); +jest.mock('react-native-google-acm', () => ({ + signInWithGoogle: () => mockSignInWithGoogle(), +})); + +describe('OAuth login handlers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + for (const os of ['ios', 'android']) { + for (const provider of Object.values(AuthConnection)) { + it(`should create the correct login handler for ${os} and ${provider}`, async () => { + const handler = createLoginHandler(os as Platform['OS'], provider); + const result = await handler.login(); + expect(result?.authConnection).toBe(provider); + + switch (os) { + case 'ios': { + switch (provider) { + case AuthConnection.Apple: + expect(mockExpoAuthSessionPromptAsync).toHaveBeenCalledTimes(0); + expect(mockSignInWithGoogle).toHaveBeenCalledTimes(0); + expect(mockSignInAsync).toHaveBeenCalledTimes(1); + break; + case AuthConnection.Google: + expect(mockExpoAuthSessionPromptAsync).toHaveBeenCalledTimes(1); + expect(mockSignInWithGoogle).toHaveBeenCalledTimes(0); + expect(mockSignInAsync).toHaveBeenCalledTimes(0); + break; + } + break; + } + case 'android': { + switch (provider) { + case AuthConnection.Apple: + expect(mockExpoAuthSessionPromptAsync).toHaveBeenCalledTimes(1); + expect(mockSignInWithGoogle).toHaveBeenCalledTimes(0); + expect(mockSignInAsync).toHaveBeenCalledTimes(0); + break; + case AuthConnection.Google: + expect(mockExpoAuthSessionPromptAsync).toHaveBeenCalledTimes(0); + expect(mockSignInWithGoogle).toHaveBeenCalledTimes(1); + expect(mockSignInAsync).toHaveBeenCalledTimes(0); + break; + } + break; + } + } + }); + } + } +}); diff --git a/app/core/OAuthService/OAuthLoginHandlers/index.ts b/app/core/OAuthService/OAuthLoginHandlers/index.ts new file mode 100644 index 000000000000..68edf39a9940 --- /dev/null +++ b/app/core/OAuthService/OAuthLoginHandlers/index.ts @@ -0,0 +1,82 @@ +import { Platform } from 'react-native'; +import { AuthConnection } from '../OAuthInterface'; +import { IosGoogleLoginHandler } from './iosHandlers/google'; +import { IosAppleLoginHandler } from './iosHandlers/apple'; +import { AndroidGoogleLoginHandler } from './androidHandlers/google'; +import { AndroidAppleLoginHandler } from './androidHandlers/apple'; +import { + AuthServerUrl, + AppRedirectUri, + IosGID, + IosGoogleRedirectUri, + AndroidGoogleWebGID, + AppleWebClientId, + IosAppleClientId, + AppleServerRedirectUri, +} from './constants'; +import { OAuthErrorType, OAuthError } from '../error'; +import { BaseLoginHandler } from './baseHandler'; + +/** + * This factory pattern function is used to create a login handler based on the platform and provider. + * + * @param platformOS - The platform of the device (ios, android) + * @param provider - The provider of the login (Google, Apple) + * @returns The login handler + */ +export function createLoginHandler( + platformOS: Platform['OS'], + provider: AuthConnection, +): BaseLoginHandler { + if ( + !AuthServerUrl || + !AppRedirectUri || + !IosGID || + !IosGoogleRedirectUri || + !AndroidGoogleWebGID || + !AppleWebClientId || + !IosAppleClientId + ) { + throw new Error('Missing environment variables'); + } + switch (platformOS) { + case 'ios': + switch (provider) { + case AuthConnection.Google: + return new IosGoogleLoginHandler({ + clientId: IosGID, + redirectUri: IosGoogleRedirectUri, + }); + case AuthConnection.Apple: + return new IosAppleLoginHandler({ clientId: IosAppleClientId }); + default: + throw new OAuthError( + 'Invalid provider', + OAuthErrorType.InvalidProvider, + ); + } + case 'android': + switch (provider) { + case AuthConnection.Google: + return new AndroidGoogleLoginHandler({ + clientId: AndroidGoogleWebGID, + }); + case AuthConnection.Apple: + return new AndroidAppleLoginHandler({ + clientId: AppleWebClientId, + redirectUri: AppleServerRedirectUri, + appRedirectUri: AppRedirectUri, + }); + default: + throw new OAuthError( + 'Invalid provider', + OAuthErrorType.InvalidProvider, + ); + } + default: + throw new OAuthError( + 'Unsupported Platform', + OAuthErrorType.UnsupportedPlatform, + ); + } +} diff --git a/app/core/OAuthService/OAuthLoginHandlers/iosHandlers/apple.ts b/app/core/OAuthService/OAuthLoginHandlers/iosHandlers/apple.ts new file mode 100644 index 000000000000..da6e31b0e607 --- /dev/null +++ b/app/core/OAuthService/OAuthLoginHandlers/iosHandlers/apple.ts @@ -0,0 +1,92 @@ +import { + LoginHandlerIdTokenResult, + AuthConnection, +} from '../../OAuthInterface'; +import { + signInAsync, + AppleAuthenticationScope, +} from 'expo-apple-authentication'; +import { BaseLoginHandler } from '../baseHandler'; +import { OAuthErrorType, OAuthError } from '../../error'; +import Logger from '../../../../util/Logger'; + +/** + * IosAppleLoginHandler is the login handler for the Apple login on ios. + */ +export class IosAppleLoginHandler extends BaseLoginHandler { + readonly #scope = [ + AppleAuthenticationScope.FULL_NAME, + AppleAuthenticationScope.EMAIL, + ]; + + protected clientId: string; + + get authConnection() { + return AuthConnection.Apple; + } + + get scope() { + return this.#scope.map((scope) => scope.toString()); + } + + get authServerPath() { + return 'api/v1/oauth/id_token'; + } + + /** + * This constructor is used to initialize the clientId. + * + * @param params.clientId - The Bundle ID from the apple developer account for the app. + */ + constructor(params: { clientId: string }) { + super(); + this.clientId = params.clientId; + } + + /** + * This method is used to login with apple via expo-apple-authentication. + * + * @returns LoginHandlerIdTokenResult + */ + async login(): Promise { + try { + const credential = await signInAsync({ + requestedScopes: this.#scope, + }); + + if (credential.identityToken) { + return { + authConnection: this.authConnection, + idToken: credential.identityToken, + clientId: this.clientId, + }; + } + throw new OAuthError( + 'handleIosAppleLogin: Unknown error', + OAuthErrorType.UnknownError, + ); + } catch (error) { + Logger.log('handleIosAppleLogin: Error', error); + + if (error instanceof OAuthError) { + throw error; + } else if (error instanceof Error) { + if ( + error.message.includes('The user canceled the authorization attempt') + ) { + throw new OAuthError( + 'handleIosAppleLogin: User canceled the authorization attempt', + OAuthErrorType.UserCancelled, + ); + } else { + throw new OAuthError(error, OAuthErrorType.UnknownError); + } + } else { + throw new OAuthError( + 'handleIosAppleLogin: Unknown error', + OAuthErrorType.UnknownError, + ); + } + } + } +} diff --git a/app/core/OAuthService/OAuthLoginHandlers/iosHandlers/google.ts b/app/core/OAuthService/OAuthLoginHandlers/iosHandlers/google.ts new file mode 100644 index 000000000000..6934877eea4e --- /dev/null +++ b/app/core/OAuthService/OAuthLoginHandlers/iosHandlers/google.ts @@ -0,0 +1,102 @@ +import { LoginHandlerCodeResult, AuthConnection } from '../../OAuthInterface'; +import { + AuthRequest, + CodeChallengeMethod, + ResponseType, +} from 'expo-auth-session'; +import { BaseLoginHandler } from '../baseHandler'; +import { OAuthErrorType, OAuthError } from '../../error'; + +/** + * IosGoogleLoginHandlerParams is the params for the Google login handler + */ +export interface IosGoogleLoginHandlerParams { + clientId: string; + redirectUri: string; +} + +/** + * IosGoogleLoginHandler is the login handler for the Google login + */ +export class IosGoogleLoginHandler extends BaseLoginHandler { + public readonly OAUTH_SERVER_URL = + 'https://accounts.google.com/o/oauth2/v2/auth'; + + readonly #scope = ['email', 'profile']; + + protected clientId: string; + protected redirectUri: string; + + get authConnection() { + return AuthConnection.Google; + } + + get scope() { + return this.#scope; + } + + get authServerPath() { + return 'api/v1/oauth/token'; + } + + /** + * IosGoogleLoginHandler constructor. + * + * @param params.clientId - The iOS clientId for the Google login. + * @param params.redirectUri - The iOS redirectUri for the Google login. + */ + constructor(params: IosGoogleLoginHandlerParams) { + super(); + this.clientId = params.clientId; + this.redirectUri = params.redirectUri; + } + + /** + * This method is used to login with Google via expo-auth-session. + * + * @returns LoginHandlerCodeResult + */ + async login(): Promise { + const state = JSON.stringify({ + nonce: this.nonce, + }); + const authRequest = new AuthRequest({ + clientId: this.clientId, + redirectUri: this.redirectUri, + scopes: this.#scope, + responseType: ResponseType.Code, + codeChallengeMethod: CodeChallengeMethod.S256, + usePKCE: true, + state, + }); + const result = await authRequest.promptAsync({ + authorizationEndpoint: this.OAUTH_SERVER_URL, + }); + + if (result.type === 'success') { + return { + authConnection: this.authConnection, + code: result.params.code, // result.params.idToken + clientId: this.clientId, + redirectUri: this.redirectUri, + codeVerifier: authRequest.codeVerifier, + }; + } + if (result.type === 'cancel') { + throw new OAuthError( + 'handleIosGoogleLogin: User cancelled the login process', + OAuthErrorType.UserCancelled, + ); + } + if (result.type === 'dismiss') { + throw new OAuthError( + 'handleIosGoogleLogin: User dismissed the login process', + OAuthErrorType.UserDismissed, + ); + } + throw new OAuthError( + 'handleIosGoogleLogin: Unknown error', + OAuthErrorType.UnknownError, + ); + } +} diff --git a/app/core/OAuthService/OAuthService.test.ts b/app/core/OAuthService/OAuthService.test.ts new file mode 100644 index 000000000000..0db532b3f8d5 --- /dev/null +++ b/app/core/OAuthService/OAuthService.test.ts @@ -0,0 +1,198 @@ +import { + AuthConnection, + AuthResponse, + LoginHandlerResult, +} from './OAuthInterface'; +import OAuthLoginService from './OAuthService'; +import ReduxService, { ReduxStore } from '../redux'; +import Engine from '../Engine'; +import { OAuthError, OAuthErrorType } from './error'; +import { Web3AuthNetwork } from '@metamask/seedless-onboarding-controller'; + +const OAUTH_AUD = 'metamask'; +const MOCK_USER_ID = 'user-id'; +const MOCK_JWT_TOKEN = + 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InN3bmFtOTA5QGdtYWlsLmNvbSIsInN1YiI6InN3bmFtOTA5QGdtYWlsLmNvbSIsImlzcyI6Im1ldGFtYXNrIiwiYXVkIjoibWV0YW1hc2siLCJpYXQiOjE3NDUyMDc1NjYsImVhdCI6MTc0NTIwNzg2NiwiZXhwIjoxNzQ1MjA3ODY2fQ.nXRRLB7fglRll7tMzFFCU0u7Pu6EddqEYf_DMyRgOENQ6tJ8OLtVknNf83_5a67kl_YKHFO-0PEjvJviPID6xg'; + +let mockLoginHandlerResponse: () => LoginHandlerResult | undefined = jest + .fn() + .mockImplementation(() => ({ + idToken: MOCK_JWT_TOKEN, + authConnection: AuthConnection.Google, + clientId: 'clientId', + web3AuthNetwork: Web3AuthNetwork.Mainnet, + })); + +let mockGetAuthTokens: () => Promise = jest + .fn() + .mockImplementation(() => ({ + verifier_id: MOCK_USER_ID, + jwt_tokens: { + [OAUTH_AUD]: MOCK_JWT_TOKEN, + }, + })); + +const mockCreateLoginHandler = jest.fn().mockImplementation(() => ({ + login: () => mockLoginHandlerResponse(), + getAuthTokens: mockGetAuthTokens, + decodeIdToken: () => + JSON.stringify({ + email: 'swnam909@gmail.com', + sub: 'swnam909@gmail.com', + iss: 'metamask', + aud: 'metamask', + iat: 1745207566, + eat: 1745207866, + exp: 1745207866, + }), +})); + +jest.mock('../Engine', () => ({ + context: { + SeedlessOnboardingController: { + authenticate: jest.fn().mockImplementation(() => ({ + nodeAuthTokens: [], + isNewUser: false, + })), + }, + }, +})); + +let mockAuthenticate = jest.fn().mockImplementation(() => ({ + nodeAuthTokens: [], + isNewUser: true, +})); +jest + .spyOn(Engine.context.SeedlessOnboardingController, 'authenticate') + .mockImplementation(mockAuthenticate); + +const expectOAuthError = async ( + promiseFunc: Promise, + errorType: OAuthErrorType, +) => { + await expect(promiseFunc).rejects.toThrow(OAuthError); + try { + await promiseFunc; + } catch (error) { + if (error instanceof OAuthError) { + expect(error.code).toBe(errorType); + } else { + fail('Expected OAuthError'); + } + } +}; + +describe('OAuth login service', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(ReduxService, 'store', 'get').mockReturnValue({ + getState: () => ({ security: { allowLoginWithRememberMe: true } }), + dispatch: jest.fn(), + } as unknown as ReduxStore); + }); + + it('should return a type success', async () => { + const loginHandler = mockCreateLoginHandler(); + const result = (await OAuthLoginService.handleOAuthLogin(loginHandler)) as { + type: string; + existingUser: boolean; + }; + expect(result).toBeDefined(); + expect(result.type).toBe('success'); + expect(result.existingUser).toBe(false); + + expect(mockLoginHandlerResponse).toHaveBeenCalledTimes(1); + expect(mockGetAuthTokens).toHaveBeenCalledTimes(1); + expect(mockAuthenticate).toHaveBeenCalledTimes(1); + }); + + it('should return a type success, existing user', async () => { + const loginHandler = mockCreateLoginHandler(); + mockAuthenticate = jest.fn().mockImplementation(() => ({ + nodeAuthTokens: [], + isNewUser: false, + })); + jest + .spyOn(Engine.context.SeedlessOnboardingController, 'authenticate') + .mockImplementation(mockAuthenticate); + + const result = await OAuthLoginService.handleOAuthLogin(loginHandler); + expect(result).toBeDefined(); + + expect(mockLoginHandlerResponse).toHaveBeenCalledTimes(1); + expect(mockGetAuthTokens).toHaveBeenCalledTimes(1); + expect(mockAuthenticate).toHaveBeenCalledTimes(1); + }); + + it('should throw on SeedlessOnboardingController error', async () => { + const loginHandler = mockCreateLoginHandler(); + mockAuthenticate = jest.fn().mockImplementation(() => { + throw new Error('Test error'); + }); + jest + .spyOn(Engine.context.SeedlessOnboardingController, 'authenticate') + .mockImplementation(mockAuthenticate); + + await expectOAuthError( + OAuthLoginService.handleOAuthLogin(loginHandler), + OAuthErrorType.LoginError, + ); + + expect(mockLoginHandlerResponse).toHaveBeenCalledTimes(1); + expect(mockGetAuthTokens).toHaveBeenCalledTimes(1); + expect(mockAuthenticate).toHaveBeenCalledTimes(1); + }); + + it('should throw on AuthServerError', async () => { + mockGetAuthTokens = jest.fn().mockImplementation(() => { + throw new OAuthError('Auth server error', OAuthErrorType.AuthServerError); + }); + const loginHandler = mockCreateLoginHandler(); + + await expectOAuthError( + OAuthLoginService.handleOAuthLogin(loginHandler), + OAuthErrorType.AuthServerError, + ); + + expect(mockLoginHandlerResponse).toHaveBeenCalledTimes(1); + expect(mockGetAuthTokens).toHaveBeenCalledTimes(1); + expect(mockAuthenticate).toHaveBeenCalledTimes(0); + }); + + it('should throw on dismiss', async () => { + const loginHandler = mockCreateLoginHandler(); + jest.spyOn(ReduxService, 'store', 'get').mockReturnValue({ + getState: () => ({ security: { allowLoginWithRememberMe: true } }), + dispatch: jest.fn(), + } as unknown as ReduxStore); + + mockLoginHandlerResponse = jest.fn().mockImplementation(() => { + throw new OAuthError('Login dismissed', OAuthErrorType.UserDismissed); + }); + + await expectOAuthError( + OAuthLoginService.handleOAuthLogin(loginHandler), + OAuthErrorType.UserDismissed, + ); + + expect(mockLoginHandlerResponse).toHaveBeenCalledTimes(1); + expect(mockGetAuthTokens).toHaveBeenCalledTimes(0); + expect(mockAuthenticate).toHaveBeenCalledTimes(0); + }); + + it('should throw on login error', async () => { + const loginHandler = mockCreateLoginHandler(); + mockLoginHandlerResponse = jest.fn().mockImplementation(() => { + throw new OAuthError('Login error', OAuthErrorType.LoginError); + }); + + await expectOAuthError( + OAuthLoginService.handleOAuthLogin(loginHandler), + OAuthErrorType.LoginError, + ); + + expect(mockLoginHandlerResponse).toHaveBeenCalledTimes(1); + expect(mockGetAuthTokens).toHaveBeenCalledTimes(0); + expect(mockAuthenticate).toHaveBeenCalledTimes(0); + }); +}); diff --git a/app/core/OAuthService/OAuthService.ts b/app/core/OAuthService/OAuthService.ts new file mode 100644 index 000000000000..b01e0c1cfddb --- /dev/null +++ b/app/core/OAuthService/OAuthService.ts @@ -0,0 +1,224 @@ +import { Platform } from 'react-native'; +import Engine from '../Engine'; +import Logger from '../../util/Logger'; +import ReduxService from '../redux'; + +import { UserActionType } from '../../actions/user'; +import { + HandleOAuthLoginResult, + AuthConnection, + AuthResponse, + OAuthUserInfo, + OAuthLoginResultType, +} from './OAuthInterface'; +import { Web3AuthNetwork } from '@metamask/seedless-onboarding-controller'; +import { + AuthConnectionId, + AuthServerUrl, + web3AuthNetwork as currentWeb3AuthNetwork, + GroupedAuthConnectionId, +} from './OAuthLoginHandlers/constants'; +import { OAuthError, OAuthErrorType } from './error'; +import { BaseLoginHandler } from './OAuthLoginHandlers/baseHandler'; + +export interface OAuthServiceConfig { + authConnectionId: string; + groupedAuthConnectionId?: string; + web3AuthNetwork: Web3AuthNetwork; + authServerUrl: string; +} + +interface OAuthServiceLocalState { + userId?: string; + accountName?: string; + loginInProgress: boolean; + oauthLoginSuccess: boolean; + oauthLoginError: string | null; +} +export class OAuthService { + public localState: OAuthServiceLocalState; + + public config: OAuthServiceConfig; + + constructor(config: OAuthServiceConfig) { + const { + authServerUrl, + web3AuthNetwork, + authConnectionId, + groupedAuthConnectionId, + } = config; + this.localState = { + loginInProgress: false, + userId: undefined, + accountName: undefined, + oauthLoginSuccess: false, + oauthLoginError: null, + }; + this.config = { + authConnectionId, + groupedAuthConnectionId, + web3AuthNetwork, + authServerUrl, + }; + } + + #dispatchLogin = () => { + this.resetOauthState(); + this.updateLocalState({ loginInProgress: true }); + ReduxService.store.dispatch({ + type: UserActionType.LOADING_SET, + }); + }; + + #dispatchPostLogin = (result: HandleOAuthLoginResult) => { + const stateToUpdate: Partial = { + loginInProgress: false, + }; + if (result.type === OAuthLoginResultType.SUCCESS) { + stateToUpdate.oauthLoginSuccess = true; + stateToUpdate.oauthLoginError = null; + } else { + stateToUpdate.oauthLoginSuccess = false; + stateToUpdate.oauthLoginError = result.error; + } + this.updateLocalState(stateToUpdate); + ReduxService.store.dispatch({ + type: UserActionType.LOADING_UNSET, + }); + }; + + handleSeedlessAuthenticate = async ( + data: AuthResponse, + authConnection: AuthConnection, + ): Promise => { + try { + const { userId, accountName } = this.localState; + + if (!userId) { + throw new Error('No user id found'); + } + + const result = + await Engine.context.SeedlessOnboardingController.authenticate({ + idTokens: Object.values(data.jwt_tokens), + authConnection, + authConnectionId: this.config.authConnectionId, + groupedAuthConnectionId: this.config.groupedAuthConnectionId, + userId, + socialLoginEmail: accountName, + }); + Logger.log('handleCodeFlow: result', result); + return { + type: OAuthLoginResultType.SUCCESS, + existingUser: !result.isNewUser, + accountName, + }; + } catch (error) { + Logger.log(error as Error, { + message: 'handleCodeFlow', + }); + throw error; + } + }; + + handleOAuthLogin = async ( + loginHandler: BaseLoginHandler, + ): Promise => { + const web3AuthNetwork = this.config.web3AuthNetwork; + + if (this.localState.loginInProgress) { + throw new OAuthError( + 'Login already in progress', + OAuthErrorType.LoginInProgress, + ); + } + this.#dispatchLogin(); + + try { + const result = await loginHandler.login(); + const authConnection = loginHandler.authConnection; + + Logger.log('handleOAuthLogin: result', result); + if (result) { + const data = await loginHandler.getAuthTokens( + { ...result, web3AuthNetwork }, + this.config.authServerUrl, + ); + const audience = 'metamask'; + + if (!data.jwt_tokens[audience]) { + throw new OAuthError('No token found', OAuthErrorType.LoginError); + } + + const jwtPayload = JSON.parse( + loginHandler.decodeIdToken(data.jwt_tokens[audience]), + ) as Partial; + const userId = jwtPayload.sub ?? ''; + const accountName = jwtPayload.email ?? ''; + + this.updateLocalState({ + userId, + accountName, + }); + const handleCodeFlowResult = await this.handleSeedlessAuthenticate( + data, + authConnection, + ); + this.#dispatchPostLogin(handleCodeFlowResult); + return handleCodeFlowResult; + } + throw new OAuthError('No result', OAuthErrorType.LoginError); + } catch (error) { + Logger.log(error as Error, { + message: 'handleOAuthLogin', + }); + this.#dispatchPostLogin({ + type: OAuthLoginResultType.ERROR, + existingUser: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + if (error instanceof OAuthError) { + throw error; + } + throw new OAuthError( + error instanceof Error ? error : 'Unknown error', + OAuthErrorType.LoginError, + ); + } + }; + + updateLocalState = (newState: Partial) => { + this.localState = { + ...this.localState, + ...newState, + }; + }; + + getAuthDetails = () => ({ + authConnectionId: this.config.authConnectionId, + groupedAuthConnectionId: this.config.groupedAuthConnectionId, + userId: this.localState.userId, + }); + + clearAuthDetails = () => { + this.updateLocalState({ + userId: undefined, + accountName: undefined, + }); + }; + + resetOauthState = () => { + this.updateLocalState({ + loginInProgress: false, + oauthLoginSuccess: false, + oauthLoginError: null, + }); + }; +} + +export default new OAuthService({ + web3AuthNetwork: currentWeb3AuthNetwork as Web3AuthNetwork, + authConnectionId: AuthConnectionId, + groupedAuthConnectionId: GroupedAuthConnectionId, + authServerUrl: AuthServerUrl, +}); diff --git a/app/core/OAuthService/error.ts b/app/core/OAuthService/error.ts new file mode 100644 index 000000000000..8d117e80222c --- /dev/null +++ b/app/core/OAuthService/error.ts @@ -0,0 +1,37 @@ +export enum OAuthErrorType { + UnknownError = 10001, + UserCancelled = 10002, + UserDismissed = 10003, + LoginError = 10004, + InvalidProvider = 10005, + UnsupportedPlatform = 10006, + LoginInProgress = 10007, + AuthServerError = 10008, +} + +export const OAuthErrorMessages: Record = { + [OAuthErrorType.UnknownError]: 'Unknown error', + [OAuthErrorType.UserCancelled]: 'User cancelled', + [OAuthErrorType.UserDismissed]: 'User dismissed', + [OAuthErrorType.LoginError]: 'Login error', + [OAuthErrorType.InvalidProvider]: 'Invalid provider', + [OAuthErrorType.UnsupportedPlatform]: 'Unsupported platform', + [OAuthErrorType.LoginInProgress]: 'Login in progress', + [OAuthErrorType.AuthServerError]: 'Auth server error', +} as const; + +export class OAuthError extends Error { + public readonly code: OAuthErrorType; + + constructor(errMessage: string | Error, code: OAuthErrorType) { + if (errMessage instanceof Error) { + super(errMessage.message); + this.stack = errMessage.stack; + this.name = errMessage.name; + } else { + super(errMessage); + } + this.message = `${OAuthErrorMessages[code]} - ${errMessage}`; + this.code = code; + } +} diff --git a/app/store/migrations/079.test.ts b/app/store/migrations/079.test.ts new file mode 100644 index 000000000000..a17399653480 --- /dev/null +++ b/app/store/migrations/079.test.ts @@ -0,0 +1,115 @@ +import { captureException } from '@sentry/react-native'; +import { cloneDeep } from 'lodash'; + +import { ensureValidState } from './util'; +import migrate from './079'; + +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); + +jest.mock('./util', () => ({ + ensureValidState: jest.fn(), +})); + +const mockedCaptureException = jest.mocked(captureException); +const mockedEnsureValidState = jest.mocked(ensureValidState); + +const createTestState = () => ({ + engine: { + backgroundState: { + SeedlessOnboardingController: { + authConnection: 'apple', + userId: '123', + socialBackupsMetadata: [], + objectState: { + childState: [], + }, + }, + }, + }, +}); + +describe('Migration 77: Add Seedless Onboarding default state', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('returns state unchanged if ensureValidState fails', () => { + const state = { some: 'state' }; + mockedEnsureValidState.mockReturnValue(false); + + const migratedState = migrate(state); + + expect(migratedState).toStrictEqual({ some: 'state' }); + expect(mockedCaptureException).not.toHaveBeenCalled(); + }); + + it('adds Seedless Onboarding default state to state', () => { + const oldState = createTestState(); + mockedEnsureValidState.mockReturnValue(true); + + const expectedData = { + engine: { + backgroundState: { + SeedlessOnboardingController: { + ...oldState.engine.backgroundState.SeedlessOnboardingController, + }, + }, + }, + }; + + const migratedState = migrate(oldState); + + expect(migratedState).toStrictEqual(expectedData); + expect(mockedCaptureException).not.toHaveBeenCalled(); + }); + + it.each([ + { + state: { + engine: {}, + }, + test: 'empty engine state', + }, + { + state: { + engine: { + backgroundState: {}, + }, + }, + test: 'empty backgroundState', + }, + { + state: { + engine: { + backgroundState: { + SeedlessOnboardingController: 'invalid', + }, + }, + }, + test: 'invalid SeedlessOnboardingController state', + }, + { + state: { + engine: { + backgroundState: { + SeedlessOnboardingController: { + socialBackupsMetadata: [], + }, + }, + }, + }, + test: 'Array [] socialBackupsMetadata state', + }, + ])('does not modify state if the state is invalid - $test', ({ state }) => { + const orgState = cloneDeep(state); + mockedEnsureValidState.mockReturnValue(true); + + const migratedState = migrate(state); + + // State should be unchanged + expect(migratedState).toStrictEqual(orgState); + expect(mockedCaptureException).not.toHaveBeenCalled(); + }); +}); diff --git a/app/store/migrations/079.ts b/app/store/migrations/079.ts new file mode 100644 index 000000000000..62179281ec55 --- /dev/null +++ b/app/store/migrations/079.ts @@ -0,0 +1,32 @@ +import { captureException } from '@sentry/react-native'; + +import { ensureValidState } from './util'; + +/** + * Migration 77: Add 'Seedless Onboarding default state' to seedless onboarding controller + * + * This migration add Seedless Onboarding default state to the seedless onboarding controller + * as a default Seedless Onboarding State. + */ +const migration = (state: unknown): unknown => { + const migrationVersion = 79; + + // Ensure the state is valid for migration + if (!ensureValidState(state, migrationVersion)) { + return state; + } + + try { + // no state migration needed as new controller is introduced with default values + return state; + } catch (error) { + captureException( + new Error( + `Migration 077: Adding Seedless Onboarding default state failed with error: ${error}`, + ), + ); + return state; + } +}; + +export default migration; diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index 916d7bac84c8..463144b1d711 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -78,6 +78,7 @@ import migration75 from './075'; import migration76 from './076'; import migration77 from './077'; import migration78 from './078'; +import migration79 from './079'; // Add migrations above this line import { validatePostMigrationState } from '../validateMigration/validateMigration'; import { RootState } from '../../reducers'; @@ -171,6 +172,7 @@ export const migrationList: MigrationsList = { 76: migration76, 77: migration77, 78: migration78, + 79: migration79, }; // Enable both synchronous and asynchronous migrations diff --git a/app/util/logs/__snapshots__/index.test.ts.snap b/app/util/logs/__snapshots__/index.test.ts.snap index eca1f2111a75..e8acda7fc98f 100644 --- a/app/util/logs/__snapshots__/index.test.ts.snap +++ b/app/util/logs/__snapshots__/index.test.ts.snap @@ -388,6 +388,9 @@ exports[`logs :: generateStateLogs generates a valid json export 1`] = ` "cacheTimestamp": 0, "remoteFeatureFlags": {}, }, + "SeedlessOnboardingController": { + "socialBackupsMetadata": [], + }, "SelectedNetworkController": { "domains": {}, }, diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json index 5038839916b2..bc55e08dd1e3 100644 --- a/app/util/test/initial-background-state.json +++ b/app/util/test/initial-background-state.json @@ -91,9 +91,7 @@ ] }, "0x279f": { - "blockExplorerUrls": [ - "https://testnet.monadexplorer.com" - ], + "blockExplorerUrls": ["https://testnet.monadexplorer.com"], "chainId": "0x279f", "defaultBlockExplorerUrlIndex": 0, "defaultRpcEndpointIndex": 0, @@ -345,6 +343,9 @@ "cacheTimestamp": 0, "remoteFeatureFlags": {} }, + "SeedlessOnboardingController": { + "socialBackupsMetadata": [] + }, "SelectedNetworkController": { "domains": {} }, diff --git a/babel.config.js b/babel.config.js index 53b651bd9c15..0695ade2253c 100644 --- a/babel.config.js +++ b/babel.config.js @@ -48,6 +48,10 @@ module.exports = { test: './app/core/NavigationService/NavigationService.ts', plugins: [['@babel/plugin-transform-private-methods', { loose: true }]], }, + { + test: './app/core/OAuthService/OAuthLoginHandlers', + plugins: [['@babel/plugin-transform-private-methods', { loose: true }]], + }, ], env: { production: { diff --git a/ios/MetaMask/Info.plist b/ios/MetaMask/Info.plist index 676cf5057d8e..74e0b421e993 100644 --- a/ios/MetaMask/Info.plist +++ b/ios/MetaMask/Info.plist @@ -2,8 +2,6 @@ - LSMinimumSystemVersion - 12.0.0 CFBundleDevelopmentRegion en CFBundleDisplayName @@ -44,6 +42,8 @@ twitter itms-apps + LSMinimumSystemVersion + 12.0.0 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/ios/MetaMask/MetaMask.entitlements b/ios/MetaMask/MetaMask.entitlements index f29c11d5d6c1..dec440b1ed51 100644 --- a/ios/MetaMask/MetaMask.entitlements +++ b/ios/MetaMask/MetaMask.entitlements @@ -4,6 +4,10 @@ aps-environment development + com.apple.developer.applesignin + + Default + com.apple.developer.associated-domains applinks:metamask.io diff --git a/ios/MetaMask/MetaMaskDebug.entitlements b/ios/MetaMask/MetaMaskDebug.entitlements index 0ee0d28c177e..ceb0098df9d9 100644 --- a/ios/MetaMask/MetaMaskDebug.entitlements +++ b/ios/MetaMask/MetaMaskDebug.entitlements @@ -4,6 +4,10 @@ aps-environment development + com.apple.developer.applesignin + + Default + com.apple.developer.associated-domains applinks:metamask.io diff --git a/ios/Podfile.lock b/ios/Podfile.lock index eec360e86ae5..dd28d2977094 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -7,6 +7,8 @@ PODS: - React-Core - CocoaAsyncSocket (7.6.5) - DoubleConversion (1.1.6) + - EXApplication (6.0.2): + - ExpoModulesCore - EXConstants (17.0.8): - ExpoModulesCore - EXJSONUtils (0.14.0) @@ -221,14 +223,20 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - ExpoAppleAuthentication (7.1.3): + - ExpoModulesCore - ExpoAsset (11.0.5): - ExpoModulesCore + - ExpoCrypto (14.0.2): + - ExpoModulesCore - ExpoFileSystem (18.0.12): - ExpoModulesCore - ExpoFont (13.0.4): - ExpoModulesCore - ExpoKeepAwake (14.0.3): - ExpoModulesCore + - ExpoLinking (7.0.5): + - ExpoModulesCore - ExpoModulesCore (2.1.4): - DoubleConversion - glog @@ -252,6 +260,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - ExpoWebBrowser (14.0.2): + - ExpoModulesCore - EXUpdatesInterface (1.0.0): - ExpoModulesCore - fast_float (6.1.4) @@ -285,6 +295,27 @@ PODS: - nanopb (< 2.30911.0, >= 2.30908.0) - fmt (11.0.2) - glog (0.3.5) + - GoogleAcm (0.1.0): + - DoubleConversion + - glog + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30911.0, >= 2.30908.0) @@ -2347,6 +2378,7 @@ DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - BVLinearGradient (from `../node_modules/react-native-linear-gradient`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - EXApplication (from `../node_modules/expo-application/ios`) - EXConstants (from `../node_modules/expo-constants/ios`) - EXJSONUtils (from `../node_modules/expo-json-utils/ios`) - EXManifests (from `../node_modules/expo-manifests/ios`) @@ -2355,17 +2387,22 @@ DEPENDENCIES: - expo-dev-launcher (from `../node_modules/expo-dev-launcher`) - expo-dev-menu (from `../node_modules/expo-dev-menu`) - expo-dev-menu-interface (from `../node_modules/expo-dev-menu-interface/ios`) + - ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`) - ExpoAsset (from `../node_modules/expo-asset/ios`) + - ExpoCrypto (from `../node_modules/expo-crypto/ios`) - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) - ExpoFont (from `../node_modules/expo-font/ios`) - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) + - ExpoLinking (from `../node_modules/expo-linking/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) + - ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`) - EXUpdatesInterface (from `../node_modules/expo-updates-interface/ios`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - FirebaseCore - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - GoogleAcm (from `../node_modules/react-native-google-acm`) - "GoogleUtilities/NSData+zlib" - GzipSwift - lottie-ios (from `../node_modules/lottie-ios`) @@ -2521,6 +2558,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-linear-gradient" DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + EXApplication: + :path: "../node_modules/expo-application/ios" EXConstants: :path: "../node_modules/expo-constants/ios" EXJSONUtils: @@ -2537,16 +2576,24 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-dev-menu" expo-dev-menu-interface: :path: "../node_modules/expo-dev-menu-interface/ios" + ExpoAppleAuthentication: + :path: "../node_modules/expo-apple-authentication/ios" ExpoAsset: :path: "../node_modules/expo-asset/ios" + ExpoCrypto: + :path: "../node_modules/expo-crypto/ios" ExpoFileSystem: :path: "../node_modules/expo-file-system/ios" ExpoFont: :path: "../node_modules/expo-font/ios" ExpoKeepAwake: :path: "../node_modules/expo-keep-awake/ios" + ExpoLinking: + :path: "../node_modules/expo-linking/ios" ExpoModulesCore: :path: "../node_modules/expo-modules-core" + ExpoWebBrowser: + :path: "../node_modules/expo-web-browser/ios" EXUpdatesInterface: :path: "../node_modules/expo-updates-interface/ios" fast_float: @@ -2557,6 +2604,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec" glog: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + GoogleAcm: + :path: "../node_modules/react-native-google-acm" lottie-ios: :path: "../node_modules/lottie-ios" lottie-react-native: @@ -2800,6 +2849,7 @@ SPEC CHECKSUMS: BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 + EXApplication: 4c72f6017a14a65e338c5e74fca418f35141e819 EXConstants: fcfc75800824ac2d5c592b5bc74130bad17b146b EXJSONUtils: 01fc7492b66c234e395dcffdd5f53439c5c29c93 EXManifests: a19d50504b8826546a4782770317bc83fffec87d @@ -2808,11 +2858,15 @@ SPEC CHECKSUMS: expo-dev-launcher: 7fce0956aaa7f44742edf09f9d1420a4906a54c7 expo-dev-menu: db64396698d88d0b65490467801eb8299c3f04b4 expo-dev-menu-interface: 00dc42302a72722fdecec3fa048de84a9133bcc4 + ExpoAppleAuthentication: 52631ed9dcb71c65712a447bbb9a5667bb8fcf0c ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516 + ExpoCrypto: e97e864c8d7b9ce4a000bca45dddb93544a1b2b4 ExpoFileSystem: 42d363d3b96f9afab980dcef60d5657a4443c655 ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188 ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680 + ExpoLinking: 8d12bee174ba0cdf31239706578e29e74a417402 ExpoModulesCore: 77c3db9f8bd0f04af39ab8d43e3a75c23d708e2a + ExpoWebBrowser: a212e6b480d8857d3e441fba51e0c968333803b3 EXUpdatesInterface: 7c977640bdd8b85833c19e3959ba46145c5719db fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 @@ -2824,6 +2878,7 @@ SPEC CHECKSUMS: FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366 fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6 glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a + GoogleAcm: b6140c6bbab70222b1c14fbdadaebc378747379d GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GZIP: 3c0abf794bfce8c7cb34ea05a1837752416c8868 diff --git a/jest.config.js b/jest.config.js index 572d445061e5..48bfe9b0a6ad 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,6 +12,22 @@ process.env.SECURITY_ALERTS_API_URL = 'https://example.com'; process.env.LAUNCH_DARKLY_URL = 'https://client-config.dev-api.cx.metamask.io/v1'; +process.env.WEB3AUTH_NETWORK = 'sapphire_devnet'; +process.env.AUTH_SERVER_URL = 'https://api-develop-torus-byoa.web3auth.io'; + +process.env.IOS_GOOGLE_CLIENT_ID = + '882363291751-nbbp9n0o307cfil1lup766g1s99k0932.apps.googleusercontent.com'; +process.env.IOS_GOOGLE_REDIRECT_URI = + 'com.googleusercontent.apps.882363291751-nbbp9n0o307cfil1lup766g1s99k0932:/oauth2redirect/google'; +process.env.IOS_APPLE_CLIENT_ID = 'io.metamask.MetaMask'; + +process.env.ANDROID_WEB_GOOGLE_CLIENT_ID = + '882363291751-2a37cchrq9oc1lfj1p419otvahnbhguv.apps.googleusercontent.com'; +process.env.ANDROID_WEB_APPLE_CLIENT_ID = 'com.web3auth.appleloginextension'; + +process.env.AUTH_CONNECTION_ID = 'byoa-server'; +process.env.GROUPED_AUTH_CONNECTION_ID = 'mm-seedless-onboarding'; + process.env.MM_SMART_ACCOUNT_UI_ENABLED = 'true'; const config = { diff --git a/metro.transform.js b/metro.transform.js index 4d755ddff7b2..14417b857c33 100644 --- a/metro.transform.js +++ b/metro.transform.js @@ -21,6 +21,7 @@ const availableFeatures = new Set([ 'multi-srp', 'bitcoin', 'solana', + 'seedless-onboarding', ]); const mainFeatureSet = new Set(['preinstalled-snaps', 'multi-srp']); @@ -39,6 +40,7 @@ const flaskFeatureSet = new Set([ 'multi-srp', 'bitcoin', 'solana', + 'seedless-onboarding', ]); /** diff --git a/package.json b/package.json index 91b727d07845..267adde28038 100644 --- a/package.json +++ b/package.json @@ -216,6 +216,7 @@ "@metamask/rpc-errors": "^7.0.2", "@metamask/scure-bip39": "^2.1.0", "@metamask/sdk-communication-layer": "0.29.0-wallet", + "@metamask/seedless-onboarding-controller": "https://github.com/Web3Auth/core/raw/ed1203793629afef06ae6ff1830fcfb7dafb53a3/packages/seedless-onboarding-controller/seedless-onboarding-controller.tgz", "@metamask/selected-network-controller": "^22.0.0", "@metamask/signature-controller": "^28.0.0", "@metamask/slip44": "^4.1.0", @@ -302,6 +303,8 @@ "eventemitter2": "^6.4.9", "events": "3.0.0", "expo": "52.0.27", + "expo-apple-authentication": "~7.1.3", + "expo-auth-session": "~6.0.3", "expo-build-properties": "~0.13.2", "expo-dev-client": "~5.0.18", "fuse.js": "3.4.4", @@ -351,6 +354,7 @@ "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^1.10.3", "react-native-get-random-values": "^1.8.0", + "react-native-google-acm": "git+https://github.com/Web3Auth/react-native-google-acm.git#3ad58f4c11273ba102ede93d2a3148e45c84d248", "react-native-gzip": "^1.1.0", "react-native-i18n": "2.0.15", "react-native-in-app-review": "^4.3.3", diff --git a/yarn.lock b/yarn.lock index 507b7ca87af0..c7a4b65a3486 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4721,6 +4721,21 @@ single-call-balance-checker-abi "^1.0.0" uuid "^8.3.2" +"@metamask/auth-network-utils@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@metamask/auth-network-utils/-/auth-network-utils-0.1.0.tgz#8520c4dd69b758030975dfb6802f4ee921033c64" + integrity sha512-eSxnbDxu+s/LCoPt/d9bSDoHfb3hWazT3xPnOxn9aeWov6L4vnjew37zUlKHDxfYzxw7oEeEsdZEHzNo5C8enQ== + dependencies: + "@noble/curves" "^1.8.1" + "@noble/hashes" "^1.7.1" + "@toruslabs/bs58" "^1.0.0" + "@toruslabs/constants" "^15.0.0" + "@toruslabs/eccrypto" "^6.1.0" + bn.js "^5.2.1" + elliptic "^6.6.1" + json-stable-stringify "^1.2.1" + loglevel "^1.9.2" + "@metamask/base-controller@^7.0.1", "@metamask/base-controller@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@metamask/base-controller/-/base-controller-7.1.1.tgz#837216ee099563b2106202fa0ed376dc909dfbb9" @@ -5639,6 +5654,16 @@ utf-8-validate "^5.0.2" uuid "^8.3.2" +"@metamask/seedless-onboarding-controller@https://github.com/Web3Auth/core/raw/ed1203793629afef06ae6ff1830fcfb7dafb53a3/packages/seedless-onboarding-controller/seedless-onboarding-controller.tgz": + version "0.0.0" + resolved "https://github.com/Web3Auth/core/raw/ed1203793629afef06ae6ff1830fcfb7dafb53a3/packages/seedless-onboarding-controller/seedless-onboarding-controller.tgz#1a8347fd5b5b6207f64f5b1e36fbbee17cc849fa" + dependencies: + "@metamask/auth-network-utils" "^0.1.0" + "@metamask/base-controller" "^8.0.1" + "@metamask/toprf-secure-backup" "^0.1.0" + "@metamask/utils" "^11.2.0" + async-mutex "^0.5.0" + "@metamask/selected-network-controller@^22.0.0": version "22.0.0" resolved "https://registry.yarnpkg.com/@metamask/selected-network-controller/-/selected-network-controller-22.0.0.tgz#6b6490e2246cf07bad9400638acea28330d2bbf9" @@ -5850,6 +5875,22 @@ "@metamask/base-controller" "^8.0.0" "@metamask/utils" "^11.2.0" +"@metamask/toprf-secure-backup@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@metamask/toprf-secure-backup/-/toprf-secure-backup-0.1.0.tgz#e06f34efb3497e43600131f63ca20ed7067f3fbb" + integrity sha512-vcbvVgT/FY08judGDbQAepPp4wgSeCYcUOyljKgvrk2bRrO4n10tkg/Vke7dJSMT8LLtWR641yY2A14bPM3KQA== + dependencies: + "@metamask/auth-network-utils" "^0.1.0" + "@noble/ciphers" "^1.2.1" + "@noble/curves" "^1.8.1" + "@noble/hashes" "^1.7.1" + "@sentry/core" "^9.10.0" + "@toruslabs/constants" "^15.0.0" + "@toruslabs/eccrypto" "^6.1.0" + "@toruslabs/fetch-node-details" "^15.0.0" + "@toruslabs/http-helpers" "^8.1.1" + bn.js "^5.2.1" + "@metamask/transaction-controller@55.0.0": version "55.0.0" resolved "https://registry.yarnpkg.com/@metamask/transaction-controller/-/transaction-controller-55.0.0.tgz#f464a930ad0ad0cb33aa5371f9b5a9c2729c1c87" @@ -5995,7 +6036,7 @@ dependencies: eslint-scope "5.1.1" -"@noble/ciphers@1.2.1": +"@noble/ciphers@1.2.1", "@noble/ciphers@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.2.1.tgz#3812b72c057a28b44ff0ad4aff5ca846e5b9cdc9" integrity sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA== @@ -7745,6 +7786,11 @@ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.54.0.tgz#a2ebec965cadcb6de89e116689feeef79d5862a6" integrity sha512-03bWf+D1j28unOocY/5FDB6bUHtYlm6m6ollVejhg45ZmK9iPjdtxNWbrLsjT1WRym0Tjzowu+A3p+eebYEv0Q== +"@sentry/core@^9.10.0": + version "9.15.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-9.15.0.tgz#590f16a15596ce01db49d9d80b31cb18048ca9a4" + integrity sha512-lBmo3bzzaYUesdzc2H5K3fajfXyUNuj5koqyFoCAI8rnt9CBl7SUc/P07+E5eipF8mxgiU3QtkI7ALzRQN8pqQ== + "@sentry/react-native@~6.10.0": version "6.10.0" resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-6.10.0.tgz#9efafb9b85870bd4c5189763edde30709b9f3213" @@ -9628,6 +9674,48 @@ resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== +"@toruslabs/bs58@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/bs58/-/bs58-1.0.0.tgz#a5a9621caba9408521d7f17949b28f9af48f4f23" + integrity sha512-osqIgm1MzEB6+fkaQeEUg4tuZXmhhXTn+K7+nZU7xDBcy+8Yr3eGNqJcQ4jds82g+dhkk2cBkge9sffv38iDQQ== + +"@toruslabs/constants@^15.0.0": + version "15.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/constants/-/constants-15.0.0.tgz#1de2c1eb578e7ed1b5c1d68d33861b2f591c7a2d" + integrity sha512-0nr6vU3FQT2eRsnPYRwfVIkjwawLTkDTkCusiny9DgAw1M2zbmNqPOAqWuljUcDaK/u4VB0/PrN5SzhVBLnNGg== + +"@toruslabs/eccrypto@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-6.1.0.tgz#1d640c0375d529dc59cbca2d57de0b8b23e3066f" + integrity sha512-9eC8UJ+XsztgJTcFbhiXIDl9bFXCY1TOKMCnfH2T+RVtpLOKQ0OWCRlFKqeMZ3ffz3gbrUwXZaE5BJ/MW4Hnkw== + dependencies: + elliptic "^6.6.1" + +"@toruslabs/fetch-node-details@^15.0.0": + version "15.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/fetch-node-details/-/fetch-node-details-15.0.0.tgz#e30fa9f6805b9756183ee9b653a14449b2e2fee9" + integrity sha512-uYTV+mv5U6egTQj94zSyy9NqlfUYdwen6GeyDdCA7g0TN+YnRV4WgkcjloN0llscrND7mzuJNSIVDHCE+kSpcQ== + dependencies: + "@toruslabs/constants" "^15.0.0" + "@toruslabs/fnd-base" "^15.0.0" + "@toruslabs/http-helpers" "^8.1.1" + loglevel "^1.9.2" + +"@toruslabs/fnd-base@^15.0.0": + version "15.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/fnd-base/-/fnd-base-15.0.0.tgz#6d6282ca6c540fefe277bf9f9fb20652949ba414" + integrity sha512-KygYyPBHADmXKmzClbGcKc0oOI/Ay5FP/fUsFvXXRv4ey+VfIKiHHSAo6IojozvVUSrs0kDwocSpeE3sp/hBPA== + dependencies: + "@toruslabs/constants" "^15.0.0" + +"@toruslabs/http-helpers@^8.1.1": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@toruslabs/http-helpers/-/http-helpers-8.1.1.tgz#49b19bd316e46c6a1d1d81d2bca8c9cdaf35e95e" + integrity sha512-bcymgOEAHjWJtqWvbCw+jCLk8vch5V53E/17moM0kAQO+tY0omhsMpZYDtFcbF3xl8/fLyW7G0HGUfThPykQ7A== + dependencies: + deepmerge "^4.3.1" + loglevel "^1.9.2" + "@tradle/react-native-http@2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@tradle/react-native-http/-/react-native-http-2.0.1.tgz#af19e240e1e580bfa249563924d1be472686f48b" @@ -13043,7 +13131,7 @@ base64-js@1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== -base64-js@^1.0.2, base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.0.2, base64-js@^1.2.3, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -13645,6 +13733,14 @@ caf@^15.0.1: resolved "https://registry.yarnpkg.com/caf/-/caf-15.0.1.tgz#28f1f17bd93dc4b5d95207ad07066eddf4768160" integrity sha512-Xp/IK6vMwujxWZXra7djdYzPdPnEQKa7Mudu2wZgDQ3TJry1I0TgtjEgwZHpoBcMp68j4fb0/FZ1SJyMEgJrXQ== +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -13656,6 +13752,24 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bin get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + call-me-maybe@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" @@ -15679,6 +15793,15 @@ dtrace-provider@~0.8: dependencies: nan "^2.14.0" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + duplexer2@^0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -16087,6 +16210,11 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -16139,6 +16267,13 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.0" +es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es-set-tostringtag@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" @@ -17066,6 +17201,16 @@ expect@^29.0.0, expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" +expo-apple-authentication@~7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/expo-apple-authentication/-/expo-apple-authentication-7.1.3.tgz#3d4ec9fa29ff336eba9b280e7db110639ae7e020" + integrity sha512-TRaF513oDGjGx3hRiAwkMiSnKLN8BIR9Se5Gi3ttz2UUgP9y+tNHV6Ji6/oztJo9ON7zerHg2mn5Y+3B8c2vTQ== + +expo-application@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/expo-application/-/expo-application-6.0.2.tgz#4384299f0518958e2fb18b8b029af5583c642479" + integrity sha512-qcj6kGq3mc7x5yIb5KxESurFTJCoEKwNEL34RdPEvTB/xhl7SeVZlu05sZBqxB1V4Ryzq/LsCb7NHNfBbb3L7A== + expo-asset@~11.0.2: version "11.0.5" resolved "https://registry.yarnpkg.com/expo-asset/-/expo-asset-11.0.5.tgz#9d0ad28da3af220d25c001cd6e4a80cc669ee18b" @@ -17076,6 +17221,18 @@ expo-asset@~11.0.2: invariant "^2.2.4" md5-file "^3.2.3" +expo-auth-session@~6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/expo-auth-session/-/expo-auth-session-6.0.3.tgz#2a31afca4b0c3654b02ca69c88e897266ff63863" + integrity sha512-s7LmmMPiiY1NXrlcXkc4+09Hlfw9X1CpaQOCDkwfQEodG1uCYGQi/WImTnDzw5YDkWI79uC8F1mB8EIerilkDA== + dependencies: + expo-application "~6.0.2" + expo-constants "~17.0.5" + expo-crypto "~14.0.2" + expo-linking "~7.0.5" + expo-web-browser "~14.0.2" + invariant "^2.2.4" + expo-build-properties@^0.13.1, expo-build-properties@~0.13.2: version "0.13.2" resolved "https://registry.yarnpkg.com/expo-build-properties/-/expo-build-properties-0.13.2.tgz#c9cef927fc8236551d940da4fd8dc1332e2d052d" @@ -17084,7 +17241,7 @@ expo-build-properties@^0.13.1, expo-build-properties@~0.13.2: ajv "^8.11.0" semver "^7.6.0" -expo-constants@~17.0.4, expo-constants@~17.0.8: +expo-constants@~17.0.4, expo-constants@~17.0.5, expo-constants@~17.0.8: version "17.0.8" resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-17.0.8.tgz#d7a21ec6f1f4834ea25aa645be20292ef99c0b81" integrity sha512-XfWRyQAf1yUNgWZ1TnE8pFBMqGmFP5Gb+SFSgszxDdOoheB/NI5D4p7q86kI2fvGyfTrxAe+D+74nZkfsGvUlg== @@ -17092,6 +17249,13 @@ expo-constants@~17.0.4, expo-constants@~17.0.8: "@expo/config" "~10.0.11" "@expo/env" "~0.4.2" +expo-crypto@~14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/expo-crypto/-/expo-crypto-14.0.2.tgz#5f5d83c849164229f7a3e6a341887142756d517e" + integrity sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ== + dependencies: + base64-js "^1.3.0" + expo-dev-client@~5.0.18: version "5.0.20" resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-5.0.20.tgz#349a6251d1d63c3142ad5232be653038b5c6cf15" @@ -17149,6 +17313,14 @@ expo-keep-awake@~14.0.2: resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz#74c91b68effdb6969bc1e8371621aad90386cfbf" integrity sha512-6Jh94G6NvTZfuLnm2vwIpKe3GdOiVBuISl7FI8GqN0/9UOg9E0WXXp5cDcfAG8bn80RfgLJS8P7EPUGTZyOvhg== +expo-linking@~7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-7.0.5.tgz#6f583c636a3cc29f02d67a1550b21f8e636fe2af" + integrity sha512-3KptlJtcYDPWohk0MfJU75MJFh2ybavbtcSd84zEPfw9s1q3hjimw3sXnH03ZxP54kiEWldvKmmnGcVffBDB1g== + dependencies: + expo-constants "~17.0.5" + invariant "^2.2.4" + expo-manifests@~0.15.8: version "0.15.8" resolved "https://registry.yarnpkg.com/expo-manifests/-/expo-manifests-0.15.8.tgz#15e7b7b99d764b40ca3e3f859a126c856e2d6206" @@ -17183,6 +17355,11 @@ expo-updates-interface@~1.0.0: resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-1.0.0.tgz#b98c66b800d29561c62409556948b2af3d5316e5" integrity sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ== +expo-web-browser@~14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-14.0.2.tgz#52d53947c42fdfb225e8c230418ffe508bcf98a7" + integrity sha512-Hncv2yojhTpHbP6SGWARBFdl7P6wBHc1O8IKaNsH0a/IEakq887o1eRhLxZ5IwztPQyRDhpqHdgJ+BjWolOnwA== + expo@52.0.27: version "52.0.27" resolved "https://registry.yarnpkg.com/expo/-/expo-52.0.27.tgz#9eeceda4990ee5a78a66d3f2c26122118ba9454c" @@ -18139,6 +18316,22 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-nonce@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" @@ -18179,6 +18372,14 @@ get-port@^6.1.2: resolved "https://registry.yarnpkg.com/get-port/-/get-port-6.1.2.tgz#c1228abb67ba0e17fb346da33b15187833b9c08a" integrity sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stdin@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" @@ -18488,6 +18689,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + got@^11.0.2, got@^11.8.1: version "11.8.5" resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" @@ -18634,6 +18840,11 @@ has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" @@ -20626,6 +20837,17 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json-stable-stringify@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz#addb683c2b78014d0b78d704c2fcbdf0695a60e2" + integrity sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -20671,6 +20893,11 @@ jsonfile@^6.0.1, jsonfile@^6.1.0: optionalDependencies: graceful-fs "^4.1.6" +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + jsonpath-plus@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz#7ad94e147b3ed42f7939c315d2b9ce490c5a3899" @@ -21395,6 +21622,11 @@ loglevel@^1.6.0, loglevel@^1.8.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7" integrity sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg== +loglevel@^1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08" + integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg== + long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -21571,6 +21803,11 @@ marky@^1.2.2: resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + md5-file@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-3.2.3.tgz#f9bceb941eca2214a4c0727f5e700314e770f06f" @@ -24668,6 +24905,10 @@ react-native-get-random-values@^1.8.0: dependencies: fast-base64-decode "^1.0.0" +"react-native-google-acm@git+https://github.com/Web3Auth/react-native-google-acm.git#3ad58f4c11273ba102ede93d2a3148e45c84d248": + version "0.1.0" + resolved "git+https://github.com/Web3Auth/react-native-google-acm.git#3ad58f4c11273ba102ede93d2a3148e45c84d248" + react-native-gzip@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/react-native-gzip/-/react-native-gzip-1.1.0.tgz#726396afddc348f60d5acc8bfaed89cc4066bacb" @@ -26277,7 +26518,7 @@ set-blocking@2.0.0, set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -set-function-length@^1.2.1: +set-function-length@^1.2.1, set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==