From 35f9cef165f63a5e1f656e258637b18a8161347e Mon Sep 17 00:00:00 2001 From: An Nguyen Date: Fri, 22 Nov 2024 13:17:13 -0600 Subject: [PATCH] More Seedless Reauth Improvement (#2113) --- .../app/seedless/store/listeners.test.ts | 42 ++++++++++++++++++- .../app/seedless/store/listeners.ts | 11 ++++- yarn.lock | 2 +- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/core-mobile/app/seedless/store/listeners.test.ts b/packages/core-mobile/app/seedless/store/listeners.test.ts index c1778ea4f..831af6517 100644 --- a/packages/core-mobile/app/seedless/store/listeners.test.ts +++ b/packages/core-mobile/app/seedless/store/listeners.test.ts @@ -1,7 +1,13 @@ import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit' import { noop } from 'lodash' import { AppStartListening } from 'store/middleware/listener' -import { onAppUnlocked, onLogOut, onRehydrationComplete } from 'store/app' +import { + onAppUnlocked, + onLogOut, + onRehydrationComplete, + selectWalletType +} from 'store/app' +import { WalletType } from 'services/wallet/types' import SeedlessService from 'seedless/services/SeedlessService' import { ErrorEvent, GlobalEvents } from '@cubist-labs/cubesigner-sdk' import * as Navigation from 'utils/Navigation' @@ -13,6 +19,15 @@ import { onTokenExpired, reducerName } from './slice' jest.mock('services/wallet/WalletService', () => ({ walletType: 'SEEDLESS' })) + +jest.mock('store/app', () => { + const actual = jest.requireActual('store/app') + return { + ...actual, + selectWalletType: jest.fn() + } +}) + jest.mock('seedless/services/SeedlessService', () => ({ sessionManager: { refreshToken: jest.fn() @@ -62,6 +77,12 @@ describe('seedless - listeners', () => { expect(SeedlessService.sessionManager.refreshToken).toHaveBeenCalled() }) it('should have dispatched onTokenExpired action', async () => { + const mockSelectWalletType = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + selectWalletType as jest.MockedFunction + mockSelectWalletType.mockImplementationOnce(() => { + return WalletType.SEEDLESS + }) store.dispatch(onRehydrationComplete()) GlobalEvents.triggerErrorEvent({ status: 403, @@ -72,7 +93,24 @@ describe('seedless - listeners', () => { params: expect.anything() }) }) - it('should have signed out', async () => { + it('should have not dispatched onTokenExpired action when wallet is not seedless', async () => { + const mockSelectWalletType = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + selectWalletType as jest.MockedFunction + mockSelectWalletType.mockImplementationOnce(() => { + return WalletType.MNEMONIC + }) + store.dispatch(onRehydrationComplete()) + GlobalEvents.triggerErrorEvent({ + status: 403, + isSessionExpiredError: () => true + } as ErrorEvent) + expect(mockNavigate).not.toHaveBeenCalledWith({ + name: 'Root.RefreshToken', + params: expect.anything() + }) + }) + it('should have signed out', async () => { store.dispatch(onLogOut()) expect(GoogleSigninService.signOut).toHaveBeenCalled() }) diff --git a/packages/core-mobile/app/seedless/store/listeners.ts b/packages/core-mobile/app/seedless/store/listeners.ts index 1fd3440da..439ef2d68 100644 --- a/packages/core-mobile/app/seedless/store/listeners.ts +++ b/packages/core-mobile/app/seedless/store/listeners.ts @@ -5,6 +5,7 @@ import { onLogOut, onRehydrationComplete, selectWalletState, + selectWalletType, WalletState } from 'store/app' import * as Navigation from 'utils/Navigation' @@ -45,7 +46,7 @@ const registerSeedlessErrorHandler = async ( _: Action, listenerApi: AppListenerEffectAPI ): Promise => { - const { dispatch } = listenerApi + const { dispatch, getState } = listenerApi const onErrorHandler = async (e: ErrResponse): Promise => { // log error @@ -54,14 +55,20 @@ const registerSeedlessErrorHandler = async ( // an example url https://prod.signer.cubist.dev/v1/org/Org%23db7f2cac-7bd0-4e5f-b7c2-b5881a4bb4e7/eth1/sign/0xD0E99cEa490Cdb54ba555922bf325952F0DE38bd Logger.error('seedless error', JSON.stringify({ ...e, url: '' })) + const walletType = selectWalletType(getState()) + // the following check is what cubist does internally for GlobalEvents.onSessionExpired // // if status is 403 and error matches one of the "invalid session" error codes // or when "signerSessionRefresh" fails (errors returned by the authorizer lambda are not forwarded to the client) + // or when e.errorCode is undefined, it means that the error came from the authorizer, which is currently the only place cubist cannot set errorCode // we will prompt user to re-authenticate if ( + walletType === WalletType.SEEDLESS && e.status === 403 && - (e.isSessionExpiredError() || e.operation === 'signerSessionRefresh') + (e.isSessionExpiredError() || + e.operation === 'signerSessionRefresh' || + e.errorCode === undefined) ) { dispatch(onTokenExpired) } diff --git a/yarn.lock b/yarn.lock index 244c0c1d5..d87e44531 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26357,7 +26357,7 @@ react-native-webview@ava-labs/react-native-webview: peerDependencies: react: "*" react-native: "*" - checksum: a187edd718e1ea3a6b1e5da167744e6ee324bc3c3e492bcb0a9d028ab68a82907f053f37c23aa4229d6a9091541cee3c73549c3c850056e4cf5eb5b3cb2c9ffc + checksum: d396f3dea807077e8789e1d463c87024e1633481d3dff53c0650c82a08d8b7d699db97ceab4e8d2c9de85c3d5378d192c968487254c62edadff77e82a9b8c929 languageName: node linkType: hard