-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat: add access restricted modal and compliance UI infrastructure #27694
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
Changes from 5 commits
3643f72
9439be0
a168324
aa89a5f
fbe58fa
de08373
0480e7b
483cd41
69cfc5f
47c4335
788d894
9ee060a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import React from 'react'; | ||
| import { render, fireEvent } from '@testing-library/react-native'; | ||
| import AccessRestrictedModal from './AccessRestrictedModal'; | ||
| import { AccessRestrictedModalSelectorsIDs } from './AccessRestrictedModal.testIds'; | ||
|
|
||
| jest.mock( | ||
| '../../../../component-library/components/BottomSheets/BottomSheet', | ||
| () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| const { View } = require('react-native'); | ||
| return { | ||
| __esModule: true, | ||
| default: jest.fn( | ||
| ({ | ||
| children, | ||
| testID, | ||
| }: { | ||
| children: React.ReactNode; | ||
| testID?: string; | ||
| }) => <View testID={testID}>{children}</View>, | ||
| ), | ||
| }; | ||
| }, | ||
| ); | ||
|
|
||
| jest.mock( | ||
| '../../../../component-library/components/BottomSheets/BottomSheetHeader', | ||
| () => ({ | ||
| __esModule: true, | ||
| default: jest.fn(({ children }) => <>{children}</>), | ||
| }), | ||
| ); | ||
|
|
||
| jest.mock('@metamask/design-system-twrnc-preset', () => ({ | ||
| useTailwind: () => ({ style: (...args: string[]) => args }), | ||
| })); | ||
|
|
||
| describe('AccessRestrictedModal', () => { | ||
| const defaultProps = { | ||
| isVisible: true, | ||
| onClose: jest.fn(), | ||
| onContactSupport: jest.fn(), | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('renders nothing when isVisible is false', () => { | ||
| const { queryByTestId } = render( | ||
| <AccessRestrictedModal {...defaultProps} isVisible={false} />, | ||
| ); | ||
|
|
||
| expect( | ||
| queryByTestId(AccessRestrictedModalSelectorsIDs.BOTTOM_SHEET), | ||
| ).toBeNull(); | ||
| }); | ||
|
|
||
| it('renders the modal with title and description when visible', () => { | ||
| const { getByTestId } = render(<AccessRestrictedModal {...defaultProps} />); | ||
|
|
||
| expect(getByTestId(AccessRestrictedModalSelectorsIDs.TITLE)).toBeTruthy(); | ||
| expect( | ||
| getByTestId(AccessRestrictedModalSelectorsIDs.DESCRIPTION), | ||
| ).toBeTruthy(); | ||
| expect( | ||
| getByTestId(AccessRestrictedModalSelectorsIDs.CONTACT_SUPPORT_BUTTON), | ||
| ).toBeTruthy(); | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
| }); | ||
|
|
||
| it('calls onContactSupport when pressing the contact support button', () => { | ||
| const { getByTestId } = render(<AccessRestrictedModal {...defaultProps} />); | ||
|
|
||
| fireEvent.press( | ||
| getByTestId(AccessRestrictedModalSelectorsIDs.CONTACT_SUPPORT_BUTTON), | ||
| ); | ||
|
|
||
| expect(defaultProps.onContactSupport).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [NON-BLOCKING] Missing test for the close (X) button calling Consider adding a test case that exposes the |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export const AccessRestrictedModalSelectorsIDs = { | ||
| BOTTOM_SHEET: 'access-restricted-modal', | ||
| TITLE: 'access-restricted-modal-title', | ||
| DESCRIPTION: 'access-restricted-modal-description', | ||
| CONTACT_SUPPORT_BUTTON: 'access-restricted-modal-contact-support', | ||
| } as const; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MINOR] The close (X) button on closeButtonProps={{ testID: AccessRestrictedModalSelectorsIDs.CLOSE_BUTTON }}Would require adding |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import React, { useRef } from 'react'; | ||
| import { Pressable } from 'react-native'; | ||
| import { useTailwind } from '@metamask/design-system-twrnc-preset'; | ||
| import { Box } from '@metamask/design-system-react-native'; | ||
| import BottomSheet, { | ||
| BottomSheetRef, | ||
| } from '../../../../component-library/components/BottomSheets/BottomSheet'; | ||
| import BottomSheetHeader from '../../../../component-library/components/BottomSheets/BottomSheetHeader'; | ||
| import Text, { | ||
| TextColor, | ||
| TextVariant, | ||
| } from '../../../../component-library/components/Texts/Text'; | ||
| import { strings } from '../../../../../locales/i18n'; | ||
| import { AccessRestrictedModalProps } from './AccessRestrictedModal.types'; | ||
| import { AccessRestrictedModalSelectorsIDs } from './AccessRestrictedModal.testIds'; | ||
|
|
||
| const AccessRestrictedModal: React.FC<AccessRestrictedModalProps> = ({ | ||
| isVisible, | ||
| onClose, | ||
| onContactSupport, | ||
| }) => { | ||
| const tw = useTailwind(); | ||
| const bottomSheetRef = useRef<BottomSheetRef>(null); | ||
|
|
||
| if (!isVisible) return null; | ||
|
|
||
| return ( | ||
| <BottomSheet | ||
| ref={bottomSheetRef} | ||
| shouldNavigateBack={false} | ||
| onClose={onClose} | ||
| testID={AccessRestrictedModalSelectorsIDs.BOTTOM_SHEET} | ||
| > | ||
| <BottomSheetHeader onClose={onClose}> | ||
|
Check warning on line 34 in app/components/UI/Compliance/AccessRestrictedModal/AccessRestrictedModal.tsx
|
||
| <Text | ||
|
Check warning on line 35 in app/components/UI/Compliance/AccessRestrictedModal/AccessRestrictedModal.tsx
|
||
| variant={TextVariant.HeadingSM} | ||
| testID={AccessRestrictedModalSelectorsIDs.TITLE} | ||
| > | ||
| {strings('access_restricted.title')} | ||
| </Text> | ||
|
Check warning on line 40 in app/components/UI/Compliance/AccessRestrictedModal/AccessRestrictedModal.tsx
|
||
| </BottomSheetHeader> | ||
|
Check warning on line 41 in app/components/UI/Compliance/AccessRestrictedModal/AccessRestrictedModal.tsx
|
||
|
|
||
| <Box twClassName="px-4 pb-6"> | ||
| <Text | ||
|
Check warning on line 44 in app/components/UI/Compliance/AccessRestrictedModal/AccessRestrictedModal.tsx
|
||
| variant={TextVariant.BodyMD} | ||
| color={TextColor.Alternative} | ||
| testID={AccessRestrictedModalSelectorsIDs.DESCRIPTION} | ||
| > | ||
| {strings('access_restricted.description_line1')} | ||
| {'\n\n'} | ||
| {strings('access_restricted.description_line2')} | ||
| </Text> | ||
|
Check warning on line 52 in app/components/UI/Compliance/AccessRestrictedModal/AccessRestrictedModal.tsx
|
||
|
|
||
| <Pressable | ||
| onPress={onContactSupport} | ||
| testID={AccessRestrictedModalSelectorsIDs.CONTACT_SUPPORT_BUTTON} | ||
| style={({ pressed }) => | ||
| tw.style( | ||
| 'w-full h-12 items-center justify-center rounded-xl bg-muted mt-6', | ||
| pressed && 'opacity-70', | ||
| ) | ||
| } | ||
| > | ||
| <Text variant={TextVariant.BodyMDMedium}> | ||
|
Check warning on line 64 in app/components/UI/Compliance/AccessRestrictedModal/AccessRestrictedModal.tsx
|
||
| {strings('access_restricted.contact_support')} | ||
| </Text> | ||
|
Check warning on line 66 in app/components/UI/Compliance/AccessRestrictedModal/AccessRestrictedModal.tsx
|
||
| </Pressable> | ||
| </Box> | ||
| </BottomSheet> | ||
| ); | ||
| }; | ||
|
|
||
| export default AccessRestrictedModal; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export interface AccessRestrictedModalProps { | ||
| isVisible: boolean; | ||
| onClose: () => void; | ||
| onContactSupport: () => void; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export { default } from './AccessRestrictedModal'; | ||
| export type { AccessRestrictedModalProps } from './AccessRestrictedModal.types'; | ||
| export { AccessRestrictedModalSelectorsIDs } from './AccessRestrictedModal.testIds'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import React from 'react'; | ||
| import { render, fireEvent, act } from '@testing-library/react-native'; | ||
| import { Text, Pressable } from 'react-native'; | ||
| import { | ||
| AccessRestrictedProvider, | ||
| useAccessRestrictedModal, | ||
| } from './AccessRestrictedContext'; | ||
|
|
||
| const mockNavigate = jest.fn(); | ||
| jest.mock('@react-navigation/native', () => ({ | ||
| useNavigation: () => ({ navigate: mockNavigate }), | ||
| })); | ||
|
|
||
| jest.mock( | ||
| '../../../../component-library/components/BottomSheets/BottomSheet', | ||
| () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| const { View } = require('react-native'); | ||
| return { | ||
| __esModule: true, | ||
| default: jest.fn( | ||
| ({ | ||
| children, | ||
| testID, | ||
| }: { | ||
| children: React.ReactNode; | ||
| testID?: string; | ||
| }) => <View testID={testID}>{children}</View>, | ||
| ), | ||
| }; | ||
| }, | ||
| ); | ||
|
|
||
| jest.mock( | ||
| '../../../../component-library/components/BottomSheets/BottomSheetHeader', | ||
| () => ({ | ||
| __esModule: true, | ||
| default: jest.fn(({ children }) => <>{children}</>), | ||
| }), | ||
| ); | ||
|
|
||
| jest.mock('@metamask/design-system-twrnc-preset', () => ({ | ||
| useTailwind: () => ({ style: (...args: string[]) => args }), | ||
| })); | ||
|
|
||
| const TestConsumer = () => { | ||
| const { | ||
| showAccessRestrictedModal, | ||
| hideAccessRestrictedModal, | ||
| isAccessRestricted, | ||
| } = useAccessRestrictedModal(); | ||
| return ( | ||
| <> | ||
| <Text testID="is-restricted">{String(isAccessRestricted)}</Text> | ||
| <Pressable testID="show-btn" onPress={showAccessRestrictedModal} /> | ||
| <Pressable testID="hide-btn" onPress={hideAccessRestrictedModal} /> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| describe('AccessRestrictedContext', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('provides isAccessRestricted as false by default', () => { | ||
| const { getByTestId } = render( | ||
| <AccessRestrictedProvider> | ||
| <TestConsumer /> | ||
| </AccessRestrictedProvider>, | ||
| ); | ||
|
|
||
| expect(getByTestId('is-restricted').props.children).toBe('false'); | ||
| }); | ||
|
|
||
| it('sets isAccessRestricted to true when showAccessRestrictedModal is called', () => { | ||
| const { getByTestId } = render( | ||
| <AccessRestrictedProvider> | ||
| <TestConsumer /> | ||
| </AccessRestrictedProvider>, | ||
| ); | ||
|
|
||
| act(() => { | ||
| fireEvent.press(getByTestId('show-btn')); | ||
| }); | ||
|
|
||
| expect(getByTestId('is-restricted').props.children).toBe('true'); | ||
| }); | ||
|
|
||
| it('sets isAccessRestricted back to false when hideAccessRestrictedModal is called', () => { | ||
| const { getByTestId } = render( | ||
| <AccessRestrictedProvider> | ||
| <TestConsumer /> | ||
| </AccessRestrictedProvider>, | ||
| ); | ||
|
|
||
| act(() => { | ||
| fireEvent.press(getByTestId('show-btn')); | ||
| }); | ||
| expect(getByTestId('is-restricted').props.children).toBe('true'); | ||
|
|
||
| act(() => { | ||
| fireEvent.press(getByTestId('hide-btn')); | ||
| }); | ||
| expect(getByTestId('is-restricted').props.children).toBe('false'); | ||
| }); | ||
|
|
||
| it('throws when useAccessRestrictedModal is used outside provider', () => { | ||
| const spy = jest | ||
| .spyOn(console, 'error') | ||
| .mockImplementation(() => undefined); | ||
|
|
||
| expect(() => render(<TestConsumer />)).toThrow( | ||
| 'useAccessRestrictedModal must be used within an AccessRestrictedProvider', | ||
| ); | ||
|
|
||
| spy.mockRestore(); | ||
| }); | ||
|
|
||
| it('navigates to support webview when contact support is tapped', () => { | ||
| const { getByTestId } = render( | ||
| <AccessRestrictedProvider> | ||
| <TestConsumer /> | ||
| </AccessRestrictedProvider>, | ||
| ); | ||
|
|
||
| act(() => { | ||
| fireEvent.press(getByTestId('show-btn')); | ||
| }); | ||
|
|
||
| const contactSupportBtn = getByTestId( | ||
| 'access-restricted-modal-contact-support', | ||
| ); | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| act(() => { | ||
| fireEvent.press(contactSupportBtn); | ||
| }); | ||
|
|
||
| expect(mockNavigate).toHaveBeenCalledWith( | ||
| expect.any(String), | ||
| expect.objectContaining({ | ||
| params: expect.objectContaining({ | ||
| url: 'https://support.metamask.io', | ||
| }), | ||
| }), | ||
| ); | ||
| expect(getByTestId('is-restricted').props.children).toBe('false'); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.