From e0b2646fe7f3a96b80f2d5d93548dc3ccf68feaf Mon Sep 17 00:00:00 2001 From: Sukhrobbek Odilov Date: Mon, 14 Oct 2024 17:43:29 +0500 Subject: [PATCH] feat: recovery facto whitelabeling fix: solving deepsource fix: returning space fix: fixing tests and deep source fix: updating tests fix: correcting a mistake fix: updating tests fix: adding more tests fix: solving deepsource feat: adding a new type and test --- .../@magic-sdk/provider/src/modules/auth.ts | 2 +- .../@magic-sdk/provider/src/modules/user.ts | 24 +- .../spec/modules/user/showSettings.spec.ts | 263 +++++++++++++++++- .../types/src/modules/auth-types.ts | 2 +- .../types/src/modules/intermediary-types.ts | 4 + .../types/src/modules/user-types.ts | 28 ++ 6 files changed, 312 insertions(+), 11 deletions(-) diff --git a/packages/@magic-sdk/provider/src/modules/auth.ts b/packages/@magic-sdk/provider/src/modules/auth.ts index 759de35a7..dabefc7ab 100644 --- a/packages/@magic-sdk/provider/src/modules/auth.ts +++ b/packages/@magic-sdk/provider/src/modules/auth.ts @@ -174,7 +174,7 @@ export class AuthModule extends BaseModule { this.createIntermediaryEvent(RecencyCheckEventEmit.Cancel, requestPayload.id as any)(); }); handle.on(RecencyCheckEventEmit.VerifyEmailOtp, (otp: string) => { - this.createIntermediaryEvent(RecencyCheckEventEmit.VerifyEmailOtp, requestPayload.id as any)(otp); + this.createIntermediaryEvent(RecencyCheckEventEmit.VerifyEmailOtp, requestPayload.id as string)(otp); }); handle.on(RecencyCheckEventEmit.VerifyMFACode, (mfa: string) => { this.createIntermediaryEvent(RecencyCheckEventEmit.VerifyMFACode, requestPayload.id as string)(mfa); diff --git a/packages/@magic-sdk/provider/src/modules/user.ts b/packages/@magic-sdk/provider/src/modules/user.ts index fcc1e845c..85dc3c777 100644 --- a/packages/@magic-sdk/provider/src/modules/user.ts +++ b/packages/@magic-sdk/provider/src/modules/user.ts @@ -13,6 +13,9 @@ import { DisableMFAConfiguration, DisableMFAEventHandlers, DisableMFAEventEmit, + RecencyCheckEventEmit, + RecoveryFactorEventHandlers, + RecoveryFactorEventEmit, } from '@magic-sdk/types'; import { getItem, setItem, removeItem } from '../util/storage'; import { BaseModule } from './base-module'; @@ -110,11 +113,30 @@ export class UserModule extends BaseModule { } public showSettings(configuration?: ShowSettingsConfiguration) { + const { showUI = true } = configuration || {}; const requestPayload = createJsonRpcRequestPayload( this.sdk.testMode ? MagicPayloadMethod.UserSettingsTestMode : MagicPayloadMethod.UserSettings, [configuration], ); - return this.request(requestPayload); + const handle = this.request(requestPayload); + if (!showUI && handle) { + handle.on(RecoveryFactorEventEmit.SendNewPhoneNumber, (phone_number: string) => { + this.createIntermediaryEvent( + RecoveryFactorEventEmit.SendNewPhoneNumber, + requestPayload.id as string, + )(phone_number); + }); + handle.on(RecoveryFactorEventEmit.SendOtpCode, (otp: string) => { + this.createIntermediaryEvent(RecoveryFactorEventEmit.SendOtpCode, requestPayload.id as string)(otp); + }); + handle.on(RecoveryFactorEventEmit.StartEditPhoneNumber, () => { + this.createIntermediaryEvent(RecoveryFactorEventEmit.StartEditPhoneNumber, requestPayload.id as string)(); + }); + handle.on(RecencyCheckEventEmit.VerifyEmailOtp, (otp: string) => { + this.createIntermediaryEvent(RecencyCheckEventEmit.VerifyEmailOtp, requestPayload.id as string)(otp); + }); + } + return handle; } public recoverAccount(configuration: RecoverAccountConfiguration) { diff --git a/packages/@magic-sdk/provider/test/spec/modules/user/showSettings.spec.ts b/packages/@magic-sdk/provider/test/spec/modules/user/showSettings.spec.ts index 87f5e9b9b..fcdb8b05a 100644 --- a/packages/@magic-sdk/provider/test/spec/modules/user/showSettings.spec.ts +++ b/packages/@magic-sdk/provider/test/spec/modules/user/showSettings.spec.ts @@ -3,11 +3,21 @@ import { DeepLinkPage } from '@magic-sdk/types/src/core/deep-link-pages'; import { createMagicSDK, createMagicSDKTestMode } from '../../../factories'; import { isPromiEvent } from '../../../../src/util'; +jest.mock('@magic-sdk/types', () => ({ + ...jest.requireActual('@magic-sdk/types'), + RecoveryFactorEventEmit: { + SendNewPhoneNumber: 'send-new-phone-number', + SendOtpCode: 'send-otp-code', + StartEditPhoneNumber: 'start-edit-phone-number', + }, +})); + beforeEach(() => { browserEnv.restore(); + jest.restoreAllMocks(); }); -test('Generate JSON RPC request payload with method `magic_auth_settings`', async () => { +test('Generate JSON RPC request payload with method `magic_auth_settings`', () => { const magic = createMagicSDK(); magic.user.request = jest.fn(); @@ -18,7 +28,7 @@ test('Generate JSON RPC request payload with method `magic_auth_settings`', asyn expect(requestPayload.params).toEqual([undefined]); }); -test('If `testMode` is enabled, testing-specific RPC method is used', async () => { +test('If `testMode` is enabled, testing-specific RPC method is used', () => { const magic = createMagicSDKTestMode(); magic.user.request = jest.fn(); @@ -29,7 +39,7 @@ test('If `testMode` is enabled, testing-specific RPC method is used', async () = expect(requestPayload.params).toEqual([undefined]); }); -test('Generate JSON RPC request payload with method `magic_auth_settings` and page params `update-email`', async () => { +test('Generate JSON RPC request payload with method `magic_auth_settings` and page params `update-email`', () => { const magic = createMagicSDK(); magic.user.request = jest.fn(); @@ -40,28 +50,265 @@ test('Generate JSON RPC request payload with method `magic_auth_settings` and pa expect(requestPayload.params).toEqual([{ page: 'update-email' }]); }); -test('Generate JSON RPC request payload with method `magic_auth_settings` and page params `mfa`', async () => { +test('Generate JSON RPC request payload with method `magic_auth_settings` and page params `mfa`', () => { + const magic = createMagicSDK(); + magic.user.request = jest.fn(); + + magic.user.showSettings({ page: DeepLinkPage.MFA, showUI: false }); + + const requestPayload = magic.user.request.mock.calls[0][0]; + expect(requestPayload.method).toBe('magic_auth_settings'); + expect(requestPayload.params).toEqual([{ page: 'mfa', showUI: false }]); +}); + +test('Generate JSON RPC request payload with method `magic_auth_settings` and page params `recovery`', () => { + const magic = createMagicSDK(); + magic.user.request = jest.fn(); + + magic.user.showSettings({ page: DeepLinkPage.Recovery, showUI: false }); + + const requestPayload = magic.user.request.mock.calls[0][0]; + expect(requestPayload.method).toBe('magic_auth_settings'); + expect(requestPayload.params).toEqual([{ page: 'recovery', showUI: false }]); +}); + +test('Generate JSON RPC request payload with `showUI` undefined', () => { const magic = createMagicSDK(); magic.user.request = jest.fn(); magic.user.showSettings({ page: DeepLinkPage.MFA }); + const requestPayload = magic.user.request.mock.calls[0][0]; + expect(requestPayload.params).toEqual([{ page: 'mfa', showUI: undefined }]); +}); + +test('Generate JSON RPC request payload with method `magic_auth_settings` and page params `recovery` and showUI true', () => { + const magic = createMagicSDK(); + magic.user.request = jest.fn(); + + magic.user.showSettings({ page: DeepLinkPage.Recovery, showUI: true }); + const requestPayload = magic.user.request.mock.calls[0][0]; expect(requestPayload.method).toBe('magic_auth_settings'); - expect(requestPayload.params).toEqual([{ page: 'mfa' }]); + expect(requestPayload.params).toEqual([{ page: 'recovery', showUI: true }]); }); -test('Generate JSON RPC request payload with method `magic_auth_settings` and page params `recovery`', async () => { +test('Generate JSON RPC request payload without `page` parameter', () => { const magic = createMagicSDK(); magic.user.request = jest.fn(); - magic.user.showSettings({ page: DeepLinkPage.Recovery }); + magic.user.showSettings(); const requestPayload = magic.user.request.mock.calls[0][0]; expect(requestPayload.method).toBe('magic_auth_settings'); - expect(requestPayload.params).toEqual([{ page: 'recovery' }]); + expect(requestPayload.params).toEqual([undefined]); }); +test('ShowSettings should attach event listeners for recovery factor events', () => { + const magic = createMagicSDK(); + + const mockOn = jest.fn(); + const mockHandle = { + on: mockOn, + }; + + magic.user.request = jest.fn().mockReturnValue(mockHandle); + + const config = { page: DeepLinkPage.Recovery, showUI: false }; + magic.user.showSettings(config); + + expect(mockOn).toHaveBeenCalledWith('send-new-phone-number', expect.any(Function)); + expect(mockOn).toHaveBeenCalledWith('send-otp-code', expect.any(Function)); + + const phoneNumber = '123-456-7890'; + const otp = '123456'; + + const createIntermediaryEventFn = jest.fn(); + magic.user.createIntermediaryEvent = jest.fn(() => createIntermediaryEventFn); + + mockOn.mock.calls[0][1](phoneNumber); + mockOn.mock.calls[1][1](otp); + + expect(createIntermediaryEventFn).toHaveBeenCalledWith(phoneNumber); + expect(createIntermediaryEventFn).toHaveBeenCalledWith(otp); +}); +test('ShowSettings should call createIntermediaryEvent with StartEditPhoneNumber', () => { + const magic = createMagicSDK(); + const mockOn = jest.fn(); + const mockHandle = { on: mockOn }; + + magic.user.request = jest.fn().mockReturnValue(mockHandle); + + const config = { page: DeepLinkPage.Recovery, showUI: false }; + magic.user.showSettings(config); + + const createIntermediaryEventFn = jest.fn(); + magic.user.createIntermediaryEvent = jest.fn(() => createIntermediaryEventFn); + + const startEditPhoneNumberListener = mockOn.mock.calls.find((call) => call[0] === 'start-edit-phone-number')[1]; + + startEditPhoneNumberListener(); + + expect(createIntermediaryEventFn).toHaveBeenCalled(); +}); + +test('ShowSettings should attach event listener for StartEditPhoneNumber event', () => { + const magic = createMagicSDK(); + const mockOn = jest.fn(); + const mockHandle = { on: mockOn }; + + magic.user.request = jest.fn().mockReturnValue(mockHandle); + + const config = { page: DeepLinkPage.Recovery, showUI: false }; + magic.user.showSettings(config); + + expect(mockOn).toHaveBeenCalledWith('Recency/auth-factor-verify-email-otp', expect.any(Function)); + + expect(mockOn).toHaveBeenCalledWith('start-edit-phone-number', expect.any(Function)); +}); +test('ShowSettings should not call createIntermediaryEvent if StartEditPhoneNumber event is not triggered', () => { + const magic = createMagicSDK(); + const mockOn = jest.fn(); + const mockHandle = { on: mockOn }; + + magic.user.request = jest.fn().mockReturnValue(mockHandle); + + const config = { page: DeepLinkPage.Recovery, showUI: false }; + magic.user.showSettings(config); + + const createIntermediaryEventFn = jest.fn(); + magic.user.createIntermediaryEvent = jest.fn(() => createIntermediaryEventFn); + + expect(magic.user.createIntermediaryEvent).not.toHaveBeenCalled(); + expect(createIntermediaryEventFn).not.toHaveBeenCalled(); +}); +test('ShowSettings should handle missing config gracefully', () => { + const magic = createMagicSDK(); + const mockOn = jest.fn(); + const mockHandle = { on: mockOn }; + + magic.user.request = jest.fn().mockReturnValue(mockHandle); + + magic.user.showSettings(undefined); + + expect(mockOn).not.toHaveBeenCalled(); +}); + +test('Generate JSON RPC request payload with invalid page param', () => { + const magic = createMagicSDK(); + magic.user.request = jest.fn(); + + magic.user.showSettings({ page: 'invalid-page' }); + + const requestPayload = magic.user.request.mock.calls[0][0]; + expect(requestPayload.params).toEqual([{ page: 'invalid-page' }]); +}); +test('method should not return a PromiEvent if called with invalid options', () => { + const magic = createMagicSDK(); + const result = magic.user.showSettings({ invalidOption: true }); + expect(isPromiEvent(result)).toBeTruthy(); +}); +test('ShowSettings should not attach event listeners when `showUI` is true', () => { + const magic = createMagicSDK(); + const mockHandle = { on: jest.fn() }; + magic.user.request = jest.fn().mockReturnValue(mockHandle); + + const config = { page: DeepLinkPage.Recovery, showUI: true }; + magic.user.showSettings(config); + + expect(mockHandle.on).not.toHaveBeenCalled(); +}); +test('ShowSettings should handle no configuration passed gracefully', () => { + const magic = createMagicSDK(); + magic.user.request = jest.fn(); + + magic.user.showSettings(); + + const requestPayload = magic.user.request.mock.calls[0][0]; + expect(requestPayload.method).toBe('magic_auth_settings'); + expect(requestPayload.params).toEqual([undefined]); +}); +test('ShowSettings should return a PromiEvent for valid config', () => { + const magic = createMagicSDK(); + const result = magic.user.showSettings({ page: DeepLinkPage.Recovery }); + + expect(isPromiEvent(result)).toBeTruthy(); +}); + +test('ShowSettings should not return a PromiEvent for invalid config', () => { + const magic = createMagicSDK(); + const result = magic.user.showSettings({ invalidOption: true }); + + expect(isPromiEvent(result)).toBeTruthy(); +}); + +test('ShowSettings called multiple times should maintain state correctly', () => { + const magic = createMagicSDK(); + const createIntermediaryEventFn = jest.fn(); + magic.user.createIntermediaryEvent = jest.fn(() => createIntermediaryEventFn); + + const handle1 = magic.user.showSettings({ page: DeepLinkPage.Recovery, showUI: false }); + handle1.emit('send-otp-code', '123456'); + + const handle2 = magic.user.showSettings({ page: DeepLinkPage.MFA, showUI: false }); + handle2.emit('send-new-phone-number', '987654'); + + expect(createIntermediaryEventFn).toHaveBeenCalledWith('123456'); + expect(createIntermediaryEventFn).toHaveBeenCalledWith('987654'); +}); +test('ShowSettings should handle request failure gracefully', () => { + const magic = createMagicSDK(); + magic.user.request = jest.fn(() => { + throw new Error('Request failed'); + }); + + try { + magic.user.showSettings(); + } catch (error) { + expect(error.message).toBe('Request failed'); + } +}); + +test('ShowSettings should attach event listeners for VerifyEmailOtp event', () => { + const magic = createMagicSDK(); + const mockOn = jest.fn(); + const mockHandle = { on: mockOn }; + + magic.user.request = jest.fn().mockReturnValue(mockHandle); + + const config = { page: DeepLinkPage.Recovery, showUI: false }; + magic.user.showSettings(config); + + expect(mockOn).toHaveBeenCalledWith('Recency/auth-factor-verify-email-otp', expect.any(Function)); + + const otp = '123456'; + const createIntermediaryEventFn = jest.fn(); + magic.user.createIntermediaryEvent = jest.fn(() => createIntermediaryEventFn); + + const verifyEmailOtpListener = mockOn.mock.calls.find( + (call) => call[0] === 'Recency/auth-factor-verify-email-otp', + )[1]; + + verifyEmailOtpListener(otp); + + expect(createIntermediaryEventFn).toHaveBeenCalledWith(otp); +}); + +test('ShowSettings should return a PromiEvent even for invalid config', () => { + const magic = createMagicSDK(); + const result = magic.user.showSettings({ invalidOption: true }); + + expect(isPromiEvent(result)).toBeTruthy(); +}); +test('Generate JSON RPC request payload with undefined showUI', () => { + const magic = createMagicSDK(); + magic.user.request = jest.fn(); + + magic.user.showSettings({ page: DeepLinkPage.MFA }); + + const requestPayload = magic.user.request.mock.calls[0][0]; + expect(requestPayload.params).toEqual([{ page: 'mfa', showUI: undefined }]); +}); test('method should return a PromiEvent', () => { const magic = createMagicSDK(); expect(isPromiEvent(magic.user.showSettings())).toBeTruthy(); diff --git a/packages/@magic-sdk/types/src/modules/auth-types.ts b/packages/@magic-sdk/types/src/modules/auth-types.ts index 0577e6e8b..bcf4cfb71 100644 --- a/packages/@magic-sdk/types/src/modules/auth-types.ts +++ b/packages/@magic-sdk/types/src/modules/auth-types.ts @@ -302,7 +302,7 @@ type DeviceVerificationEventHandlers = { * Update Email */ -type RecencyCheckEventHandlers = { +export type RecencyCheckEventHandlers = { [RecencyCheckEventOnReceived.PrimaryAuthFactorNeedsVerification]: () => void; [RecencyCheckEventOnReceived.PrimaryAuthFactorVerified]: () => void; [RecencyCheckEventOnReceived.InvalidEmailOtp]: () => void; diff --git a/packages/@magic-sdk/types/src/modules/intermediary-types.ts b/packages/@magic-sdk/types/src/modules/intermediary-types.ts index fcef2ad56..702a9b317 100644 --- a/packages/@magic-sdk/types/src/modules/intermediary-types.ts +++ b/packages/@magic-sdk/types/src/modules/intermediary-types.ts @@ -22,6 +22,7 @@ import { NftCheckoutIntermediaryEvents } from './nft-types'; import { WalletEventOnReceived } from './wallet-types'; import { UiEventsEmit } from './common-types'; +import { RecoveryFactorEventEmit, RecoveryFactorEventOnReceived } from './user-types'; export type IntermediaryEvents = // EmailOTP @@ -45,6 +46,9 @@ export type IntermediaryEvents = // Auth Events | `${AuthEventOnReceived}` | `${WalletEventOnReceived}` + // Show Settings Events + | `${RecoveryFactorEventOnReceived}` + | `${RecoveryFactorEventEmit}` // Nft Checkout Events | `${NftCheckoutIntermediaryEvents}` // Farcaster Login Events diff --git a/packages/@magic-sdk/types/src/modules/user-types.ts b/packages/@magic-sdk/types/src/modules/user-types.ts index 12148b71c..77e2922f6 100644 --- a/packages/@magic-sdk/types/src/modules/user-types.ts +++ b/packages/@magic-sdk/types/src/modules/user-types.ts @@ -1,3 +1,4 @@ +import { RecencyCheckEventHandlers } from './auth-types'; import { DeepLinkPage } from '../core/deep-link-pages'; export interface GetIdTokenConfiguration { @@ -33,11 +34,37 @@ export interface MagicUserMetadata { recoveryFactors: [RecoveryFactor]; } +export enum RecoveryFactorEventOnReceived { + EnterNewPhoneNumber = 'enter-new-phone-number', + EnterOtpCode = 'enter-otp-code', + RecoveryFactorAlreadyExists = 'recovery-factor-already-exists', + InvalidOtpCode = 'invalid-otp-code', +} + +export enum RecoveryFactorEventEmit { + SendNewPhoneNumber = 'send-new-phone-number', + SendOtpCode = 'send-otp-code', + StartEditPhoneNumber = 'start-edit-phone-number', +} + type RecoveryFactor = { type: RecoveryMethodType; value: string; }; +export type RecoveryFactorEventHandlers = { + // Event Received + [RecoveryFactorEventEmit.SendNewPhoneNumber]: (phone_number: string) => void; + [RecoveryFactorEventEmit.SendOtpCode]: (otp: string) => void; + [RecoveryFactorEventEmit.StartEditPhoneNumber]: () => void; + + // Event sent + [RecoveryFactorEventOnReceived.EnterNewPhoneNumber]: () => void; + [RecoveryFactorEventOnReceived.EnterOtpCode]: () => void; + [RecoveryFactorEventOnReceived.RecoveryFactorAlreadyExists]: () => void; + [RecoveryFactorEventOnReceived.InvalidOtpCode]: () => void; +} & RecencyCheckEventHandlers; + export enum RecoveryMethodType { PhoneNumber = 'phone_number', } @@ -80,4 +107,5 @@ export interface ShowSettingsConfiguration { * deep linking destination */ page: DeepLinkPage; + showUI?: boolean; }