diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index fd87c09567db..2086586d4100 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -5084,7 +5084,7 @@ "message": "Snaps connected" }, "snapsNoInsight": { - "message": "The snap didn't return any insight" + "message": "No insight to show" }, "snapsPrivacyWarningFirstMessage": { "message": "You acknowledge that any Snap that you install is a Third Party Service, unless otherwise identified, as defined in the Consensys $1. Your use of Third Party Services is governed by separate terms and conditions set forth by the Third Party Service provider. Consensys does not recommend the use of any Snap by any particular person for any particular reason. You access, rely upon or use the Third Party Service at your own risk. Consensys disclaims all responsibility and liability for any losses on account of your use of Third Party Services.", diff --git a/ui/components/ui/delineator/delineator.tsx b/ui/components/ui/delineator/delineator.tsx index c180d5005eaa..fcdcbfd786cb 100644 --- a/ui/components/ui/delineator/delineator.tsx +++ b/ui/components/ui/delineator/delineator.tsx @@ -38,7 +38,7 @@ const ExpandableIcon = ({ isExpanded }: { isExpanded: boolean }) => { ); }; @@ -49,14 +49,16 @@ const Header = ({ isCollapsible, isExpanded, isLoading, + isDisabled, onHeaderClick, type, }: { headerComponent: DelineatorProps['headerComponent']; - iconName: IconName; + iconName?: IconName; isCollapsible: boolean; isExpanded: boolean; isLoading: boolean; + isDisabled: boolean; onHeaderClick: () => void; type?: DelineatorType; }) => { @@ -67,18 +69,19 @@ const Header = ({ delineator__header: true, 'delineator__header--expanded': isExpanded, 'delineator__header--loading': isLoading, + 'delineator__header--disabled': isDisabled, })} display={Display.Flex} alignItems={AlignItems.center} justifyContent={JustifyContent.spaceBetween} paddingTop={2} paddingRight={4} - paddingBottom={2} + paddingBottom={isExpanded ? 0 : 2} paddingLeft={4} onClick={onHeaderClick} > - + {iconName && } {overrideTextComponentColorByType({ component: headerComponent, type, @@ -89,7 +92,13 @@ const Header = ({ ); }; -const Content = ({ children }: { children: React.ReactNode }) => { +const Content = ({ + children, + contentBoxProps, +}: { + children: React.ReactNode; + contentBoxProps: DelineatorProps['contentBoxProps']; +}) => { return ( { paddingBottom={4} paddingLeft={4} flexDirection={FlexDirection.Column} + {...contentBoxProps} > {children} @@ -131,21 +141,23 @@ export const Delineator: React.FC = ({ isCollapsible = true, isExpanded: isExpandedProp, isLoading = false, + isDisabled = false, onExpandChange, type, wrapperBoxProps, + contentBoxProps, }) => { const [isExpanded, setIsExpanded] = useState(isExpandedProp || false); const shouldShowContent = !isCollapsible || (isCollapsible && isExpanded); const handleHeaderClick = useCallback(() => { - if (isLoading || !isCollapsible) { + if (isDisabled || isLoading || !isCollapsible) { return; } const newExpandedState = !isExpanded; onExpandChange?.(newExpandedState); setIsExpanded(newExpandedState); - }, [isLoading, isCollapsible, isExpanded, onExpandChange]); + }, [isLoading, isCollapsible, isExpanded, isDisabled, onExpandChange]); return ( @@ -155,10 +167,13 @@ export const Delineator: React.FC = ({ isCollapsible={isCollapsible} isExpanded={isExpanded} isLoading={isLoading} + isDisabled={isDisabled} onHeaderClick={handleHeaderClick} type={type} /> - {shouldShowContent && !isLoading && {children}} + {shouldShowContent && !isLoading && ( + {children} + )} ); }; diff --git a/ui/components/ui/delineator/delineator.types.ts b/ui/components/ui/delineator/delineator.types.ts index 3f49790fae98..dc83f55a289a 100644 --- a/ui/components/ui/delineator/delineator.types.ts +++ b/ui/components/ui/delineator/delineator.types.ts @@ -3,13 +3,15 @@ import { Box, IconName, Text } from '../../component-library'; export type DelineatorProps = { children?: React.ReactNode; headerComponent: React.ReactElement; - iconName: IconName; + iconName?: IconName; isCollapsible?: boolean; isExpanded?: boolean; isLoading?: boolean; + isDisabled?: boolean; onExpandChange?: (isExpanded: boolean) => void; type?: DelineatorType; wrapperBoxProps?: React.ComponentProps; + contentBoxProps?: React.ComponentProps; }; export enum DelineatorType { diff --git a/ui/components/ui/delineator/index.scss b/ui/components/ui/delineator/index.scss index 4d222a35b951..46bcfe11050d 100644 --- a/ui/components/ui/delineator/index.scss +++ b/ui/components/ui/delineator/index.scss @@ -5,5 +5,10 @@ &--loading { cursor: default; } + + &--disabled { + cursor: default; + opacity: 0.5; + } } } diff --git a/ui/components/ui/delineator/utils.ts b/ui/components/ui/delineator/utils.ts index 5675042e8772..666d3ad5e990 100644 --- a/ui/components/ui/delineator/utils.ts +++ b/ui/components/ui/delineator/utils.ts @@ -42,7 +42,7 @@ const getTextColorByType = (type?: DelineatorType) => { case DelineatorType.Error: return TextColor.errorDefault; default: - return TextColor.textAlternative; + return TextColor.textDefault; } }; diff --git a/ui/pages/confirmations/components/confirm/pluggable-section/pluggable-section.tsx b/ui/pages/confirmations/components/confirm/pluggable-section/pluggable-section.tsx index c167cdb83c0f..a8f1ecc34f6b 100644 --- a/ui/pages/confirmations/components/confirm/pluggable-section/pluggable-section.tsx +++ b/ui/pages/confirmations/components/confirm/pluggable-section/pluggable-section.tsx @@ -3,9 +3,10 @@ import { ReactComponentLike } from 'prop-types'; import { useSelector } from 'react-redux'; import { currentConfirmationSelector } from '../../../selectors'; +import { SnapsSection } from '../snaps/snaps-section'; // Components to be plugged into confirmation page can be added to the array below -const pluggedInSections: ReactComponentLike[] = []; +const pluggedInSections: ReactComponentLike[] = [SnapsSection]; const PluggableSection = () => { const currentConfirmation = useSelector(currentConfirmationSelector); diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap new file mode 100644 index 000000000000..609ee9224182 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SnapsSection renders section for typed sign request 1`] = ` +
+
+
+
+
+

+ + + Insights from + + BIP-32 Test Snap + + + + +

+
+ +
+
+
+
+

+ Hello world again! +

+
+
+
+
+
+
+`; + +exports[`SnapsSection renders section personal sign request 1`] = ` +
+
+
+
+
+

+ + + Insights from + + BIP-32 Test Snap + + + + +

+
+ +
+
+
+
+

+ Hello world! +

+
+
+
+
+
+
+`; diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/index.ts b/ui/pages/confirmations/components/confirm/snaps/snaps-section/index.ts new file mode 100644 index 000000000000..1e898dac9479 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/index.ts @@ -0,0 +1 @@ +export * from './snaps-section'; diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/snap-insight.tsx b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snap-insight.tsx new file mode 100644 index 000000000000..b428ee8d158d --- /dev/null +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snap-insight.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { SnapUIRenderer } from '../../../../../../components/app/snaps/snap-ui-renderer'; +import { Delineator } from '../../../../../../components/ui/delineator'; +import { Text } from '../../../../../../components/component-library'; +import { + TextColor, + TextVariant, + FontWeight, +} from '../../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../../hooks/useI18nContext'; +import { getSnapMetadata } from '../../../../../../selectors'; +import Tooltip from '../../../../../../components/ui/tooltip'; + +export type SnapInsightProps = { + snapId: string; + interfaceId: string; + loading: boolean; +}; + +export const SnapInsight: React.FunctionComponent = ({ + snapId, + interfaceId, + loading, +}) => { + const t = useI18nContext(); + const { name: snapName } = useSelector((state) => + /* @ts-expect-error wrong type on selector. */ + getSnapMetadata(state, snapId), + ); + + const headerComponent = ( + + {t('insightsFromSnap', [ + + {snapName} + , + ])} + + ); + + const hasNoInsight = !loading && !interfaceId; + + if (hasNoInsight) { + return ( + + + + ); + } + + return ( + + + + ); +}; diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.test.tsx b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.test.tsx new file mode 100644 index 000000000000..1c13c6eb4bdc --- /dev/null +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.test.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { Text } from '@metamask/snaps-sdk/jsx'; + +import { fireEvent } from '@testing-library/react'; +import mockState from '../../../../../../../test/data/mock-state.json'; +import { unapprovedPersonalSignMsg } from '../../../../../../../test/data/confirmations/personal_sign'; +import { unapprovedTypedSignMsgV3 } from '../../../../../../../test/data/confirmations/typed_sign'; +import { renderWithProvider } from '../../../../../../../test/lib/render-helpers'; +import { SnapsSection } from './snaps-section'; + +const additionalMockState = { + insights: { + [unapprovedPersonalSignMsg.id]: { + 'npm:@metamask/test-snap-bip32': { + snapId: 'npm:@metamask/test-snap-bip32', + loading: false, + interfaceId: 'interface-id', + }, + }, + [unapprovedTypedSignMsgV3.id]: { + 'npm:@metamask/test-snap-bip32': { + snapId: 'npm:@metamask/test-snap-bip32', + loading: false, + interfaceId: 'interface-id2', + }, + }, + }, + interfaces: { + 'interface-id': { + snapId: 'npm:@metamask/test-snap-bip32', + content: Text({ children: 'Hello world!' }), + state: {}, + context: null, + }, + 'interface-id2': { + snapId: 'npm:@metamask/test-snap-bip32', + content: Text({ children: 'Hello world again!' }), + state: {}, + context: null, + }, + }, +}; + +describe('SnapsSection', () => { + it('renders section personal sign request', () => { + const state = { + ...mockState, + confirm: { + currentConfirmation: unapprovedPersonalSignMsg, + }, + metamask: { + ...mockState.metamask, + ...additionalMockState, + }, + }; + const mockStore = configureMockStore([])(state); + const { container, getByText } = renderWithProvider( + , + mockStore, + ); + + fireEvent.click(getByText('Insights from')); + + expect(container).toMatchSnapshot(); + expect(getByText('Hello world!')).toBeDefined(); + }); + + it('renders section for typed sign request', () => { + const state = { + ...mockState, + confirm: { + currentConfirmation: unapprovedTypedSignMsgV3, + }, + metamask: { + ...mockState.metamask, + ...additionalMockState, + }, + }; + const mockStore = configureMockStore([])(state); + const { container, getByText } = renderWithProvider( + , + mockStore, + ); + + fireEvent.click(getByText('Insights from')); + + expect(container).toMatchSnapshot(); + expect(getByText('Hello world again!')).toBeDefined(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.tsx b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.tsx new file mode 100644 index 000000000000..f09b4acccf53 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { currentConfirmationSelector } from '../../../../selectors'; +import { useInsightSnaps } from '../../../../../../hooks/snaps/useInsightSnaps'; +import { Box } from '../../../../../../components/component-library'; +import { + Display, + FlexDirection, +} from '../../../../../../helpers/constants/design-system'; +import { SnapInsight } from './snap-insight'; + +export const SnapsSection = () => { + const currentConfirmation = useSelector(currentConfirmationSelector); + const { data } = useInsightSnaps(currentConfirmation?.id); + + if (data.length === 0) { + return null; + } + + return ( + + {data.map(({ snapId, interfaceId, loading }) => ( + + ))} + + ); +};