diff --git a/app/components/Nav/Main/RootRPCMethodsUI.js b/app/components/Nav/Main/RootRPCMethodsUI.js index 2500c634865..eaf3b7f68b5 100644 --- a/app/components/Nav/Main/RootRPCMethodsUI.js +++ b/app/components/Nav/Main/RootRPCMethodsUI.js @@ -31,7 +31,7 @@ import { import { BN } from 'ethereumjs-util'; import Logger from '../../../util/Logger'; import Approve from '../../Views/ApproveView/Approve'; -import ApprovalFlowLoader from '../../UI/ApprovalFlowLoader'; +import ApprovalFlowLoader from '../../UI/Approval/ApprovalFlowLoader'; import WatchAssetRequest from '../../UI/WatchAssetRequest'; import AccountApproval from '../../UI/AccountApproval'; import TransactionTypes from '../../../core/TransactionTypes'; @@ -58,6 +58,8 @@ import { selectProviderType, } from '../../../selectors/networkController'; import { createAccountConnectNavDetails } from '../../Views/AccountConnect'; +import { ApprovalResult } from '../../UI/Approval/ApprovalResult'; +import { ApprovalResultType } from '../../UI/Approval/ApprovalResult/ApprovalResult'; const APPROVAL_TYPES_WITH_DISABLED_CLOSE_ON_APPROVE = [ ApprovalTypes.TRANSACTION, @@ -91,6 +93,8 @@ const RootRPCMethodsUI = (props) => { const [watchAsset, setWatchAsset] = useState(undefined); + const [approvalResultRequest, setApprovalResultRequest] = useState(undefined); + const [signMessageParams, setSignMessageParams] = useState(undefined); const setTransactionObject = props.setTransactionObject; @@ -403,6 +407,46 @@ const RootRPCMethodsUI = (props) => { ); + const onApprovalResultConfirm = () => { + setShowPendingApproval(false); + acceptPendingApproval(approvalResultRequest.id, approvalResultRequest.data); + setApprovalResultRequest(undefined); + }; + + const renderApprovalResultModal = () => { + if ( + ![ApprovalTypes.RESULT_SUCCESS, ApprovalTypes.RESULT_ERROR].includes( + showPendingApproval?.type, + ) + ) { + return null; + } + return ( + + + + ); + }; + const renderQRSigningModal = () => { const { isSigningQRObject, QRState } = props; @@ -789,6 +833,14 @@ const RootRPCMethodsUI = (props) => { origin: request.origin, }); break; + case ApprovalTypes.RESULT_SUCCESS: + case ApprovalTypes.RESULT_ERROR: + setApprovalResultRequest({ data: requestData, id: request.id }); + showPendingApprovalModal({ + type: request.type, + origin: request.origin, + }); + break; default: break; } @@ -850,6 +902,7 @@ const RootRPCMethodsUI = (props) => { {renderQRSigningModal()} {renderAccountsApprovalModal()} {renderApprovalFlowModal()} + {renderApprovalResultModal()} ); }; diff --git a/app/components/UI/ApprovalFlowLoader/__snapshots__/index.test.tsx.snap b/app/components/UI/Approval/ApprovalFlowLoader/__snapshots__/index.test.tsx.snap similarity index 100% rename from app/components/UI/ApprovalFlowLoader/__snapshots__/index.test.tsx.snap rename to app/components/UI/Approval/ApprovalFlowLoader/__snapshots__/index.test.tsx.snap diff --git a/app/components/UI/ApprovalFlowLoader/index.js b/app/components/UI/Approval/ApprovalFlowLoader/index.js similarity index 86% rename from app/components/UI/ApprovalFlowLoader/index.js rename to app/components/UI/Approval/ApprovalFlowLoader/index.js index acacd5a0ebc..637e05eb536 100644 --- a/app/components/UI/ApprovalFlowLoader/index.js +++ b/app/components/UI/Approval/ApprovalFlowLoader/index.js @@ -1,10 +1,10 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; import PropTypes from 'prop-types'; -import Device from '../../../util/device'; -import { useTheme } from '../../../util/theme'; -import Text from '../../Base/Text'; -import Spinner from '../AnimatedSpinner'; +import Device from '../../../../util/device'; +import { useTheme } from '../../../../util/theme'; +import Text from '../../../Base/Text'; +import Spinner from '../../AnimatedSpinner'; const createStyles = (colors) => StyleSheet.create({ diff --git a/app/components/UI/ApprovalFlowLoader/index.test.tsx b/app/components/UI/Approval/ApprovalFlowLoader/index.test.tsx similarity index 86% rename from app/components/UI/ApprovalFlowLoader/index.test.tsx rename to app/components/UI/Approval/ApprovalFlowLoader/index.test.tsx index dcc3f445e0f..2752a7d4836 100644 --- a/app/components/UI/ApprovalFlowLoader/index.test.tsx +++ b/app/components/UI/Approval/ApprovalFlowLoader/index.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import ApprovalFlowLoader from './'; +import ApprovalFlowLoader from '.'; describe('ApprovalFlowLoader', () => { it('should render correctly', () => { diff --git a/app/components/UI/Approval/ApprovalResult/ApprovalResult.styles.ts b/app/components/UI/Approval/ApprovalResult/ApprovalResult.styles.ts new file mode 100644 index 00000000000..1f2c1c087ec --- /dev/null +++ b/app/components/UI/Approval/ApprovalResult/ApprovalResult.styles.ts @@ -0,0 +1,60 @@ +import { StyleSheet } from 'react-native'; +import { Theme } from '../../../../util/theme/models'; +import Device from '../../../../util/device'; + +/** + * + * @param params Style sheet params. + * @param params.theme App theme from ThemeContext. + * @param params.vars Inputs that the style sheet depends on. + * @returns StyleSheet object. + */ +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + const { colors } = theme; + return StyleSheet.create({ + root: { + backgroundColor: colors.background.default, + paddingTop: 24, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + minHeight: 200, + paddingBottom: Device.isIphoneX() ? 20 : 0, + }, + accountCardWrapper: { + paddingHorizontal: 24, + }, + actionContainer: { + flex: 0, + paddingVertical: 16, + justifyContent: 'center', + }, + description: { + textAlign: 'center', + paddingBottom: 16, + }, + snapCell: { + marginVertical: 16, + }, + snapPermissionContainer: { + maxHeight: 300, + borderWidth: 1, + borderRadius: 8, + borderColor: colors.border.muted, + }, + iconContainer: { + justifyContent: 'center', + alignItems: 'center', + }, + iconWrapper: { + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: colors.success.muted, + justifyContent: 'center', + alignItems: 'center', + }, + }); +}; + +export default styleSheet; diff --git a/app/components/UI/Approval/ApprovalResult/ApprovalResult.test.tsx b/app/components/UI/Approval/ApprovalResult/ApprovalResult.test.tsx new file mode 100644 index 00000000000..77b43028b46 --- /dev/null +++ b/app/components/UI/Approval/ApprovalResult/ApprovalResult.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import ApprovalResult, { ApprovalResultType } from './ApprovalResult'; + +describe('ApprovalResult', () => { + const mockProps = { + requestData: { + message: 'Success message', + }, + onConfirm: jest.fn(), + requestType: ApprovalResultType.Success, + }; + + it('renders approval result with success type', () => { + const wrapper = render(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('renders approval result with error type', () => { + const errorMockProps = { + ...mockProps, + requestData: { + error: 'Error message', + }, + requestType: ApprovalResultType.Failure, + }; + + const wrapper = render(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/Approval/ApprovalResult/ApprovalResult.tsx b/app/components/UI/Approval/ApprovalResult/ApprovalResult.tsx new file mode 100644 index 00000000000..d2b94f49065 --- /dev/null +++ b/app/components/UI/Approval/ApprovalResult/ApprovalResult.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { View } from 'react-native'; +import stylesheet from './ApprovalResult.styles'; +import { strings } from '../../../../../locales/i18n'; +import SheetHeader from '../../../../component-library/components/Sheet/SheetHeader'; +import Text, { + TextVariant, +} from '../../../../component-library/components/Texts/Text'; +import Icon, { + IconColor, + IconName, + IconSize, +} from '../../../../component-library/components/Icons/Icon'; +import { + ButtonSize, + ButtonVariants, +} from '../../../../component-library/components/Buttons/Button'; +import BottomSheetFooter, { + ButtonsAlignment, +} from '../../../../component-library/components/BottomSheets/BottomSheetFooter'; +import { ButtonProps } from '../../../../component-library/components/Buttons/Button/Button.types'; +import { useStyles } from '../../../hooks/useStyles'; + +export enum ApprovalResultType { + Success = 'success', + Failure = 'failure', +} + +export interface ApprovalResultData { + message?: string; + error?: string; + header?: unknown; +} + +export interface ApprovalResultProps { + requestData: ApprovalResultData; + onConfirm: () => void; + requestType: ApprovalResultType; +} +const isApprovalResultTypeSuccess = (type: string) => + ApprovalResultType.Success === type; + +const processMessage = ( + requestData: ApprovalResultData, + requestType: ApprovalResultType, +) => { + if (isApprovalResultTypeSuccess(requestType)) { + return ( + requestData?.message ?? + strings('approval_result.resultPageSuccessDefaultMessage') + ); + } + return ( + requestData?.error ?? + strings('approval_result.resultPageErrorDefaultMessage') + ); +}; + +const ApprovalResult = ({ + requestData, + onConfirm, + requestType, +}: ApprovalResultProps) => { + const { styles } = useStyles(stylesheet, {}); + + const okButtonProps: ButtonProps = { + variant: ButtonVariants.Primary, + label: strings('approval_result.ok'), + size: ButtonSize.Lg, + onPress: onConfirm, + }; + + return ( + + + + + + + + + + {processMessage(requestData, requestType)} + + + + + + + ); +}; + +export default ApprovalResult; diff --git a/app/components/UI/Approval/ApprovalResult/__snapshots__/ApprovalResult.test.tsx.snap b/app/components/UI/Approval/ApprovalResult/__snapshots__/ApprovalResult.test.tsx.snap new file mode 100644 index 00000000000..c927307a618 --- /dev/null +++ b/app/components/UI/Approval/ApprovalResult/__snapshots__/ApprovalResult.test.tsx.snap @@ -0,0 +1,349 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApprovalResult renders approval result with error type 1`] = ` + + + + + + + + + + + Error + + + + + Error message + + + + + + OK + + + + + + +`; + +exports[`ApprovalResult renders approval result with success type 1`] = ` + + + + + + + + + + + Success + + + + + Success message + + + + + + OK + + + + + + +`; diff --git a/app/components/UI/Approval/ApprovalResult/index.ts b/app/components/UI/Approval/ApprovalResult/index.ts new file mode 100644 index 00000000000..b7f534d9a4a --- /dev/null +++ b/app/components/UI/Approval/ApprovalResult/index.ts @@ -0,0 +1,4 @@ +/* eslint-disable import/prefer-default-export */ +import ApprovalResult from './ApprovalResult'; + +export { ApprovalResult }; diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index ee5f185c1ed..b25cdc7acc1 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -43,6 +43,8 @@ export enum ApprovalTypes { ETH_SIGN_TYPED_DATA = 'eth_signTypedData', WATCH_ASSET = 'wallet_watchAsset', TRANSACTION = 'transaction', + RESULT_ERROR = 'result_error', + RESULT_SUCCESS = 'result_success', } interface RPCMethodsMiddleParameters { diff --git a/locales/languages/en.json b/locales/languages/en.json index 83b21410a38..f261d7b42cc 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -430,6 +430,13 @@ "cta_no_thanks": "No thanks", "cta_i_agree": "I agree" }, + "approval_result": { + "ok": "OK", + "success": "Success", + "error": "Error", + "resultPageSuccessDefaultMessage": "The operation completed successfully.", + "resultPageErrorDefaultMessage": "The operation failed." + }, "token": { "token_symbol": "Token Symbol", "token_address": "Token Address", diff --git a/package.json b/package.json index 0946bbb931e..1ee004d50d7 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "react-native-level-fs/**/semver": "^4.3.2", "@metamask/contract-metadata": "^2.1.0", "@metamask/controller-utils": "~3.0.0", - "@metamask/approval-controller": "3.3.0", + "@metamask/approval-controller": "3.4.0", "@exodus/react-native-payments/validator": "^13.7.0", "react-devtools-core": "4.22.1", "**/got": "^11.8.5", @@ -153,7 +153,7 @@ "@keystonehq/metamask-airgapped-keyring": "^0.3.0", "@keystonehq/ur-decoder": "^0.6.1", "@metamask/address-book-controller": "^2.0.0", - "@metamask/approval-controller": "^3.3.0", + "@metamask/approval-controller": "^3.4.0", "@metamask/assets-controllers": "5.0.0", "@metamask/base-controller": "^2.0.0", "@metamask/composable-controller": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index cede7eea116..da4119a4a26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5107,10 +5107,10 @@ "@metamask/base-controller" "^2.0.0" "@metamask/controller-utils" "^3.0.0" -"@metamask/approval-controller@3.3.0", "@metamask/approval-controller@^2.0.0", "@metamask/approval-controller@^2.1.1", "@metamask/approval-controller@^3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@metamask/approval-controller/-/approval-controller-3.3.0.tgz#8e98de054ea92af82e50d90d300dc5222c444f3b" - integrity sha512-UlnNWm/mewteLbVNv+e4yepPkwov1cxHGtJWa5OKN+jE8ZoxX1MGUFBr6G4JczyOmv6JSR03mx0jP7T04ZN0nw== +"@metamask/approval-controller@3.4.0", "@metamask/approval-controller@^2.0.0", "@metamask/approval-controller@^2.1.1", "@metamask/approval-controller@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@metamask/approval-controller/-/approval-controller-3.4.0.tgz#282900361d42f785578728b45014ff8cb5e557ea" + integrity sha512-DjqrhiX9+W/Fh6Crr7FPJ87Y/uhPWzBvfXGtekv1LHZNmEtUxkrA7aelddUM0fpTdURIGT4aNGBoQudFidc+Lw== dependencies: "@metamask/base-controller" "^3.0.0" "@metamask/utils" "^5.0.2"