Skip to content

Commit

Permalink
feat: correct source in connection event
Browse files Browse the repository at this point in the history
  • Loading branch information
abretonc7s committed Nov 27, 2024
1 parent 78abd0d commit 0bfe731
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 26 deletions.
15 changes: 12 additions & 3 deletions app/components/Approvals/PermissionApproval/PermissionApproval.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { useEffect, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import useApprovalRequest from '../../Views/confirmations/hooks/useApprovalRequest';
import { ApprovalTypes } from '../../../core/RPCMethods/RPCMethodMiddleware';
import { MetaMetricsEvents } from '../../../core/Analytics';
import { createAccountConnectNavDetails } from '../../Views/AccountConnect';
import { useSelector } from 'react-redux';
import { selectAccountsLength } from '../../../selectors/accountTrackerController';
import { useMetrics } from '../../../components/hooks/useMetrics';
import DevLogger from '../../../core/SDKConnect/utils/DevLogger';
import useOriginSource from '../../hooks/useOriginSource';

export interface PermissionApprovalProps {
// TODO: Replace "any" with type
Expand All @@ -20,8 +22,10 @@ const PermissionApproval = (props: PermissionApprovalProps) => {
const totalAccounts = useSelector(selectAccountsLength);
const isProcessing = useRef<boolean>(false);

const eventSource = useOriginSource({ origin: approvalRequest?.requestData?.metadata?.origin });

useEffect(() => {
if (approvalRequest?.type !== ApprovalTypes.REQUEST_PERMISSIONS) {
if (approvalRequest?.type !== ApprovalTypes.REQUEST_PERMISSIONS || !eventSource) {
isProcessing.current = false;
return;
}
Expand All @@ -38,11 +42,15 @@ const PermissionApproval = (props: PermissionApprovalProps) => {

isProcessing.current = true;

DevLogger.log(
`PermissionApproval::useEffect() totalAccounts=${totalAccounts} source=${eventSource}`,
);

trackEvent(
createEventBuilder(MetaMetricsEvents.CONNECT_REQUEST_STARTED)
.addProperties({
number_of_accounts: totalAccounts,
source: 'PERMISSION SYSTEM',
source: eventSource,
})
.build(),
);
Expand All @@ -59,6 +67,7 @@ const PermissionApproval = (props: PermissionApprovalProps) => {
props.navigation,
trackEvent,
createEventBuilder,
eventSource,
]);

return null;
Expand Down
28 changes: 5 additions & 23 deletions app/components/Views/AccountConnect/AccountConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ import { RootState } from '../../../reducers';
import { trackDappViewedEvent } from '../../../util/metrics';
import { useTheme } from '../../../util/theme';
import useFavicon from '../../hooks/useFavicon/useFavicon';
import { SourceType } from '../../hooks/useMetrics/useMetrics.types';
import {
AccountConnectProps,
AccountConnectScreens,
Expand All @@ -83,6 +82,8 @@ import { CaveatTypes } from '../../../core/Permissions/constants';
import { useNetworkInfo } from '../../../selectors/selectedNetworkController';
import { AvatarSize } from '../../../component-library/components/Avatars/Avatar';
import { selectNetworkConfigurations } from '../../../selectors/networkController';
import { isUUID } from '../../../core/SDKConnect/utils/isUUID';
import useOriginSource from '../../hooks/useOriginSource';

const createStyles = () =>
StyleSheet.create({
Expand Down Expand Up @@ -144,12 +145,6 @@ const AccountConnect = (props: AccountConnectProps) => {
origin: string;
};

const isUUID = (str: string) => {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
};

const isChannelId = isUUID(channelIdOrHostname);

const sdkConnection = SDKConnect.getInstance().getConnection({
Expand Down Expand Up @@ -320,20 +315,7 @@ const AccountConnect = (props: AccountConnectProps) => {
[hostname],
);

const eventSource = useMemo(() => {
// walletconnect channelId format: app.name.org
// sdk channelId format: uuid
// inappbrowser channelId format: app.name.org but origin is set
if (isOriginWalletConnect) {
return SourceType.WALLET_CONNECT;
}

if (sdkConnection) {
return SourceType.SDK;
}

return SourceType.IN_APP_BROWSER;
}, [isOriginWalletConnect, sdkConnection]);
const eventSource = useOriginSource({ origin: channelIdOrHostname });

// Refreshes selected addresses based on the addition and removal of accounts.
useEffect(() => {
Expand Down Expand Up @@ -370,12 +352,12 @@ const AccountConnect = (props: AccountConnectProps) => {
createEventBuilder(MetaMetricsEvents.CONNECT_REQUEST_CANCELLED)
.addProperties({
number_of_accounts: accountsLength,
source: SourceType.PERMISSION_SYSTEM,
source: eventSource,
})
.build(),
);
},
[accountsLength, channelIdOrHostname, trackEvent, createEventBuilder],
[accountsLength, channelIdOrHostname, trackEvent, createEventBuilder, eventSource],
);

const navigateToUrlInEthPhishingModal = useCallback(
Expand Down
110 changes: 110 additions & 0 deletions app/components/hooks/__tests__/useOriginSource.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { renderHook } from '@testing-library/react-hooks';
import { useOriginSource } from '../useOriginSource';
import { SourceType } from '../useMetrics/useMetrics.types';
import AppConstants from '../../../core/AppConstants';
import { RootState } from '../../../reducers';

// Mock dependencies
jest.mock('react-redux', () => ({
useSelector: jest.fn((selector: (state: RootState) => unknown) =>
selector({
sdk: {
wc2Metadata: {
id: '',
},
},
} as RootState),
),
}));

jest.mock('../../../core/SDKConnect/SDKConnect', () => ({
getInstance: jest.fn(() => ({
getConnection: jest.fn(({ channelId }) => {
if (channelId === '123e4567-e89b-12d3-a456-426614174000') {
return {
id: channelId,
trigger: 'test-trigger',
otherPublicKey: 'test-public-key',
origin: 'test-origin',
protocolVersion: '1.0',
originatorInfo: { name: 'test-originator' },
initialConnection: true,
validUntil: Date.now() + 10000,
};
}
return undefined;
}),
})),
}));

describe('useOriginSource', () => {
const mockState = {
sdk: {
wc2Metadata: {
id: '',
},
},
} as RootState;

const mockSelector = jest.requireMock('react-redux').useSelector;

beforeEach(() => {
jest.clearAllMocks();
mockSelector.mockImplementation((selector: (state: RootState) => unknown) =>
selector(mockState)
);
});

it('should return undefined when origin is undefined', () => {
const { result } = renderHook(() => useOriginSource({ origin: undefined }));
expect(result.current).toBeUndefined();
});

it('should return SDK source for valid UUID origin with connection', () => {
const { result } = renderHook(() =>
useOriginSource({ origin: '123e4567-e89b-12d3-a456-426614174000' }),
);
expect(result.current).toBe(SourceType.SDK);
});

it('should return SDK source for SDK_REMOTE_ORIGIN', () => {
const { result } = renderHook(() =>
useOriginSource({
origin: `${AppConstants.MM_SDK.SDK_REMOTE_ORIGIN}some-path`,
}),
);
expect(result.current).toBe(SourceType.SDK);
});

it('should return WALLET_CONNECT source when WC metadata is present', () => {
// Mock WalletConnect metadata
const wcState = {
...mockState,
sdk: {
wc2Metadata: {
id: 'some-wc-id',
},
},
} as RootState;

jest.requireMock('react-redux').useSelector.mockImplementation(
(selector: (state: RootState) => unknown) => selector(wcState),
);

const { result } = renderHook(() =>
useOriginSource({ origin: 'some-non-uuid-origin' }),
);
expect(result.current).toBe(SourceType.WALLET_CONNECT);
});

it('should return IN_APP_BROWSER source as default', () => {
mockSelector.mockImplementation((selector: (state: RootState) => unknown) =>
selector(mockState)
);

const { result } = renderHook(() =>
useOriginSource({ origin: 'https://example.com' }),
);
expect(result.current).toBe(SourceType.IN_APP_BROWSER);
});
});
44 changes: 44 additions & 0 deletions app/components/hooks/useOriginSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useSelector } from 'react-redux';
import SDKConnect from '../../core/SDKConnect/SDKConnect';
import { RootState } from '../../reducers';
import { isUUID } from '../../core/SDKConnect/utils/isUUID';
import { SourceType } from './useMetrics/useMetrics.types';
import AppConstants from '../../core/AppConstants';

interface UseOriginSourceProps {
origin?: string;
}

type SourceTypeValue = typeof SourceType[keyof typeof SourceType];

export const useOriginSource = ({ origin }: UseOriginSourceProps): SourceTypeValue | undefined => {
const { wc2Metadata } = useSelector((state: RootState) => state.sdk);

// Return undefined if origin is undefined
if (!origin) {
return undefined;
}

// Check if origin is a UUID (SDK channel ID format) or starts with SDK_REMOTE_ORIGIN
const isChannelId = isUUID(origin);
const isSDKRemoteOrigin = origin.startsWith(AppConstants.MM_SDK.SDK_REMOTE_ORIGIN);

const sdkConnection = isChannelId
? SDKConnect.getInstance().getConnection({ channelId: origin })
: undefined;

// Check if it's SDK (either by UUID connection or remote origin)
if (sdkConnection || isSDKRemoteOrigin) {
return SourceType.SDK;
}

// Check if origin matches WalletConnect metadata
const isWalletConnect = wc2Metadata?.id && wc2Metadata.id.length > 0;
if (isWalletConnect) {
return SourceType.WALLET_CONNECT;
}

return SourceType.IN_APP_BROWSER;
};

export default useOriginSource;
35 changes: 35 additions & 0 deletions app/core/SDKConnect/utils/isUUID.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { isUUID } from './isUUID';

describe('isUUID', () => {
it('should return true for valid UUIDs', () => {
const validUUIDs = [
'123e4567-e89b-12d3-a456-426614174000',
'c73bcdcc-2669-4bf6-81d3-e4ae73fb11fd',
'507f191e-1f7f-4d1b-9bc8-d8d49c6b1012',
// Uppercase should also work due to case-insensitive flag
'A987FBC9-4BED-3078-CF07-9141BA07C9F3',
];

validUUIDs.forEach((uuid) => {
expect(isUUID(uuid)).toBe(true);
});
});

it('should return false for invalid UUIDs', () => {
const invalidUUIDs = [
'',
'not-a-uuid',
'123e4567-e89b-12d3-a456', // incomplete
'123e4567-e89b-12d3-a456-42661417400z', // invalid character
'123e4567-e89b-12d3-a456-4266141740000', // too long
'123e4567.e89b.12d3.a456.426614174000', // wrong separator
null,
undefined,
];

invalidUUIDs.forEach((uuid) => {
// @ts-expect-error Testing invalid inputs
expect(isUUID(uuid)).toBe(false);
});
});
});
5 changes: 5 additions & 0 deletions app/core/SDKConnect/utils/isUUID.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const isUUID = (str: string) => {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
};

Check warning on line 5 in app/core/SDKConnect/utils/isUUID.ts

View workflow job for this annotation

GitHub Actions / scripts (lint)

Newline required at end of file but not found

0 comments on commit 0bfe731

Please sign in to comment.