diff --git a/app/components/Nav/App/App.tsx b/app/components/Nav/App/App.tsx index 2cc4b3f76351..9397b51e1f57 100644 --- a/app/components/Nav/App/App.tsx +++ b/app/components/Nav/App/App.tsx @@ -147,6 +147,7 @@ import RevealSRP from '../../Views/MultichainAccounts/sheets/RevealSRP'; import { DeepLinkModal } from '../../UI/DeepLinkModal'; import { checkForDeeplink } from '../../../actions/user'; import { WalletDetails } from '../../Views/MultichainAccounts/WalletDetails/WalletDetails'; +import { SmartAccountUpdateModal } from '../../Views/confirmations/components/smart-account-update-modal'; const clearStackNavigatorOptions = { headerShown: false, @@ -656,6 +657,21 @@ const ModalSwitchAccountType = () => ( ); +const ModalSmartAccountOptIn = () => ( + + + +); + const AppFlow = () => { const userLoggedIn = useSelector(selectUserLoggedIn); @@ -796,6 +812,10 @@ const AppFlow = () => { name={Routes.CONFIRMATION_SWITCH_ACCOUNT_TYPE} component={ModalSwitchAccountType} /> + ); }; diff --git a/app/components/UI/Carousel/constants.ts b/app/components/UI/Carousel/constants.ts index bb2662732782..57a88c4ff3b8 100644 --- a/app/components/UI/Carousel/constants.ts +++ b/app/components/UI/Carousel/constants.ts @@ -13,6 +13,7 @@ import cashoutImage from '../../../images/banners/banner_image_cashout.png'; import aggregatedImage from '../../../images/banners/banner_image_aggregated.png'; import backupAndSyncImage from '../../../images/banners/banner_image_backup_and_sync.png'; import multiSrpImage from '../../../images/banners/banner_image_multisrp.png'; +import { createSmartAccountNavigationDetails } from '../../Views/confirmations/utils/generic'; ///: BEGIN:ONLY_INCLUDE_IF(solana) import solanaImage from '../../../images/banners/banner_image_solana.png'; import { WalletClientType } from '../../../core/SnapKeyring/MultichainWalletSnapClient'; @@ -48,7 +49,7 @@ export const PREDEFINED_SLIDES: CarouselSlide[] = [ undismissable: false, navigation: { type: 'function', - navigate: () => [Routes.CONFIRMATION_SWITCH_ACCOUNT_TYPE], + navigate: createSmartAccountNavigationDetails, }, }, { diff --git a/app/components/UI/Carousel/index.test.tsx b/app/components/UI/Carousel/index.test.tsx index 0fad4c6d4704..40e1b8250be1 100644 --- a/app/components/UI/Carousel/index.test.tsx +++ b/app/components/UI/Carousel/index.test.tsx @@ -62,6 +62,11 @@ jest.mock('@react-navigation/native', () => ({ jest.mock('../../../core/Engine', () => ({ getTotalEvmFiatAccountBalance: jest.fn(), setSelectedAddress: jest.fn(), + context: { + PreferencesController: { + state: {}, + }, + }, })); jest.mock('../../../util/theme', () => ({ diff --git a/app/components/UI/Carousel/types.ts b/app/components/UI/Carousel/types.ts index b139c4147125..93d776b68986 100644 --- a/app/components/UI/Carousel/types.ts +++ b/app/components/UI/Carousel/types.ts @@ -27,7 +27,9 @@ interface NavigationScreen { params: NavigationParams; } -type NavigationRoute = readonly [string] | readonly [string, NavigationScreen]; +export type NavigationRoute = + | readonly [string] + | readonly [string, NavigationScreen]; export interface UrlNavigationAction { type: 'url'; diff --git a/app/components/Views/confirmations/components/confirm/confirm-component.test.tsx b/app/components/Views/confirmations/components/confirm/confirm-component.test.tsx index 906794f18cea..e8164b8733a1 100644 --- a/app/components/Views/confirmations/components/confirm/confirm-component.test.tsx +++ b/app/components/Views/confirmations/components/confirm/confirm-component.test.tsx @@ -84,7 +84,7 @@ jest.mock('../../../../../core/Engine', () => ({ id: '01JNG7170V9X27V5NFDTY04PJ4', name: '', }, - } + }, ], }, getOrAddQRKeyring: jest.fn(), @@ -287,7 +287,6 @@ describe('Confirm', () => { }); expect(getByText('Use smart account?')).toBeTruthy(); - expect(getByText('Request for')).toBeTruthy(); }); it('returns null if confirmation redesign is not enabled', () => { diff --git a/app/components/Views/confirmations/components/smart-account-update-content/index.ts b/app/components/Views/confirmations/components/smart-account-update-content/index.ts new file mode 100644 index 000000000000..d0cd9b837410 --- /dev/null +++ b/app/components/Views/confirmations/components/smart-account-update-content/index.ts @@ -0,0 +1 @@ +export { SmartAccountUpdateContent } from './smart-account-update-content'; diff --git a/app/components/Views/confirmations/components/smart-account-update-content/smart-account-update-content.styles.ts b/app/components/Views/confirmations/components/smart-account-update-content/smart-account-update-content.styles.ts new file mode 100644 index 000000000000..82df54174545 --- /dev/null +++ b/app/components/Views/confirmations/components/smart-account-update-content/smart-account-update-content.styles.ts @@ -0,0 +1,35 @@ +import { StyleSheet } from 'react-native'; + +const styleSheet = () => + StyleSheet.create({ + image: { + marginTop: 28, + height: '28%', + width: '100%', + }, + title: { + marginLeft: 8, + }, + requestSection: { + flexDirection: 'row', + alignItems: 'center', + }, + icon: { + height: 24, + width: 24, + }, + listWrapper: { + flexDirection: 'row', + alignItems: 'flex-start', + paddingInline: 2, + }, + textSection: { + marginLeft: 8, + width: '90%', + }, + accountIcon: { + marginRight: -6, + }, + }); + +export default styleSheet; diff --git a/app/components/Views/confirmations/components/smart-account-update-content/smart-account-update-content.test.tsx b/app/components/Views/confirmations/components/smart-account-update-content/smart-account-update-content.test.tsx new file mode 100644 index 000000000000..af9de1d553b2 --- /dev/null +++ b/app/components/Views/confirmations/components/smart-account-update-content/smart-account-update-content.test.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import renderWithProvider from '../../../../../util/test/renderWithProvider'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; +import { SmartAccountUpdateContent } from './smart-account-update-content'; + +const renderComponent = () => + renderWithProvider(, { + state: { + engine: { backgroundState }, + }, + }); + +describe('SmartContractWithLogo', () => { + it('renders correctly', () => { + const { getByText } = renderComponent(); + expect(getByText('Use smart account?')).toBeTruthy(); + expect(getByText('Faster transactions, lower fees')).toBeTruthy(); + expect(getByText('Pay with any token, any time')).toBeTruthy(); + expect(getByText('Same account, smarter features.')).toBeTruthy(); + }); +}); diff --git a/app/components/Views/confirmations/components/smart-account-update-content/smart-account-update-content.tsx b/app/components/Views/confirmations/components/smart-account-update-content/smart-account-update-content.tsx new file mode 100644 index 000000000000..041fbf095caf --- /dev/null +++ b/app/components/Views/confirmations/components/smart-account-update-content/smart-account-update-content.tsx @@ -0,0 +1,99 @@ +import React, { ReactElement } from 'react'; +import { Image, Linking, View } from 'react-native'; + +import { strings } from '../../../../../../locales/i18n'; +import AppConstants from '../../../../../core/AppConstants'; +import AvatarIcon from '../../../../../component-library/components/Avatars/Avatar/variants/AvatarIcon'; +import Text, { + TextColor, + TextVariant, +} from '../../../../../component-library/components/Texts/Text'; +import { IconName } from '../../../../../component-library/components/Icons/Icon'; +import { useTheme } from '../../../../../util/theme'; +import { useStyles } from '../../../../hooks/useStyles'; +import styleSheet from './smart-account-update-content.styles'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires, import/no-commonjs +const smartAccountUpdateImage = require('../../../../../images/smart-account-update.png'); + +const ListItem = ({ + iconName, + title, + description, + styles, +}: { + iconName: IconName; + title: string; + description: ReactElement; + styles: ReturnType; +}) => { + const { colors } = useTheme(); + return ( + + + + {title} + + {description} + + + + ); +}; + +export const SmartAccountUpdateContent = () => { + const { styles } = useStyles(styleSheet, {}); + + return ( + <> + + + {strings('confirm.7702_functionality.splashpage.splashTitle')} + + + + + + {strings( + 'confirm.7702_functionality.splashpage.featuresDescription', + )}{' '} + + Linking.openURL(AppConstants.URLS.SMART_ACCOUNTS) + } + > + {strings('alert_system.upgrade_account.learn_more')} + + + + } + styles={styles} + /> + + ); +}; diff --git a/app/components/Views/confirmations/components/smart-account-update-modal/index.ts b/app/components/Views/confirmations/components/smart-account-update-modal/index.ts new file mode 100644 index 000000000000..ac813ac5f874 --- /dev/null +++ b/app/components/Views/confirmations/components/smart-account-update-modal/index.ts @@ -0,0 +1 @@ +export { SmartAccountUpdateModal } from './smart-account-update-modal'; diff --git a/app/components/Views/confirmations/components/smart-account-update-modal/smart-account-update-modal.styles.ts b/app/components/Views/confirmations/components/smart-account-update-modal/smart-account-update-modal.styles.ts new file mode 100644 index 000000000000..e03500638ffa --- /dev/null +++ b/app/components/Views/confirmations/components/smart-account-update-modal/smart-account-update-modal.styles.ts @@ -0,0 +1,51 @@ +import { Theme } from '@metamask/design-tokens'; +import { StyleSheet } from 'react-native'; + +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + + return StyleSheet.create({ + bottomSheet: { + backgroundColor: theme.colors.background.alternative, + }, + wrapper: { + backgroundColor: theme.colors.background.alternative, + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'space-between', + height: 640, + width: '100%', + paddingInline: 8, + paddingTop: 20, + }, + actionIcon: { + position: 'absolute', + right: 10, + top: 8, + }, + button: { + alignSelf: 'center', + marginVertical: 12, + width: '90%', + }, + successWrapper: { + backgroundColor: theme.colors.background.alternative, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + height: 640, + width: '100%', + paddingInline: 8, + paddingTop: 20, + }, + successInner: { + height: '28%', + width: '80%', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'space-between', + }, + }); +}; + +export default styleSheet; diff --git a/app/components/Views/confirmations/components/smart-account-update-modal/smart-account-update-modal.test.tsx b/app/components/Views/confirmations/components/smart-account-update-modal/smart-account-update-modal.test.tsx new file mode 100644 index 000000000000..0e2f6f7c5cd1 --- /dev/null +++ b/app/components/Views/confirmations/components/smart-account-update-modal/smart-account-update-modal.test.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { fireEvent } from '@testing-library/react-native'; + +import Engine from '../../../../../core/Engine'; +import renderWithProvider from '../../../../../util/test/renderWithProvider'; +import { + getAppStateForConfirmation, + upgradeAccountConfirmation, +} from '../../../../../util/test/confirm-data-helpers'; +// eslint-disable-next-line import/no-namespace +import * as AddressUtils from '../../../../../util/address'; +import { SmartAccountUpdateModal } from './smart-account-update-modal'; + +jest.mock('react-native-safe-area-context', () => { + // using disting digits for mock rects to make sure they are not mixed up + const inset = { top: 1, right: 2, bottom: 3, left: 4 }; + const frame = { width: 5, height: 6, x: 7, y: 8 }; + return { + SafeAreaProvider: jest.fn().mockImplementation(({ children }) => children), + SafeAreaConsumer: jest + .fn() + .mockImplementation(({ children }) => children(inset)), + useSafeAreaInsets: jest.fn().mockImplementation(() => inset), + useSafeAreaFrame: jest.fn().mockImplementation(() => frame), + }; +}); + +jest.mock('../../../../hooks/AssetPolling/AssetPollingProvider', () => ({ + AssetPollingProvider: () => null, +})); + +jest.mock('../../../../../core/Engine', () => ({ + getTotalEvmFiatAccountBalance: () => ({ tokenFiat: 10 }), + context: { + PreferencesController: { + setSmartAccountOptIn: jest.fn(), + }, + }, +})); + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useNavigation: () => ({ + navigate: jest.fn(), + }), + }; +}); + +const renderComponent = (state?: Record) => + renderWithProvider(, { + state: + state ?? + getAppStateForConfirmation(upgradeAccountConfirmation, { + PreferencesController: { smartAccountOptIn: false }, + }), + }); + +describe('SmartAccountUpdateModal', () => { + beforeEach(() => { + jest.spyOn(AddressUtils, 'isHardwareAccount').mockReturnValue(false); + }); + + it('renders correctly', () => { + const { getByText } = renderComponent(); + expect(getByText('Use smart account?')).toBeTruthy(); + }); + + it('show success after `Yes` button is clicked', () => { + const { getByText, queryByText } = renderComponent(); + expect(getByText('Use smart account?')).toBeTruthy(); + fireEvent.press(getByText('Use smart account')); + expect( + Engine.context.PreferencesController.setSmartAccountOptIn, + ).toHaveBeenCalled(); + expect(queryByText('Use smart account?')).toBeNull(); + expect(getByText('Successful!')).toBeTruthy(); + }); +}); diff --git a/app/components/Views/confirmations/components/smart-account-update-modal/smart-account-update-modal.tsx b/app/components/Views/confirmations/components/smart-account-update-modal/smart-account-update-modal.tsx new file mode 100644 index 000000000000..be8fd48c55da --- /dev/null +++ b/app/components/Views/confirmations/components/smart-account-update-modal/smart-account-update-modal.tsx @@ -0,0 +1,43 @@ +import React, { useCallback, useState } from 'react'; +import { View } from 'react-native'; + +import { strings } from '../../../../../../locales/i18n'; +import Engine from '../../../../../core/Engine'; +import Button, { + ButtonSize, + ButtonVariants, +} from '../../../../../component-library/components/Buttons/Button'; +import BottomSheet from '../../../../../component-library/components/BottomSheets/BottomSheet'; +import { useStyles } from '../../../../hooks/useStyles'; +import { SmartAccountUpdateContent } from '../smart-account-update-content'; +import styleSheet from './smart-account-update-modal.styles'; +import { SmartAccountUpdateSuccess } from './smart-account-update-success'; + +export const SmartAccountUpdateModal = () => { + const { PreferencesController } = Engine.context; + const [acknowledged, setAcknowledged] = useState(false); + const { styles } = useStyles(styleSheet, {}); + + const onUpdate = useCallback(() => { + PreferencesController.setSmartAccountOptIn(true); + setAcknowledged(true); + }, [setAcknowledged]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + {acknowledged && } + {!acknowledged && ( + + +