diff --git a/.depcheckrc.yml b/.depcheckrc.yml index 5a506bc7a51..e8fe7a446ec 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -4,6 +4,8 @@ ignores: - 'webpack-cli' - '@react-native-community/datetimepicker' - '@react-native-community/slider' + - 'patch-package' + - '@lavamoat/allow-scripts' # This is used on the patch for TokenRatesController of Assets controllers, for we to be able to use the last version of it - cockatiel diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f13a32ae47f..64bd5bea831 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,16 +5,22 @@ on: pull_request: merge_group: types: [checks_requested] - + jobs: - setup: - runs-on: ubuntu-20.04 + check-diff: + runs-on: macos-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version-file: '.nvmrc' cache: yarn + - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 #v1 + with: + ruby-version: '3.1.5' + bundler-cache: true + env: + BUNDLE_GEMFILE: ios/Gemfile - name: Determine whether the current PR is a draft id: set-is-draft if: github.event_name == 'pull_request' && github.event.pull_request.number @@ -27,7 +33,7 @@ jobs: run: printf '%s\n\n%s' '@metamask:registry=https://npm.pkg.github.com' "//npm.pkg.github.com/:_authToken=${PACKAGE_READ_TOKEN}" > .npmrc env: PACKAGE_READ_TOKEN: ${{ secrets.PACKAGE_READ_TOKEN }} - - run: yarn setup --node + - run: yarn setup - name: Require clean working directory shell: bash run: | @@ -39,7 +45,6 @@ jobs: fi dedupe: runs-on: ubuntu-20.04 - needs: setup steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -58,7 +63,6 @@ jobs: fi scripts: runs-on: ubuntu-20.04 - needs: setup strategy: matrix: scripts: @@ -86,7 +90,6 @@ jobs: fi unit-tests: runs-on: ubuntu-20.04 - needs: setup strategy: matrix: shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -156,7 +159,6 @@ jobs: js-bundle-size-check: runs-on: ubuntu-20.04 - needs: setup steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -220,6 +222,10 @@ jobs: uses: actions/checkout@v3 - name: SonarCloud Quality Gate Status id: sonar-status + env: + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Skip step if event is a PR if [[ "${{ github.event_name }}" != "pull_request" ]]; then @@ -227,31 +233,40 @@ jobs: exit 0 fi - sleep 30 + # Bypass step if skip-sonar-cloud label is found + LABEL=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels" | \ + jq -r '.[] | select(.name=="skip-sonar-cloud") | .name') - PROJECT_KEY="metamask-mobile" - PR_NUMBER="${{ github.event.pull_request.number }}" - SONAR_TOKEN="${{ secrets.SONAR_TOKEN }}" + if [[ "$LABEL" == "skip-sonar-cloud" ]]; then + echo "skip-sonar-cloud label found. Skipping SonarCloud Quality Gate check." + else + sleep 30 - if [ -z "$PR_NUMBER" ]; then - echo "No pull request number found. Failing the check." - exit 1 - fi + PROJECT_KEY="metamask-mobile" + PR_NUMBER="${{ github.event.pull_request.number }}" + SONAR_TOKEN="${{ secrets.SONAR_TOKEN }}" - RESPONSE=$(curl -s -u "$SONAR_TOKEN:" \ - "https://sonarcloud.io/api/qualitygates/project_status?projectKey=$PROJECT_KEY&pullRequest=$PR_NUMBER") - echo "SonarCloud API Response: $RESPONSE" + if [ -z "$PR_NUMBER" ]; then + echo "No pull request number found. Failing the check." + exit 1 + fi - STATUS=$(echo "$RESPONSE" | jq -r '.projectStatus.status') + RESPONSE=$(curl -s -u "$SONAR_TOKEN:" \ + "https://sonarcloud.io/api/qualitygates/project_status?projectKey=$PROJECT_KEY&pullRequest=$PR_NUMBER") + echo "SonarCloud API Response: $RESPONSE" - if [[ "$STATUS" == "ERROR" ]]; then - echo "Quality Gate failed." - exit 1 - elif [[ "$STATUS" == "OK" ]]; then - echo "Quality Gate passed." - else - echo "Could not determine Quality Gate status." - exit 1 + STATUS=$(echo "$RESPONSE" | jq -r '.projectStatus.status') + + if [[ "$STATUS" == "ERROR" ]]; then + echo "Quality Gate failed." + exit 1 + elif [[ "$STATUS" == "OK" ]]; then + echo "Quality Gate passed." + else + echo "Could not determine Quality Gate status." + exit 1 + fi fi check-workflows: name: Check workflows @@ -270,12 +285,11 @@ jobs: runs-on: ubuntu-20.04 needs: [ - setup, + check-diff, dedupe, scripts, unit-tests, check-workflows, - sonar-cloud, js-bundle-size-check, sonar-cloud-quality-gate-status, ] diff --git a/.github/workflows/crowdin_action.yml b/.github/workflows/crowdin_action.yml index 2582db7e5a7..eba1ac69154 100644 --- a/.github/workflows/crowdin_action.yml +++ b/.github/workflows/crowdin_action.yml @@ -10,8 +10,6 @@ on: - main schedule: - cron: "0 */12 * * *" - merge_group: - types: [checks_requested] jobs: synchronize-with-crowdin: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f577c5ba755..a95001cb1c6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -4,21 +4,20 @@ on: branches: main pull_request: - jobs: docker: runs-on: ubuntu-latest steps: - name: Set up QEMU - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3 + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 - uses: actions/checkout@v3 - name: Build and load - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 with: context: . file: scripts/docker/Dockerfile diff --git a/.storybook/main.js b/.storybook/main.js index 64a3f3cf602..fd51e44c137 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -3,6 +3,7 @@ module.exports = { '../app/component-library/components/**/*.stories.?(ts|tsx|js|jsx)', '../app/component-library/base-components/**/*.stories.?(ts|tsx|js|jsx)', '../app/component-library/components-temp/TagColored/**/*.stories.?(ts|tsx|js|jsx)', + '../app/component-library/components-temp/KeyValueRow/**/*.stories.?(ts|tsx|js|jsx)', ], addons: ['@storybook/addon-ondevice-controls'], framework: '@storybook/react-native', diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index ae2e8f2d55b..3b26602e64d 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -30,6 +30,13 @@ global.STORIES = [ importPathMatcher: "^\\.[\\\\/](?:app\\/component-library\\/components-temp\\/TagColored(?:\\/(?!\\.)(?:(?:(?!(?:^|\\/)\\.).)*?)\\/|\\/|$)(?!\\.)(?=.)[^/]*?\\.stories\\.(?:ts|tsx|js|jsx)?)$", }, + { + titlePrefix: "", + directory: "./app/component-library/components-temp/KeyValueRow", + files: "**/*.stories.?(ts|tsx|js|jsx)", + importPathMatcher: + "^\\.[\\\\/](?:app\\/component-library\\/components-temp\\/KeyValueRow(?:\\/(?!\\.)(?:(?:(?!(?:^|\\/)\\.).)*?)\\/|\\/|$)(?!\\.)(?=.)[^/]*?\\.stories\\.(?:ts|tsx|js|jsx)?)$", + }, ]; import "@storybook/addon-ondevice-controls/register"; @@ -115,8 +122,7 @@ const getStories = () => { "./app/component-library/components/Toast/Toast.stories.tsx": require("../app/component-library/components/Toast/Toast.stories.tsx"), "./app/component-library/base-components/TagBase/TagBase.stories.tsx": require("../app/component-library/base-components/TagBase/TagBase.stories.tsx"), "./app/component-library/components-temp/TagColored/TagColored.stories.tsx": require("../app/component-library/components-temp/TagColored/TagColored.stories.tsx"), - "./app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.stories.tsx": require("../app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.stories.tsx"), - + "./app/component-library/components-temp/KeyValueRow/KeyValueRow.stories.tsx": require("../app/component-library/components-temp/KeyValueRow/KeyValueRow.stories.tsx"), }; }; diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 3c02108f3ce..00000000000 --- a/Gemfile +++ /dev/null @@ -1,10 +0,0 @@ -source 'https://rubygems.org' - -# Recommended to use http://rbenv.org/ to install and use this version -ruby '>= 3.1.5' - -# Allow minor version updates up to but excluding 2.0.0 -gem 'cocoapods', '~> 1.12' - -# Allow all version updates up to but excluding 7.1.0 -gem 'activesupport', '>= 6.1.7.3', '< 7.1.0' diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueLabel/KeyValueLabel.styles.tsx b/app/component-library/components-temp/KeyValueRow/KeyValueLabel/KeyValueLabel.styles.tsx new file mode 100644 index 00000000000..ae5c4860b71 --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueLabel/KeyValueLabel.styles.tsx @@ -0,0 +1,11 @@ +import { StyleSheet } from 'react-native'; + +const styleSheet = () => + StyleSheet.create({ + labelContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + }); + +export default styleSheet; diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueLabel/KeyValueLabel.tsx b/app/component-library/components-temp/KeyValueRow/KeyValueLabel/KeyValueLabel.tsx new file mode 100644 index 00000000000..aefa71eecbc --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueLabel/KeyValueLabel.tsx @@ -0,0 +1,64 @@ +import ButtonIcon from '../../../../component-library/components/Buttons/ButtonIcon'; +import Label from '../../../../component-library/components/Form/Label'; +import { + IconColor, + IconName, +} from '../../../../component-library/components/Icons/Icon'; +import { + TextVariant, + TextColor, +} from '../../../../component-library/components/Texts/Text'; +import { useStyles } from '../../../../component-library/hooks'; +import useTooltipModal from '../../../../components/hooks/useTooltipModal'; +import React from 'react'; +import { View } from 'react-native'; +import { KeyValueRowLabelProps, TooltipSizes } from '../KeyValueRow.types'; +import styleSheet from './KeyValueLabel.styles'; + +/** + * A label and tooltip component. + * + * @param {Object} props - Component props. + * @param {TextVariant} [props.variant] - Optional text variant. Defaults to TextVariant.BodyMDMedium. + * @param {TextVariant} [props.color] - Optional text color. Defaults to TextColor.Default. + * @param {TextVariant} [props.tooltip] - Optional tooltip to render to the right of the label text. + * + * @returns {JSX.Element} The rendered KeyValueRowLabel component. + */ +const KeyValueRowLabel = ({ + label, + variant = TextVariant.BodyMDMedium, + color = TextColor.Default, + tooltip, +}: KeyValueRowLabelProps) => { + const { styles } = useStyles(styleSheet, {}); + + const { openTooltipModal } = useTooltipModal(); + + const hasTooltip = tooltip?.title && tooltip?.text; + + const onNavigateToTooltipModal = () => { + if (!hasTooltip) return; + openTooltipModal(tooltip.title, tooltip.text); + }; + + return ( + + + {hasTooltip && ( + + )} + + ); +}; + +export default KeyValueRowLabel; diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueRoot/KeyValueRoot.styles.tsx b/app/component-library/components-temp/KeyValueRow/KeyValueRoot/KeyValueRoot.styles.tsx new file mode 100644 index 00000000000..a3de4af351f --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueRoot/KeyValueRoot.styles.tsx @@ -0,0 +1,13 @@ +import { StyleSheet } from 'react-native'; + +const styleSheet = () => + StyleSheet.create({ + rootContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + overflow: 'hidden', + }, + }); + +export default styleSheet; diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueRoot/KeyValueRoot.tsx b/app/component-library/components-temp/KeyValueRow/KeyValueRoot/KeyValueRoot.tsx new file mode 100644 index 00000000000..e308488648a --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueRoot/KeyValueRoot.tsx @@ -0,0 +1,37 @@ +import { useStyles } from '../../../hooks'; +import React from 'react'; +import { View } from 'react-native'; +import { KeyValueRowRootProps } from '../KeyValueRow.types'; +import styleSheet from './KeyValueRoot.styles'; + +/** + * The main container for the KeyValueRow component. + * When creating custom KeyValueRow components, this must be the outermost component wrapping the two components. + * + * e.g. + * ``` + * + * + * + * + * ``` + * + * @component + * @param {Object} props - Component props. + * @param {Array} props.children - The two children. + * @param {ViewProps} [props.style] - Optional styling + * + * @returns {JSX.Element} The rendered Root component. + */ +const KeyValueRowRoot = ({ + children, + style: customStyles, +}: KeyValueRowRootProps) => { + const { styles: defaultStyles } = useStyles(styleSheet, {}); + + const styles = [defaultStyles.rootContainer, customStyles]; + + return {children}; +}; + +export default KeyValueRowRoot; diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueRow.stories.tsx b/app/component-library/components-temp/KeyValueRow/KeyValueRow.stories.tsx new file mode 100644 index 00000000000..e5a440b3276 --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueRow.stories.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { withNavigation } from '../../../../storybook/decorators'; +import { View, StyleSheet } from 'react-native'; +import KeyValueRowComponent, { + KeyValueRowFieldIconSides, + TooltipSizes, +} from './index'; +import Text, { TextColor, TextVariant } from '../../components/Texts/Text'; +import Title from '../../../components/Base/Title'; +import { IconColor, IconName, IconSize } from '../../components/Icons/Icon'; + +const KeyValueRowMeta = { + title: 'Components Temp / KeyValueRow', + component: KeyValueRowComponent, + decorators: [withNavigation], +}; + +export default KeyValueRowMeta; + +const styles = StyleSheet.create({ + container: { + padding: 16, + }, + listItem: { + marginVertical: 16, + gap: 16, + }, +}); + +export const KeyValueRow = { + render: () => ( + + KeyValueRow Component + + Prebuilt component displayed below but KeyValueRow stubs are available + to create new KeyValueRow variants. + + + + + + + + + ), +}; diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueRow.styles.ts b/app/component-library/components-temp/KeyValueRow/KeyValueRow.styles.ts new file mode 100644 index 00000000000..33999c44819 --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueRow.styles.ts @@ -0,0 +1,12 @@ +import { StyleSheet } from 'react-native'; + +const styleSheet = () => + StyleSheet.create({ + flexRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + }); + +export default styleSheet; diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueRow.test.tsx b/app/component-library/components-temp/KeyValueRow/KeyValueRow.test.tsx new file mode 100644 index 00000000000..a9b00ece7e8 --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueRow.test.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import KeyValueRow from './KeyValueRow'; +import { IconName } from '../../components/Icons/Icon'; + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useNavigation: () => ({ + navigate: jest.fn(), + }), + }; +}); + +describe('KeyValueRow', () => { + describe('Prebuilt Component', () => { + describe('KeyValueRow', () => { + it('should render when there is only text', () => { + const { toJSON } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('should render text with tooltips', () => { + const { toJSON } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('should render text with icons', () => { + const { toJSON } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('should render text with icons and tooltips', () => { + const { toJSON } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueRow.tsx b/app/component-library/components-temp/KeyValueRow/KeyValueRow.tsx new file mode 100644 index 00000000000..18c6300b1cf --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueRow.tsx @@ -0,0 +1,93 @@ +import { useStyles } from '../../hooks'; +import React from 'react'; +import stylesheet from './KeyValueRow.styles'; +import { + KeyValueRowProps, + KeyValueRowFieldIconSides, + KeyValueRowSectionAlignments, +} from './KeyValueRow.types'; +import Icon from '../../components/Icons/Icon'; +import { View } from 'react-native'; +import { areKeyValueRowPropsEqual } from './KeyValueRow.utils'; +import KeyValueSection from './KeyValueSection/KeyValueSection'; +import KeyValueRowLabel from './KeyValueLabel/KeyValueLabel'; +import KeyValueRowRoot from './KeyValueRoot/KeyValueRoot'; + +/** + * Prebuilt convenience component to format and render a key/value KeyValueRowLabel pair. + * The KeyValueRowLabel component has props to display a tooltip and icon. + * + * Examples are in the Storybook: [StorybookLink](./KeyValueRow.stories.tsx) + * + * @param {Object} props - Component props + * @param {KeyValueRowField} props.field - Represents the left side of the key value row pair + * @param {KeyValueRowField} props.value - Represents the right side of the key value row pair + * @param {ViewProps} [props.style] - Optional styling + * + * @returns {JSX.Element} The rendered KeyValueRow component. + */ +const KeyValueRow = React.memo(({ field, value, style }: KeyValueRowProps) => { + const { styles } = useStyles(stylesheet, {}); + + // Field (left side) + const fieldIcon = field?.icon; + const shouldShowFieldIcon = fieldIcon?.name; + + // Value (right side) + const valueIcon = value?.icon; + const shouldShowValueIcon = valueIcon?.name; + + return ( + + + + {shouldShowFieldIcon && + (fieldIcon.side === KeyValueRowFieldIconSides.LEFT || + fieldIcon.side === KeyValueRowFieldIconSides.BOTH || + !fieldIcon?.side) && } + + {shouldShowFieldIcon && + (fieldIcon?.side === KeyValueRowFieldIconSides.RIGHT || + fieldIcon?.side === KeyValueRowFieldIconSides.BOTH) && ( + + )} + + + + + {shouldShowValueIcon && + (valueIcon?.side === KeyValueRowFieldIconSides.LEFT || + valueIcon?.side === KeyValueRowFieldIconSides.BOTH || + !valueIcon?.side) && } + + {shouldShowValueIcon && + (valueIcon?.side === KeyValueRowFieldIconSides.RIGHT || + valueIcon?.side === KeyValueRowFieldIconSides.BOTH) && ( + + )} + + + + ); +}, areKeyValueRowPropsEqual); + +/** + * Exported sub-components to provide a base for new KeyValueRow variants. + */ +export const KeyValueRowStubs = { + Root: KeyValueRowRoot, + Section: KeyValueSection, + Label: KeyValueRowLabel, +}; + +export default KeyValueRow; diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueRow.types.ts b/app/component-library/components-temp/KeyValueRow/KeyValueRow.types.ts new file mode 100644 index 00000000000..0e9c864d309 --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueRow.types.ts @@ -0,0 +1,157 @@ +import { + IconProps, + IconSize, +} from '../../../component-library/components/Icons/Icon'; +import { ButtonIconSizes } from '../../components/Buttons/ButtonIcon'; +import { ReactNode } from 'react'; +import { TextProps } from '../../components/Texts/Text/Text.types'; +import { ViewProps } from 'react-native'; + +/** + * The optional tooltip tha can be displayed within a KeyValueRowField or KeyValueRowLabel. + * + * @see KeyValueRowField + * @see KeyValueRowLabel + */ +interface KeyValueRowTooltip { + /** + * The title displayed at the top of the tooltip. + */ + title: string; + /** + * The text displayed within the tooltip body. + */ + text: string; + /** + * Optional size of the tooltip icon. + * @default TooltipSizes.Md + */ + size?: ButtonIconSizes; +} + +/** + * Used to position icon in KeyValueRowField + * + * @see KeyValueRowField + */ +export enum KeyValueRowFieldIconSides { + LEFT = 'LEFT', + RIGHT = 'RIGHT', + BOTH = 'BOTH', +} + +/** + * Represents a field displayed within KeyValueRowProps. + * + * @see KeyValueRowProps + */ +interface KeyValueRowField { + /** + * The text to display. + */ + text: string; + /** + * Optional text variant. + * @default TextVariant.BodyMDMedium + */ + variant?: TextProps['variant']; + /** + * Optional text color. + * @default TextColor.Default + */ + color?: TextProps['color']; + /** + * Optional icon to display. If undefined, no icon is displayed. + */ + icon?: IconProps & { side?: KeyValueRowFieldIconSides }; + /** + * Optional tooltip to display. If undefined, no tooltip is displayed. + */ + tooltip?: KeyValueRowTooltip; +} + +export const IconSizes = IconSize; + +export const TooltipSizes = ButtonIconSizes; + +/** + * The KeyValueRowLabel prop interface. + * + * @see KeyValueRowLabel in ./KeyValueRow.tsx + */ +export interface KeyValueRowLabelProps { + /** + * Text to display. + */ + label: string; + /** + * Optional text variant. + * @default TextVariant.BodyMDMedium + */ + variant?: TextProps['variant']; + /** + * Optional text color. + * @default TextColor.Default + */ + color?: TextProps['color']; + /** + * Optional tooltip. If undefined, the tooltip won't be displayed. + */ + tooltip?: KeyValueRowTooltip; +} + +/** + * Represents the main container for the KeyValueRow component. + */ +export interface KeyValueRowRootProps { + /** + * Must have exactly two children. Adding more will lead to an undesired outcome. + */ + children: [ReactNode, ReactNode]; + /** + * Optional styles. Useful for controlling padding and margins. + */ + style?: ViewProps['style']; +} + +/** + * Represents the valid KeyValueSection alignments. + */ +export enum KeyValueRowSectionAlignments { + LEFT = 'flex-start', + RIGHT = 'flex-end', +} + +/** + * The KeyValueSection component props. + */ +export interface KeyValueSectionProps { + /** + * Child components. + */ + children: ReactNode; + /** + * Optional content alignment. + * @default KeyValueRowSectionAlignments.RIGHT + */ + align?: KeyValueRowSectionAlignments; +} + +/** + * The KeyValueRow component props. + */ +export interface KeyValueRowProps { + /** + * The "key" portion of the KeyValueRow (left side). + * Using the variable name field because key is reserved. + */ + field: KeyValueRowField; + /** + * The "value" portion of the KeyValueRow (right side). + */ + value: KeyValueRowField; + /** + * Optional styles. E.g. specifying padding or margins. + */ + style?: ViewProps['style']; +} diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueRow.utils.ts b/app/component-library/components-temp/KeyValueRow/KeyValueRow.utils.ts new file mode 100644 index 00000000000..bf4c38843e4 --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueRow.utils.ts @@ -0,0 +1,8 @@ +import { KeyValueRowProps } from './KeyValueRow.types'; + +export const areKeyValueRowPropsEqual = ( + prevProps: KeyValueRowProps, + newProps: KeyValueRowProps, +) => + JSON.stringify(prevProps.field) === JSON.stringify(newProps.field) && + JSON.stringify(prevProps.value) === JSON.stringify(newProps.value); diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueSection/KeyValueSection.styles.tsx b/app/component-library/components-temp/KeyValueRow/KeyValueSection/KeyValueSection.styles.tsx new file mode 100644 index 00000000000..f8d8adf679d --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueSection/KeyValueSection.styles.tsx @@ -0,0 +1,10 @@ +import { StyleSheet } from 'react-native'; + +const styleSheet = () => + StyleSheet.create({ + keyValueSectionContainer: { + flex: 1, + }, + }); + +export default styleSheet; diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueSection/KeyValueSection.tsx b/app/component-library/components-temp/KeyValueRow/KeyValueSection/KeyValueSection.tsx new file mode 100644 index 00000000000..7c01d2b438a --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/KeyValueSection/KeyValueSection.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { useStyles } from '../../../hooks'; +import { View } from 'react-native'; +import { + KeyValueRowSectionAlignments, + KeyValueSectionProps, +} from '../KeyValueRow.types'; +import stylesSheet from './KeyValueSection.styles'; + +/** + * A container representing either the left or right side of the KeyValueRow. + * For desired results, use only two components within the . + * + * @component + * @param {Object} props - Component props. + * @param {ReactNode} props.children - The child components. + * @param {KeyValueRowSectionAlignments} [props.align] - The alignment of the KeyValueSection. Defaults to KeyValueRowSectionAlignments.RIGHT + * + * @returns {JSX.Element} The rendered KeyValueSection component. + */ +const KeyValueSection = ({ + children, + align = KeyValueRowSectionAlignments.LEFT, +}: KeyValueSectionProps) => { + const { styles } = useStyles(stylesSheet, {}); + + return ( + + {children} + + ); +}; + +export default KeyValueSection; diff --git a/app/component-library/components-temp/KeyValueRow/__snapshots__/KeyValueRow.test.tsx.snap b/app/component-library/components-temp/KeyValueRow/__snapshots__/KeyValueRow.test.tsx.snap new file mode 100644 index 00000000000..c83d3a4fd5f --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/__snapshots__/KeyValueRow.test.tsx.snap @@ -0,0 +1,609 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KeyValueRow Prebuilt Component KeyValueRow should render text with icons 1`] = ` + + + + + + + Key Text + + + + + + + + + + Value Text + + + + + +`; + +exports[`KeyValueRow Prebuilt Component KeyValueRow should render text with icons and tooltips 1`] = ` + + + + + + + Key Text + + + + + + + + + + + + + Value Text + + + + + + + + +`; + +exports[`KeyValueRow Prebuilt Component KeyValueRow should render text with tooltips 1`] = ` + + + + + + Key Text + + + + + + + + + + + + Value Text + + + + + + + + +`; + +exports[`KeyValueRow Prebuilt Component KeyValueRow should render when there is only text 1`] = ` + + + + + + Sample Key Text + + + + + + + + + Sample Value Text + + + + + +`; diff --git a/app/component-library/components-temp/KeyValueRow/index.tsx b/app/component-library/components-temp/KeyValueRow/index.tsx new file mode 100644 index 00000000000..ee4a0c18825 --- /dev/null +++ b/app/component-library/components-temp/KeyValueRow/index.tsx @@ -0,0 +1,8 @@ +export { KeyValueRowStubs, default } from './KeyValueRow'; +export { + KeyValueRowSectionAlignments, + KeyValueRowFieldIconSides, + TooltipSizes, + IconSizes, +} from './KeyValueRow.types'; +export type * from './KeyValueRow.types'; diff --git a/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx b/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx index 5a4c293777f..ce9a9ab9014 100644 --- a/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx +++ b/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx @@ -51,7 +51,7 @@ const PickerAccount: React.ForwardRefRenderFunction< /> {accountName} diff --git a/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap b/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap index f2c7d176b13..0afdb8affeb 100644 --- a/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap +++ b/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap @@ -178,11 +178,11 @@ exports[`PickerAccount should render correctly 1`] = ` style={ { "color": "#141618", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 16, - "fontWeight": "400", + "fontFamily": "EuclidCircularB-Medium", + "fontSize": 14, + "fontWeight": "500", "letterSpacing": 0, - "lineHeight": 24, + "lineHeight": 22, } } testID="account-label" @@ -207,7 +207,7 @@ exports[`PickerAccount should render correctly 1`] = ` diff --git a/app/component-library/components/Pickers/PickerBase/__snapshots__/PickerBase.test.tsx.snap b/app/component-library/components/Pickers/PickerBase/__snapshots__/PickerBase.test.tsx.snap index 6d3ac0d242b..f93ccf2a438 100644 --- a/app/component-library/components/Pickers/PickerBase/__snapshots__/PickerBase.test.tsx.snap +++ b/app/component-library/components/Pickers/PickerBase/__snapshots__/PickerBase.test.tsx.snap @@ -17,7 +17,7 @@ exports[`PickerBase should render correctly 1`] = ` > +interface SelectorButtonProps { + onPress: (event: GestureResponderEvent) => void; + disabled?: boolean; + children: React.ReactNode; +} + +const createStyles = (colors: Theme['colors']) => StyleSheet.create({ container: { backgroundColor: colors.background.alternative, @@ -23,7 +29,7 @@ const createStyles = (colors) => }, }); -function SelectorButton({ onPress, disabled, children, ...props }) { +const SelectorButton: React.FC = ({ onPress, disabled, children, ...props }) => { const { colors } = useTheme(); const styles = createStyles(colors); @@ -35,12 +41,6 @@ function SelectorButton({ onPress, disabled, children, ...props }) { ); -} - -SelectorButton.propTypes = { - children: PropTypes.node, - onPress: PropTypes.func, - disabled: PropTypes.bool, }; export default SelectorButton; diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 1353c66b04a..d800e78ce30 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -21,14 +21,11 @@ import BackgroundTimer from 'react-native-background-timer'; import NotificationManager from '../../../core/NotificationManager'; import Engine from '../../../core/Engine'; import AppConstants from '../../../core/AppConstants'; -import notifee from '@notifee/react-native'; import I18n, { strings } from '../../../../locales/i18n'; import FadeOutOverlay from '../../UI/FadeOutOverlay'; import BackupAlert from '../../UI/BackupAlert'; import Notification from '../../UI/Notification'; import RampOrders from '../../UI/Ramp'; -import Device from '../../../util/device'; -import Routes from '../../../constants/navigation/Routes'; import { showTransactionNotification, hideCurrentNotification, @@ -36,12 +33,12 @@ import { removeNotificationById, removeNotVisibleNotifications, } from '../../../actions/notification'; + import ProtectYourWalletModal from '../../UI/ProtectYourWalletModal'; import MainNavigator from './MainNavigator'; import SkipAccountSecurityModal from '../../UI/SkipAccountSecurityModal'; import { query } from '@metamask/controller-utils'; import SwapsLiveness from '../../UI/Swaps/SwapsLiveness'; -import useNotificationHandler from '../../../util/notifications/hooks'; import { setInfuraAvailabilityBlocked, @@ -70,6 +67,8 @@ import { selectNetworkImageSource, } from '../../../selectors/networkInfos'; import { selectShowIncomingTransactionNetworks } from '../../../selectors/preferencesController'; + +import useNotificationHandler from '../../../util/notifications/hooks'; import { DEPRECATED_NETWORKS, NETWORKS_CHAIN_ID, @@ -105,16 +104,18 @@ const Main = (props) => { const [showDeprecatedAlert, setShowDeprecatedAlert] = useState(true); const { colors } = useTheme(); const styles = createStyles(colors); - const backgroundMode = useRef(false); const locale = useRef(I18n.locale); const removeConnectionStatusListener = useRef(); const removeNotVisibleNotifications = props.removeNotVisibleNotifications; - + useNotificationHandler(props.navigation); useEnableAutomaticSecurityChecks(); useMinimumVersions(); + + + useEffect(() => { if (DEPRECATED_NETWORKS.includes(props.chainId)) { setShowDeprecatedAlert(true); @@ -267,23 +268,8 @@ const Main = (props) => { initForceReload(); return; } - }); - - const bootstrapAndroidInitialNotification = useCallback(async () => { - if (Device.isAndroid()) { - const initialNotification = await notifee.getInitialNotification(); - - if ( - initialNotification?.data?.action === 'tx' && - initialNotification.data.id - ) { - NotificationManager.setTransactionToView(initialNotification.data.id); - props.navigation.navigate(Routes.TRANSACTIONS_VIEW); - } - } - }, [props.navigation]); - useNotificationHandler(bootstrapAndroidInitialNotification, props.navigation); + }); // Remove all notifications that aren't visible useEffect(() => { diff --git a/app/components/UI/AddressCopy/AddressCopy.styles.ts b/app/components/UI/AddressCopy/AddressCopy.styles.ts index 5a79e70438d..089c48d5136 100644 --- a/app/components/UI/AddressCopy/AddressCopy.styles.ts +++ b/app/components/UI/AddressCopy/AddressCopy.styles.ts @@ -7,14 +7,18 @@ const styleSheet = (params: { theme: Theme }) => { const { colors } = theme; return StyleSheet.create({ - address: { flexDirection: 'row' }, + address: { + flexDirection: 'row', + alignItems: 'center', + }, copyButton: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.primary.muted, borderRadius: 20, - paddingHorizontal: 8, - marginLeft: 8, + paddingHorizontal: 12, + padding: 4, + marginLeft: 12, }, icon: { marginLeft: 4 }, }); diff --git a/app/components/UI/AddressCopy/AddressCopy.tsx b/app/components/UI/AddressCopy/AddressCopy.tsx index 17f8dbd1550..d295c5f75ad 100644 --- a/app/components/UI/AddressCopy/AddressCopy.tsx +++ b/app/components/UI/AddressCopy/AddressCopy.tsx @@ -79,7 +79,7 @@ const AddressCopy = ({ formatAddressType = 'full' }: AddressCopyProps) => { > {selectedInternalAccount diff --git a/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx b/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx index c66a2390c3d..e7980f2848d 100644 --- a/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx +++ b/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx @@ -4,12 +4,12 @@ import { useSelector } from 'react-redux'; import { strings } from '../../../../locales/i18n'; import TagUrl from '../../../component-library/components/Tags/TagUrl'; +import { useStyles } from '../../../component-library/hooks'; import AppConstants from '../../../core/AppConstants'; +import { selectInternalAccounts } from '../../../selectors/accountsController'; import { selectAccountsByChainId } from '../../../selectors/accountTrackerController'; import { prefixUrlWithProtocol } from '../../../util/browser'; import useFavicon from '../../hooks/useFavicon/useFavicon'; -import { selectInternalAccounts } from '../../../selectors/accountsController'; -import { useStyles } from '../../../component-library/hooks'; import stylesheet from './ApprovalTagUrl.styles'; const { ORIGIN_DEEPLINK, ORIGIN_QR_CODE } = AppConstants.DEEPLINKS; @@ -51,12 +51,14 @@ const ApprovalTagUrl = ({ const domainTitle = useMemo(() => { let title = ''; - if (url || currentEnsName) { - title = prefixUrlWithProtocol(currentEnsName || url || ''); + if (url || currentEnsName || origin) { + title = prefixUrlWithProtocol(currentEnsName || origin || url); + } else { + title = ''; } return title; - }, [currentEnsName, url]); + }, [currentEnsName, origin, url]); const faviconSource = useFavicon(origin as string) as | { uri: string } diff --git a/app/components/UI/AssetOverview/Balance/Balance.tsx b/app/components/UI/AssetOverview/Balance/Balance.tsx index abbaa021a1f..651ff62892d 100644 --- a/app/components/UI/AssetOverview/Balance/Balance.tsx +++ b/app/components/UI/AssetOverview/Balance/Balance.tsx @@ -25,6 +25,10 @@ import Text, { TextVariant, } from '../../../../component-library/components/Texts/Text'; import { TokenI } from '../../Tokens/types'; +import { useNavigation } from '@react-navigation/native'; +import { isPooledStakingFeatureEnabled } from '../../Stake/constants'; +import StakingBalance from '../../Stake/components/StakingBalance/StakingBalance'; + interface BalanceProps { asset: TokenI; mainBalance: string; @@ -46,6 +50,7 @@ const NetworkBadgeSource = (chainId: string, ticker: string) => { const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => { const { styles } = useStyles(styleSheet, {}); + const navigation = useNavigation(); const networkName = useSelector(selectNetworkName); const chainId = useSelector(selectChainId); @@ -58,6 +63,7 @@ const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => { asset={asset} mainBalance={mainBalance} balance={secondaryBalance} + onPress={() => !asset.isETH && navigation.navigate('AssetDetails')} > { {asset.name || asset.symbol} + {isPooledStakingFeatureEnabled() && asset?.isETH && } ); }; diff --git a/app/components/UI/AssetOverview/Balance/index.test.tsx b/app/components/UI/AssetOverview/Balance/index.test.tsx index 8df5db0ed87..79d5d4c5c0d 100644 --- a/app/components/UI/AssetOverview/Balance/index.test.tsx +++ b/app/components/UI/AssetOverview/Balance/index.test.tsx @@ -1,15 +1,27 @@ import React from 'react'; +import { Image } from 'react-native'; import Balance from '.'; -import { render } from '@testing-library/react-native'; +import { render, fireEvent } from '@testing-library/react-native'; import { selectNetworkName } from '../../../../selectors/networkInfos'; import { selectChainId } from '../../../../selectors/networkController'; -import { useSelector } from 'react-redux'; +import { Provider, useSelector } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +import { backgroundState } from '../../../../util/test/initial-root-state'; jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useSelector: jest.fn(), })); +const mockNavigate = jest.fn(); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + navigate: mockNavigate, + }), +})); + const mockDAI = { address: '0x6b175474e89094c44da98b954eedeac495271d0f', aggregators: ['Metamask', 'Coinmarketcap'], @@ -25,7 +37,35 @@ const mockDAI = { logo: 'image-path', }; +const mockETH = { + address: '0x0000000000000000000000000000', + aggregators: [], + balanceError: null, + balance: '100', + balanceFiat: '$10000', + decimals: 18, + image: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b175474e89094c44da98b954eedeac495271d0f.png', + name: 'Ethereum', + symbol: 'ETH', + isETH: true, + logo: 'image-path', +}; + +const mockInitialState = { + engine: { + backgroundState, + }, +}; + describe('Balance', () => { + const mockStore = configureMockStore(); + const store = mockStore(mockInitialState); + + Image.getSize = jest.fn((_uri, success) => { + success(100, 100); // Mock successful response for ETH native Icon Image + }); + beforeEach(() => { (useSelector as jest.Mock).mockImplementation((selector) => { switch (selector) { @@ -38,9 +78,11 @@ describe('Balance', () => { } }); }); - beforeAll(() => { - jest.resetAllMocks(); + + afterEach(() => { + jest.clearAllMocks(); }); + it('should render correctly with a fiat balance', () => { const wrapper = render( , @@ -58,4 +100,24 @@ describe('Balance', () => { ); expect(wrapper).toMatchSnapshot(); }); + + it('should fire navigation event for non native tokens', () => { + const { queryByTestId } = render( + , + ); + const assetElement = queryByTestId('asset-DAI'); + fireEvent.press(assetElement); + expect(mockNavigate).toHaveBeenCalledTimes(1); + }); + + it('should not fire navigation event for native tokens', () => { + const { queryByTestId } = render( + + + , + ); + const assetElement = queryByTestId('asset-ETH'); + fireEvent.press(assetElement); + expect(mockNavigate).toHaveBeenCalledTimes(0); + }); }); diff --git a/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx b/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx index b2d26f68838..e01d3afdccb 100644 --- a/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx +++ b/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx @@ -26,10 +26,7 @@ import Icon, { IconSize, } from '../../../../component-library/components/Icons/Icon'; import Routes from '../../../../constants/navigation/Routes'; -import { - asyncAlert, - requestPushNotificationsPermission, -} from '../../../../util/notifications'; +import NotificationsService from '../../../../util/notifications/services/NotificationService'; import { MetaMetricsEvents } from '../../../../core/Analytics'; import { useEnableNotifications } from '../../../../util/notifications/hooks/useNotifications'; import { useMetrics } from '../../../hooks/useMetrics'; @@ -37,7 +34,6 @@ import { selectIsProfileSyncingEnabled, selectIsMetamaskNotificationsEnabled, } from '../../../../selectors/notifications'; -import { AuthorizationStatus } from '@notifee/react-native'; interface Props { route: { @@ -65,18 +61,11 @@ const BasicFunctionalityModal = ({ route }: Props) => { const { enableNotifications } = useEnableNotifications(); const enableNotificationsFromModal = useCallback(async () => { - const nativeNotificationStatus = await requestPushNotificationsPermission( - asyncAlert, - ); - - if (nativeNotificationStatus?.authorizationStatus === AuthorizationStatus.AUTHORIZED) { - /** - * Although this is an async function, we are dispatching an action (firing & forget) - * to emulate optimistic UI. - * - */ - enableNotifications(); + const { permission } = await NotificationsService.getAllPermissions(false); + if (permission !== 'authorized') { + return; } + enableNotifications(); }, [enableNotifications]); const closeBottomSheet = async () => { diff --git a/app/components/UI/ComponentErrorBoundary/index.js b/app/components/UI/ComponentErrorBoundary/index.js deleted file mode 100644 index d532728cdc3..00000000000 --- a/app/components/UI/ComponentErrorBoundary/index.js +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Logger from '../../../util/Logger'; -import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics'; - -class ComponentErrorBoundary extends React.Component { - state = { error: null }; - - static propTypes = { - /** - * Component to be used when there is no error - */ - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - ]), - /** - * Component label for logging - */ - componentLabel: PropTypes.string.isRequired, - /** - * Function to be called when there is an error - */ - onError: PropTypes.func, - /** - * Will not track as an error, but still log to analytics - */ - dontTrackAsError: PropTypes.bool, - }; - - static getDerivedStateFromError(error) { - return { error }; - } - - componentDidCatch(error, errorInfo) { - // eslint-disable-next-line no-unused-expressions - this.props.onError?.(); - - const { componentLabel, dontTrackAsError } = this.props; - - if (dontTrackAsError) { - return trackErrorAsAnalytics( - `Component Error Boundary: ${componentLabel}`, - error?.message, - ); - } - Logger.error(error, { View: this.props.componentLabel, ...errorInfo }); - } - - getErrorMessage = () => - `Component: ${this.props.componentLabel}\n${this.state.error.toString()}`; - - render() { - return this.state.error ? null : this.props.children; - } -} - -export default ComponentErrorBoundary; diff --git a/app/components/UI/ComponentErrorBoundary/index.tsx b/app/components/UI/ComponentErrorBoundary/index.tsx new file mode 100644 index 00000000000..788c502297c --- /dev/null +++ b/app/components/UI/ComponentErrorBoundary/index.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import Logger from '../../../util/Logger'; +import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics'; + +interface ComponentErrorBoundaryProps { + /** + * Component to be used when there is no error + */ + children: React.ReactNode; + /** + * Component label for logging + */ + componentLabel: string; + /** + * Function to be called when there is an error + */ + onError?: () => void; + /** + * Will not track as an error, but still log to analytics + */ + dontTrackAsError?: boolean; +} + +interface ComponentErrorBoundaryState { + error: Error | null; +} + +class ComponentErrorBoundary extends React.Component { + state: ComponentErrorBoundaryState = { error: null }; + + static getDerivedStateFromError(error: Error): ComponentErrorBoundaryState { + return { error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { + this.props.onError?.(); + + const { componentLabel, dontTrackAsError } = this.props; + + if (dontTrackAsError) { + return trackErrorAsAnalytics( + `Component Error Boundary: ${componentLabel}`, + error?.message, + ); + } + Logger.error(error, { View: this.props.componentLabel, ...errorInfo }); + } + + getErrorMessage = (): string => + `Component: ${this.props.componentLabel}\n${this.state.error?.toString()}`; + + render(): React.ReactNode { + return this.state.error ? null : this.props.children; + } +} + +export default ComponentErrorBoundary; diff --git a/app/components/UI/GenericButton/index.android.js b/app/components/UI/GenericButton/index.android.tsx similarity index 74% rename from app/components/UI/GenericButton/index.android.js rename to app/components/UI/GenericButton/index.android.tsx index d715f3567a7..31c0f904e48 100644 --- a/app/components/UI/GenericButton/index.android.js +++ b/app/components/UI/GenericButton/index.android.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { View, ViewPropTypes, TouchableNativeFeedback } from 'react-native'; +import { View, TouchableNativeFeedback, StyleProp, ViewStyle, GestureResponderEvent } from 'react-native'; /** * @deprecated The `` component has been deprecated in favor of the new `