Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(snaps): Settings Page #29234

Merged
merged 10 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions shared/constants/snaps/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const EndowmentPermissions = Object.freeze({
'endowment:webassembly': 'endowment:webassembly',
'endowment:lifecycle-hooks': 'endowment:lifecycle-hooks',
'endowment:page-home': 'endowment:page-home',
'endowment:page-settings': 'endowment:page-settings',
'endowment:signature-insight': 'endowment:signature-insight',
'endowment:name-lookup': 'endowment:name-lookup',
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
Expand Down
1 change: 1 addition & 0 deletions ui/components/app/snaps/snap-settings-page/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './snap-settings-renderer';
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { FunctionComponent, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { useI18nContext } from '../../../../hooks/useI18nContext';

import { deleteInterface } from '../../../../store/actions';
import { Box, Text } from '../../../component-library';
import {
BackgroundColor,
BlockSize,
TextVariant,
} from '../../../../helpers/constants/design-system';
import { SnapDelineator } from '../snap-delineator';
import { getSnapMetadata } from '../../../../selectors';
import { DelineatorType } from '../../../../helpers/constants/snaps';
import { Copyable } from '../copyable';
import { SnapUIRenderer } from '../snap-ui-renderer';
import { useSnapSettings } from '../../../../hooks/snaps/useSnapSettings';
import { decodeSnapIdFromPathname } from '../../../../helpers/utils/snaps';

type SnapSettingsRendererProps = {
snapId: string;
};

export const SnapSettingsRenderer: FunctionComponent<
SnapSettingsRendererProps
> = () => {
const { pathname } = useLocation();
const dispatch = useDispatch();
const t = useI18nContext();

const snapId = useMemo(() => decodeSnapIdFromPathname(pathname), [pathname]);

const { name: snapName } = useSelector((state) =>
getSnapMetadata(state, snapId),
GuillaumeRx marked this conversation as resolved.
Show resolved Hide resolved
);

const { data, error, loading } = useSnapSettings({
snapId,
});

const interfaceId = !loading && !error ? data?.id : undefined;

useEffect(() => {
return () => {
interfaceId && dispatch(deleteInterface(interfaceId));
};
}, [interfaceId]);

if (!snapId) {
return null;
}

return (
<Box
height={BlockSize.Full}
width={BlockSize.Full}
backgroundColor={BackgroundColor.backgroundDefault}
>
{error && (
<Box height={BlockSize.Full} padding={4}>
<SnapDelineator snapName={snapName} type={DelineatorType.Error}>
<Text variant={TextVariant.bodySm} marginBottom={4}>
{t('snapsUIError', [<b key="0">{snapName}</b>])}
</Text>
<Copyable text={error.message} />
</SnapDelineator>
</Box>
)}
{(interfaceId || loading) && (
<SnapUIRenderer
snapId={snapId}
interfaceId={interfaceId}
isLoading={loading}
useDelineator={false}
contentBackgroundColor={BackgroundColor.backgroundDefault}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The settings page having a backgroundDefault background color interferes with some of our components.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we fix the clash by forcing a background or is there additional problems?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clash still exists, the primary problem is with Section that has the same background color

/>
)}
</Box>
);
};
5 changes: 5 additions & 0 deletions ui/components/app/tab-bar/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@
display: flex;
align-items: center;
position: relative;
overflow: hidden;
width: 100%;

&__title {
@include design-system.H4;

white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

@include design-system.screen-sm-min {
@include design-system.H6;
}
Expand Down
3 changes: 3 additions & 0 deletions ui/helpers/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ PATH_NAME_MAP[CONTACT_ADD_ROUTE] = 'Add Contact Settings Page';
export const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact';
PATH_NAME_MAP[`${CONTACT_VIEW_ROUTE}/:address`] = 'View Contact Settings Page';

export const SNAP_SETTINGS_ROUTE = '/settings/snap';
PATH_NAME_MAP[`${SNAP_SETTINGS_ROUTE}/:snapId`] = 'Snap Settings Page';

export const REVEAL_SEED_ROUTE = '/seed';
PATH_NAME_MAP[REVEAL_SEED_ROUTE] = 'Reveal Secret Recovery Phrase Page';

Expand Down
14 changes: 0 additions & 14 deletions ui/helpers/utils/snaps.js

This file was deleted.

40 changes: 40 additions & 0 deletions ui/helpers/utils/snaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { SnapId } from '@metamask/snaps-sdk';
import { isProduction } from '../../../shared/modules/environment';

/**
* Check if the given value is a valid snap ID.
*
* NOTE: This function is a duplicate oF a yet to be released version in @metamask/snaps-utils.
*
* @param value - The value to check.
* @returns `true` if the value is a valid snap ID, and `false` otherwise.
*/
export function isSnapId(value: unknown): value is SnapId {
return (
(typeof value === 'string' || value instanceof String) &&
(value.startsWith('local:') || value.startsWith('npm:'))
);
}

/**
* Decode a snap ID fron a pathname.
*
* @param pathname - The pathname to decode the snap ID from.
* @returns The decoded snap ID, or `undefined` if the snap ID could not be decoded.
*/
export const decodeSnapIdFromPathname = (pathname: string) => {
const snapIdURI = pathname?.match(/[^/]+$/u)?.[0];
return snapIdURI && decodeURIComponent(snapIdURI);
};

const IGNORED_EXAMPLE_SNAPS = ['npm:@metamask/preinstalled-example-snap'];

/**
* Check if the given snap ID is ignored in production.
*
* @param snapId - The snap ID to check.
* @returns `true` if the snap ID is ignored in production, and `false` otherwise.
*/
export const isSnapIgnoredInProd = (snapId: string) => {
return isProduction() ? IGNORED_EXAMPLE_SNAPS.includes(snapId) : false;
};
53 changes: 53 additions & 0 deletions ui/hooks/snaps/useSnapSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import {
forceUpdateMetamaskState,
handleSnapRequest,
} from '../../store/actions';

export function useSnapSettings({ snapId }: { snapId?: string }) {
const dispatch = useDispatch();
const [loading, setLoading] = useState<boolean>(true);
const [data, setData] = useState<{ id: string } | undefined>(undefined);
const [error, setError] = useState<Error | undefined>(undefined);

useEffect(() => {
let cancelled = false;
async function fetchPage() {
try {
setError(undefined);
setLoading(true);

const newData = snapId
? ((await handleSnapRequest({
snapId,
origin: '',
handler: 'onSettingsPage',
request: {
jsonrpc: '2.0',
method: ' ',
},
})) as { id: string })
: undefined;
if (!cancelled) {
setData(newData);
forceUpdateMetamaskState(dispatch);
}
} catch (err) {
if (!cancelled) {
setError(err as Error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchPage();
GuillaumeRx marked this conversation as resolved.
Show resolved Hide resolved
return () => {
cancelled = true;
};
}, [snapId]);

return { data, error, loading };
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ describe('PersonalSignInfo', () => {
getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE);

(utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false);
(snapUtils.isSnapId as jest.Mock).mockReturnValue(true);
(snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true);

const mockStore = configureMockStore([])(state);
const { queryByText, getByText } = renderWithConfirmContextProvider(
Expand All @@ -167,7 +167,7 @@ describe('PersonalSignInfo', () => {
const state =
getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE);
(utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false);
(snapUtils.isSnapId as jest.Mock).mockReturnValue(true);
(snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true);

const mockStore = configureMockStore([])(state);
const { getByText, queryByText } = renderWithConfirmContextProvider(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('TypedSignInfo', () => {
type: TransactionType.signTypedData,
chainId: '0x5',
});
(snapUtils.isSnapId as jest.Mock).mockReturnValue(true);
(snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true);
const mockStore = configureMockStore([])(mockState);
const { queryByText } = renderWithConfirmContextProvider(
<TypedSignInfoV1 />,
Expand All @@ -88,7 +88,7 @@ describe('TypedSignInfo', () => {
type: TransactionType.signTypedData,
chainId: '0x5',
});
(snapUtils.isSnapId as jest.Mock).mockReturnValue(false);
(snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(false);
const mockStore = configureMockStore([])(mockState);
const { queryByText } = renderWithConfirmContextProvider(
<TypedSignInfoV1 />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ describe('TypedSignInfo', () => {
type: TransactionType.signTypedData,
chainId: '0x5',
});
(snapUtils.isSnapId as jest.Mock).mockReturnValue(true);
(snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true);
const mockStore = configureMockStore([])(mockState);
const { queryByText } = renderWithConfirmContextProvider(
<TypedSignInfo />,
Expand All @@ -177,7 +177,7 @@ describe('TypedSignInfo', () => {
type: TransactionType.signTypedData,
chainId: '0x5',
});
(snapUtils.isSnapId as jest.Mock).mockReturnValue(false);
(snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a type error now that the function is written in typescript

const mockStore = configureMockStore([])(mockState);
const { queryByText } = renderWithConfirmContextProvider(
<TypedSignInfo />,
Expand Down
3 changes: 2 additions & 1 deletion ui/pages/settings/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@

&__title {
@include design-system.screen-sm-min {
width: 197px;
margin-right: 16px;
}

@include design-system.screen-sm-max {
Expand Down Expand Up @@ -230,6 +230,7 @@
display: flex;
flex-direction: column;
flex: 1 1 auto;
max-width: 100vw;

@include design-system.screen-sm-min {
flex: 0 0 40%;
Expand Down
Loading
Loading