diff --git a/.github/actions/android-play-store-manifest-check/action.yml b/.github/actions/android-play-store-manifest-check/action.yml new file mode 100644 index 000000000000..91ee7c37a17b --- /dev/null +++ b/.github/actions/android-play-store-manifest-check/action.yml @@ -0,0 +1,165 @@ +name: 'Android Play Store manifest validation' +description: > + Builds the prodRelease variant used for Play uploads (same manifest merge path), runs Android Lint + on that variant, produces a signed release bundle using the repo debug keystore via AGP signing + injection, and runs bundletool validate on the AAB. + +runs: + using: composite + steps: + - name: Set up JDK 17 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 + with: + distribution: temurin + java-version: '17' + + # Third-party actions such as android-actions/setup-android are not allowlisted for this org; + # install SDK components with Google's cmdline-tools + sdkmanager (same packages as android/build.gradle). + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-android-manifest-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle-android-manifest- + + - name: Cache Android SDK + id: android-sdk-cache + uses: actions/cache@v4 + with: + path: ~/android-sdk + key: ${{ runner.os }}-android-sdk-manifest-v2-cmd11076708-api35-ndk26.1-cmake322 + + - name: Install Android SDK (cmdline-tools + packages) + if: steps.android-sdk-cache.outputs.cache-hit != 'true' + shell: bash + run: | + set -euo pipefail + CMDLINE_TOOLS_BUILD=11076708 + SDK_ROOT="${ANDROID_SDK_ROOT:-$HOME/android-sdk}" + mkdir -p "$SDK_ROOT" + TMPZIP="$(mktemp)" + curl -fsSL -o "$TMPZIP" \ + "https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_BUILD}_latest.zip" + rm -rf "$SDK_ROOT/cmdline-tools" + unzip -q "$TMPZIP" -d "$SDK_ROOT/cmdline-tools-staging" + rm -f "$TMPZIP" + mkdir -p "$SDK_ROOT/cmdline-tools" + mv "$SDK_ROOT/cmdline-tools-staging/cmdline-tools" "$SDK_ROOT/cmdline-tools/latest" + rm -rf "$SDK_ROOT/cmdline-tools-staging" + + SDKMANAGER="$SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" + # yes exits with SIGPIPE when sdkmanager closes stdin; with pipefail that fails the step. + set +o pipefail + yes | "$SDKMANAGER" --sdk_root="$SDK_ROOT" --licenses >/dev/null + set -o pipefail + "$SDKMANAGER" --sdk_root="$SDK_ROOT" \ + "platform-tools" \ + "platforms;android-33" \ + "platforms;android-34" \ + "platforms;android-35" \ + "build-tools;33.0.0" \ + "build-tools;34.0.0" \ + "build-tools;35.0.0" \ + "ndk;26.1.10909125" \ + "cmake;3.22.1" + + - name: Configure Android SDK environment + shell: bash + run: | + SDK_ROOT="${ANDROID_SDK_ROOT:-$HOME/android-sdk}" + echo "ANDROID_SDK_ROOT=${SDK_ROOT}" >> "$GITHUB_ENV" + echo "ANDROID_HOME=${SDK_ROOT}" >> "$GITHUB_ENV" + echo "${SDK_ROOT}/cmdline-tools/latest/bin" >> "$GITHUB_PATH" + echo "${SDK_ROOT}/platform-tools" >> "$GITHUB_PATH" + + - uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + cache: yarn + + - name: Restore .metamask folder + id: restore-metamask + uses: actions/cache@v4 + with: + path: .metamask + key: .metamask-${{ hashFiles('package.json', 'yarn.lock') }} + + - name: Install Foundry if cache missed + if: steps.restore-metamask.outputs.cache-hit != 'true' + shell: bash + run: yarn install:foundryup + + - name: Install Yarn dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn install --immutable + + - name: Project setup for CI (Android tooling path) + shell: bash + run: yarn setup:github-ci --no-build-ios + + - name: Write google-services.json + shell: bash + run: | + set -euo pipefail + # Caller workflow must set GOOGLE_SERVICES_B64_ANDROID (job env from GitHub secret). + if [[ -z "${GOOGLE_SERVICES_B64_ANDROID:-}" ]]; then + echo "::error::GOOGLE_SERVICES_B64_ANDROID is not set; cannot run Play-shaped Android build." + exit 1 + fi + echo -n "$GOOGLE_SERVICES_B64_ANDROID" | base64 -d > android/app/google-services.json + + - name: Configure Gradle for GitHub Actions + shell: bash + run: cp android/gradle.properties.github android/gradle.properties + + - name: Cache bundletool + id: bundletool-cache + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/bundletool-all.jar + key: bundletool-1.18.3-jar + + - name: Download bundletool + if: steps.bundletool-cache.outputs.cache-hit != 'true' + shell: bash + env: + BUNDLETOOL_URL: https://github.com/google/bundletool/releases/download/1.18.3/bundletool-all-1.18.3.jar + run: curl -fsSL -o "${RUNNER_TEMP}/bundletool-all.jar" "$BUNDLETOOL_URL" + + - name: Lint, bundle (debug-signed), validate with bundletool + shell: bash + env: + SENTRY_DISABLE_AUTO_UPLOAD: true + run: | + set -euo pipefail + STORE_FILE="${GITHUB_WORKSPACE}/android/keystores/debug.keystore" + cd android + chmod +x ./gradlew + ./gradlew \ + :app:lintProdRelease \ + :app:bundleProdRelease \ + --no-daemon \ + --stacktrace \ + -Pandroid.injected.signing.store.file="$STORE_FILE" \ + -Pandroid.injected.signing.store.password=android \ + -Pandroid.injected.signing.key.alias=androiddebugkey \ + -Pandroid.injected.signing.key.password=android + + # Exactly one AAB for prodRelease (main prod Play track). + shopt -s nullglob + aabs=( "$GITHUB_WORKSPACE/android/app/build/outputs/bundle/prodRelease/"*.aab ) + shopt -u nullglob + if [[ "${#aabs[@]}" -ne 1 ]]; then + echo "::error::Expected exactly one .aab under prodRelease; found ${#aabs[@]}" + find "$GITHUB_WORKSPACE/android/app/build/outputs/bundle/prodRelease" -maxdepth 2 -print || true + exit 1 + fi + echo "Validating bundle: ${aabs[0]}" + java -jar "${RUNNER_TEMP}/bundletool-all.jar" validate --bundle="${aabs[0]}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6996d5725389..40e27cec6734 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -455,6 +455,47 @@ jobs: SCRIPT_NAME="build:${{ matrix.platform }}:${SCRIPT_BASE//-/:}" yarn "$SCRIPT_NAME" + # Prod Play–shaped Android: lint merged prodRelease manifest, then validate the AAB produced above (bundletool). + # Skips e2e (no AAB) and Debug dev builds. Mirrors former ci.yml android-play-store-manifest-check without a second bundle. + - name: Cache bundletool (Android Play bundle validation) + id: bundletool-cache-play-check + if: >- + success() && + matrix.platform == 'android' && + env.CONFIGURATION != 'Debug' && + env.METAMASK_BUILD_TYPE == 'main' && + env.METAMASK_ENVIRONMENT != 'e2e' + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/bundletool-all.jar + key: bundletool-1.18.3-jar + + - name: Android Play Store lint and bundle validation (prodRelease, non-blocking) + if: >- + success() && + matrix.platform == 'android' && + env.CONFIGURATION != 'Debug' && + env.METAMASK_BUILD_TYPE == 'main' && + env.METAMASK_ENVIRONMENT != 'e2e' + env: + SENTRY_DISABLE_AUTO_UPLOAD: true + run: node scripts/android-play-store-check-slack.mjs + + - name: Upload Android Play Store Slack report (main-rc only) + if: >- + success() && + matrix.platform == 'android' && + inputs.build_name == 'main-rc' && + env.CONFIGURATION != 'Debug' && + env.METAMASK_BUILD_TYPE == 'main' && + env.METAMASK_ENVIRONMENT != 'e2e' + uses: actions/upload-artifact@v4 + with: + name: android-play-store-check-slack + path: android-play-store-check-slack.md + if-no-files-found: warn + retention-days: 14 + # Rename build artifacts (ios_simulator_path / ios_ipa_path / ios_archive_path / android_*_path outputs) - name: Rename ${{ matrix.platform }} artifacts if: success() diff --git a/.github/workflows/slack-rc-notification.yml b/.github/workflows/slack-rc-notification.yml index 4183005e56df..097cfa66a20e 100644 --- a/.github/workflows/slack-rc-notification.yml +++ b/.github/workflows/slack-rc-notification.yml @@ -37,6 +37,10 @@ on: type: string default: '' +permissions: + actions: read + contents: read + jobs: slack-notification: name: Post Slack Notification @@ -46,6 +50,15 @@ jobs: with: ref: ${{ inputs.source_branch }} fetch-depth: 0 + + - name: Download Android Play Store check report (optional) + id: download-play-store-check + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: android-play-store-check-slack + path: android-play-store-check-out + - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -71,3 +84,4 @@ jobs: ANDROID_PUBLIC_URL: ${{ secrets.ANDROID_PUBLIC_BUCKET_URL }} SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} PR_NUMBER: ${{ inputs.pr_number }} + ANDROID_PLAY_STORE_CHECK_MRKDWN_FILE: ${{ github.workspace }}/android-play-store-check-out/android-play-store-check-slack.md diff --git a/android/app/build.gradle b/android/app/build.gradle index 4647d5eeced6..1ba0ebf1e0cb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -172,7 +172,9 @@ def reactNativeArchitectures() { android { ndkVersion rootProject.ext.ndkVersion - + lint { + baseline = file("lint-baseline.xml") + } buildToolsVersion rootProject.ext.buildToolsVersion compileSdk rootProject.ext.compileSdkVersion diff --git a/android/app/lint-baseline.xml b/android/app/lint-baseline.xml new file mode 100644 index 000000000000..daad37b1387a --- /dev/null +++ b/android/app/lint-baseline.xml @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/gradle.properties b/android/gradle.properties index 566ababa6068..52814bdfb292 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -22,6 +22,9 @@ org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true android.enableJetifier=true +# byte-buddy 1.17+ ships class files newer than Jetifier's ASM can read (Java 24 / major 68). +# It is not a support-library artifact and does not need jetification. See b/184622491. +android.jetifier.ignorelist=.*byte-buddy.* # Enable AAPT2 PNG crunching android.enablePngCrunchInReleaseBuilds=true @@ -55,4 +58,4 @@ expo.useLegacyPackaging=false # Note: Only works with ReactActivity and should not be used with custom Activity. edgeToEdgeEnabled=false reactNativeDir=../node_modules/react-native -REACT_NATIVE_DIR=../node_modules/react-native \ No newline at end of file +REACT_NATIVE_DIR=../node_modules/react-native diff --git a/android/gradle.properties.github b/android/gradle.properties.github index 766d08bbc5f7..1c9734cfe58c 100644 --- a/android/gradle.properties.github +++ b/android/gradle.properties.github @@ -35,6 +35,9 @@ org.gradle.welcome=NEVER # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true android.enableJetifier=true +# byte-buddy 1.17+ ships class files newer than Jetifier's ASM can read (Java 24 / major 68). +# It is not a support-library artifact and does not need jetification. See b/184622491. +android.jetifier.ignorelist=.*byte-buddy.* # Enable AAPT2 PNG crunching android.enablePngCrunchInReleaseBuilds=true diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.constants.ts b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.constants.ts deleted file mode 100644 index 2364089e9241..000000000000 --- a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const CUSTOM_SPEND_CAP_INPUT_TEST_ID = 'custom-spend-cap-input-test-id'; -export const CUSTOM_SPEND_CAP_MAX_TEST_ID = 'custom-spend-cap-max-test-id'; -export const CUSTOM_SPEND_CAP_INPUT_INPUT_ID = - 'custom-spend-cap-input-input-id'; diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.styles.ts b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.styles.ts deleted file mode 100644 index 63bb0d923c06..000000000000 --- a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.styles.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Third party dependencies. -import { StyleSheet, TextStyle } from 'react-native'; - -import { Theme } from '../../../../util/theme/models'; -import { getFontFamily, TextVariant } from '../../../components/Texts/Text'; -/** - * Style sheet for Custom Input component. - * - * @returns StyleSheet object. - */ - -const styleSheet = (params: { theme: Theme }) => { - const { theme } = params; - const { colors, typography } = theme; - - return StyleSheet.create({ - container: { - backgroundColor: colors.background.default, - borderRadius: 8, - padding: 16, - flexDirection: 'row', - justifyContent: 'space-between', - }, - fixedPadding: { - padding: 0, - }, - body: { - flexDirection: 'row', - flex: 1, - alignItems: 'center', - }, - input: { - paddingTop: 0, - paddingBottom: 0, - flexGrow: 1, - marginRight: 16, - color: colors.text.default, - ...typography.sBodyMD, - fontFamily: getFontFamily(TextVariant.BodyMD), - } as TextStyle, - maxValueText: { - color: theme.colors.text.alternative, - }, - warningValue: { - color: theme.colors.error.default, - }, - }); -}; - -export default styleSheet; diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.test.tsx b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.test.tsx deleted file mode 100644 index 20ef379ea0ea..000000000000 --- a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -// Third party dependencies. -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react-native'; - -// External dependencies. -import { TICKER } from '../CustomSpendCap.constants'; -// Internal dependencies. -import CustomInput from './CustomInput'; -import { - CUSTOM_SPEND_CAP_INPUT_INPUT_ID, - CUSTOM_SPEND_CAP_MAX_TEST_ID, -} from './CustomInput.constants'; -import { CustomInputProps } from './CustomInput.types'; - -describe('CustomInput', () => { - let props: CustomInputProps; - - beforeEach(() => { - props = { - ticker: TICKER, - value: '123', - isInputGreaterThanBalance: false, - isEditDisabled: false, - setMaxSelected: jest.fn(), - setValue: jest.fn(), - tokenDecimal: 4, - }; - }); - - const renderComponent = () => render(); - - it('should render correctly', () => { - const { toJSON } = renderComponent(); - expect(toJSON()).toBeDefined(); - }); - - it('should call setMaxSelected when max button is pressed', () => { - renderComponent(); - fireEvent.press(screen.getByTestId(CUSTOM_SPEND_CAP_MAX_TEST_ID)); - expect(props.setMaxSelected).toHaveBeenCalled(); - }); - - it('should update value if input is integer', () => { - renderComponent(); - fireEvent.changeText( - screen.getByTestId(CUSTOM_SPEND_CAP_INPUT_INPUT_ID), - '123', - ); - expect(props.setValue).toHaveBeenCalledWith('123'); - }); - - it('should update value if input is decimal and decimal points are less than or equal to tokenDecimal', () => { - renderComponent(); - fireEvent.changeText( - screen.getByTestId(CUSTOM_SPEND_CAP_INPUT_INPUT_ID), - '123.1234', - ); - expect(props.setValue).toHaveBeenCalledWith('123.1234'); - }); - - it('should not update value if input is decimal and decimal points are greater than tokenDecimal', () => { - renderComponent(); - fireEvent.changeText( - screen.getByTestId(CUSTOM_SPEND_CAP_INPUT_INPUT_ID), - '123.1234567', - ); - expect(props.setValue).not.toHaveBeenCalled(); - }); -}); diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx deleted file mode 100644 index 164442f15241..000000000000 --- a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx +++ /dev/null @@ -1,101 +0,0 @@ -// Third party dependencies. -import React from 'react'; -import { TextInput, View } from 'react-native'; - -import { strings } from '../../../../../locales/i18n'; -import formatNumber from '../../../../util/formatNumber'; -import { dotAndCommaDecimalFormatter } from '../../../../util/number'; -import Text, { TextVariant } from '../../../components/Texts/Text'; -// External dependencies. -import { useStyles } from '../../../hooks'; -import { - CUSTOM_SPEND_CAP_INPUT_INPUT_ID, - CUSTOM_SPEND_CAP_INPUT_TEST_ID, - CUSTOM_SPEND_CAP_MAX_TEST_ID, -} from './CustomInput.constants'; -import stylesheet from './CustomInput.styles'; -// Internal dependencies. -import { CustomInputProps } from './CustomInput.types'; - -const CustomInput = ({ - ticker, - value, - setMaxSelected, - isInputGreaterThanBalance, - setValue, - isEditDisabled, - tokenDecimal, -}: CustomInputProps) => { - const handleUpdate = (text: string) => { - const decimalIndex = text.indexOf('.'); - const fractionalLength = text.substring(decimalIndex + 1).length; - - if (decimalIndex !== -1 && fractionalLength > Number(tokenDecimal)) { - return; - } - setValue(dotAndCommaDecimalFormatter(text)); - }; - - const handleMaxPress = () => { - setMaxSelected(true); - }; - - const { - styles, - theme: { colors }, - } = useStyles(stylesheet, {}); - - const onChangeValueText = (text: string) => { - handleUpdate(text); - setMaxSelected(false); - }; - - return ( - - - {!isEditDisabled ? ( - - ) : ( - {`${formatNumber(value)} ${ticker}`} - )} - - {!isEditDisabled && ( - - {strings('contract_allowance.custom_spend_cap.max')} - - )} - - ); -}; - -export default CustomInput; diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.types.ts b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.types.ts deleted file mode 100644 index 0a419ae6071b..000000000000 --- a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.types.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface CustomInputProps { - /** - * Token native symbol - * @default 'ETH' - */ - ticker: string; - /** - * Input Value - */ - value: string; - /** - * Function that updates the input value - */ - setValue: (value: string) => void; - /** - * Boolean to determine if input is greater than balance - * @default false - */ - isInputGreaterThanBalance: boolean; - /** - * Function to update max state - */ - setMaxSelected: (value: boolean) => void; - /** - * Boolean to disable edit - */ - isEditDisabled: boolean; - tokenDecimal?: number; -} diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/index.ts b/app/component-library/components-temp/CustomSpendCap/CustomInput/index.ts deleted file mode 100644 index c0a26168dd00..000000000000 --- a/app/component-library/components-temp/CustomSpendCap/CustomInput/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './CustomInput'; diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.constants.ts b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.constants.ts deleted file mode 100644 index 7d068672471a..000000000000 --- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.constants.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const TICKER = 'DAI'; -export const DAPP_PROPOSED_VALUE = - '115792089237316195423570985008687907853269984665640564039457.584007913129639936'; -export const ACCOUNT_BALANCE = '200.12'; -export const DAPP_DOMAIN = 'Uniswap.org'; -export const CUSTOM_SPEND_CAP_TEST_ID = 'custom-spend-cap'; -export const INPUT_VALUE_CHANGED = (value: string) => { - /* eslint-disable no-console */ - // do something with value - console.log(value); -}; diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.styles.ts b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.styles.ts deleted file mode 100644 index 44d2371b2a05..000000000000 --- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.styles.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Third party dependencies. -import { StyleSheet } from 'react-native'; - -import { Theme } from '../../../util/theme/models'; -/** - * Style sheet for CustomSpendCap component. - * - * @returns StyleSheet object. - */ - -const styleSheet = (params: { theme: Theme }) => { - const { theme } = params; - const { colors } = theme; - return StyleSheet.create({ - container: { - backgroundColor: colors.background.alternative, - borderRadius: 8, - padding: 16, - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - }, - titleContainer: { - flexDirection: 'row', - alignItems: 'center', - }, - title: { - color: colors.text.default, - marginRight: 4, - }, - descriptionContainer: { - marginTop: 16, - }, - description: { - color: theme.colors.text.alternative, - }, - errorDescription: { - color: colors.error.default, - marginTop: 8, - }, - inputContainer: { - marginTop: 8, - }, - modalTitle: { - color: colors.text.default, - }, - modalTitleDanger: { - color: colors.error.default, - }, - }); -}; - -export default styleSheet; diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.test.tsx b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.test.tsx deleted file mode 100644 index 0a0714cde92e..000000000000 --- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.test.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { fireEvent, screen } from '@testing-library/react-native'; -import React from 'react'; -import renderWithProvider from '../../../util/test/renderWithProvider'; -import CustomSpendCap from './CustomSpendCap'; -import { - ACCOUNT_BALANCE, - CUSTOM_SPEND_CAP_TEST_ID, - DAPP_PROPOSED_VALUE, - INPUT_VALUE_CHANGED, - TICKER, -} from './CustomSpendCap.constants'; - -function RenderCustomSpendCap( - tokenSpendValue = '', - isInputValid: () => boolean = () => true, - dappProposedValue: string = DAPP_PROPOSED_VALUE, - accountBalance: string = ACCOUNT_BALANCE, - unroundedAccountBalance: string = ACCOUNT_BALANCE, -) { - return ( - ({})} - tokenSpendValue={tokenSpendValue} - isInputValid={isInputValid} - tokenDecimal={18} - toggleLearnMoreWebPage={() => undefined} - /> - ); -} - -const isInputValid = jest.fn(); - -describe('CustomSpendCap', () => { - it('should render CustomSpendCap', () => { - renderWithProvider(RenderCustomSpendCap('')); - expect(screen.getByTestId(CUSTOM_SPEND_CAP_TEST_ID)).toBeTruthy(); - }); - - it('displays error message when value is not a number', async () => { - const { findByText } = renderWithProvider(RenderCustomSpendCap('abc')); - - expect(await findByText('Error: Enter only numbers')).toBeOnTheScreen(); - }); - - it('displays caution message when value is 0', async () => { - const { findByText } = renderWithProvider(RenderCustomSpendCap('0')); - - expect( - await findByText( - `Only enter a number that you're comfortable with the third party spending now or in the future. You can always increase the spending cap later. Learn more`, - ), - ).toBeOnTheScreen(); - }); - - it('displays spend value in ticker format when value is within account balance', async () => { - const { toJSON } = renderWithProvider(RenderCustomSpendCap('100')); - - expect(JSON.stringify(toJSON())).toMatch(`100 ${TICKER}`); - }); - - it('displays over-balance warning when value exceeds account balance', async () => { - const { findByText } = renderWithProvider(RenderCustomSpendCap('300')); - - expect( - await findByText( - 'This allows the third party to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap. Learn more', - ), - ).toBeOnTheScreen(); - }); - - it('calls isInputValid with false when value is not a number', () => { - renderWithProvider(RenderCustomSpendCap('abc', isInputValid)); - - expect(isInputValid).toHaveBeenCalledWith(false); - }); - - it('calls isInputValid with true when value is a number', () => { - renderWithProvider(RenderCustomSpendCap('100', isInputValid)); - - expect(isInputValid).toHaveBeenCalledWith(true); - }); - - it('displays token spend value when tokenSpendValue is provided', async () => { - const { findByText } = renderWithProvider( - RenderCustomSpendCap('100', isInputValid, DAPP_PROPOSED_VALUE), - ); - - expect(await findByText(`100 ${TICKER}`)).toBeOnTheScreen(); - }); - - it('populates input with rounded balance when max is pressed and unrounded balance is empty', async () => { - const { findByTestId, findByText } = renderWithProvider( - RenderCustomSpendCap( - '100', - isInputValid, - DAPP_PROPOSED_VALUE, - '3.14', - '', - ), - ); - - fireEvent.press(await findByText('Max')); - - const input = await findByTestId('custom-spend-cap-input-input-id'); - expect(input.props.value).toEqual('3.14'); - }); - - it('populates input with unrounded balance when max is pressed and unrounded balance is set', async () => { - const { findByTestId, findByText } = renderWithProvider( - RenderCustomSpendCap( - '100', - isInputValid, - DAPP_PROPOSED_VALUE, - '3.14', - '3.141592654', - ), - ); - - fireEvent.press(await findByText('Max')); - - const input = await findByTestId('custom-spend-cap-input-input-id'); - expect(input.props.value).toEqual('3.141592654'); - }); -}); diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx deleted file mode 100644 index daef84999913..000000000000 --- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx +++ /dev/null @@ -1,238 +0,0 @@ -// Third party dependencies. -import React, { useEffect, useState } from 'react'; -import { Pressable, View } from 'react-native'; - -import { strings } from '../../../../locales/i18n'; -import InfoModal from '../../../components/Base/InfoModal'; -import { TOKEN_APPROVAL_SPENDING_CAP } from '../../../constants/urls'; -import formatNumber from '../../../util/formatNumber'; -import { isNumber } from '../../../util/number'; -import Button, { ButtonVariants } from '../../components/Buttons/Button'; -import Icon, { IconName, IconSize } from '../../components/Icons/Icon'; -import Text, { TextVariant } from '../../components/Texts/Text'; -// External dependencies. -import { useStyles } from '../../hooks'; -import CustomInput from './CustomInput'; -// Internal dependencies. -import { CUSTOM_SPEND_CAP_TEST_ID } from './CustomSpendCap.constants'; -import customSpendCapStyles from './CustomSpendCap.styles'; -import { CustomSpendCapProps } from './CustomSpendCap.types'; - -const CustomSpendCap = ({ - ticker, - dappProposedValue, - accountBalance, - unroundedAccountBalance, - onInputChanged, - isEditDisabled, - editValue, - tokenSpendValue, - isInputValid, - tokenDecimal, - toggleLearnMoreWebPage, -}: CustomSpendCapProps) => { - const { - styles, - theme: { colors }, - } = useStyles(customSpendCapStyles, {}); - - const [value, setValue] = useState(tokenSpendValue); - const [inputDisabled, setInputDisabled] = useState(true); - const [maxSelected, setMaxSelected] = useState(false); - const [ - inputValueHigherThanAccountBalance, - setInputValueHigherThanAccountBalance, - ] = useState(false); - const [isModalVisible, setIsModalVisible] = useState(false); - const [inputHasError, setInputHasError] = useState(false); - - useEffect(() => { - if (isNumber(value)) { - setInputHasError(false); - } else { - setInputHasError(true); - } - - onInputChanged(value); - }, [value, onInputChanged]); - - useEffect(() => { - isInputValid(!inputHasError); - }, [inputHasError, isInputValid]); - - useEffect(() => { - const spendValue = tokenSpendValue || dappProposedValue; - setValue(spendValue); - }, [dappProposedValue, tokenSpendValue]); - - const handleDefaultValue = () => { - setMaxSelected(false); - setValue(dappProposedValue); - setInputDisabled(!inputDisabled); - }; - - const handlePress = () => { - isEditDisabled ? editValue() : handleDefaultValue(); - }; - - useEffect(() => { - if (maxSelected) setValue(unroundedAccountBalance || accountBalance); - }, [maxSelected, accountBalance, unroundedAccountBalance]); - - useEffect(() => { - if (Number(value) > Number(accountBalance)) - return setInputValueHigherThanAccountBalance(true); - return setInputValueHigherThanAccountBalance(false); - }, [value, accountBalance]); - - const MAX_VALUE_SELECTED = ( - <> - {strings('contract_allowance.custom_spend_cap.this_contract_allows')} - - {` ${formatNumber(accountBalance)} ${ticker} `} - - {strings('contract_allowance.custom_spend_cap.from_your_balance')} - - ); - - const NO_SELECTED = strings( - 'contract_allowance.custom_spend_cap.default_error_message', - ); - - const INPUT_VALUE_GREATER_THAN_ACCOUNT_BALANCE = strings( - 'contract_allowance.custom_spend_cap.amount_greater_than_balance', - ); - - const INPUT_VALUE_LOWER_THAN_ACCOUNT_BALANCE = ( - <> - {strings('contract_allowance.custom_spend_cap.this_contract_allows')} - - {` ${formatNumber(tokenSpendValue ?? '0')} ${ticker} `} - - {strings('contract_allowance.custom_spend_cap.from_your_balance')} - - ); - - const toggleModal = () => { - setIsModalVisible(!isModalVisible); - }; - - const infoModalTitle = inputValueHigherThanAccountBalance ? ( - <> - - - {strings('contract_allowance.custom_spend_cap.be_careful')} - {' '} - - ) : ( - - {strings('contract_allowance.custom_spend_cap.set_spend_cap')} - - ); - - let message; - - if (!value || !Number(value)) { - message = NO_SELECTED; - } else if (maxSelected) { - message = MAX_VALUE_SELECTED; - } else if (inputValueHigherThanAccountBalance) { - message = INPUT_VALUE_GREATER_THAN_ACCOUNT_BALANCE; - } else { - message = INPUT_VALUE_LOWER_THAN_ACCOUNT_BALANCE; - } - - const openLearnMore = () => - toggleLearnMoreWebPage(TOKEN_APPROVAL_SPENDING_CAP); - - return ( - - {isModalVisible ? ( - - {inputValueHigherThanAccountBalance - ? strings( - 'contract_allowance.custom_spend_cap.info_modal_description_default', - ) - : strings( - 'contract_allowance.custom_spend_cap.default_error_message', - )} - - } - toggleModal={toggleModal} - /> - ) : null} - - - - {strings('contract_allowance.custom_spend_cap.title')} - - - - - -