diff --git a/.github/workflows/add-release-label.yml b/.github/workflows/add-release-label.yml index 80941c645364..42a21bfa4c03 100644 --- a/.github/workflows/add-release-label.yml +++ b/.github/workflows/add-release-label.yml @@ -22,9 +22,13 @@ jobs: with: node-version-file: '.nvmrc' - - name: Install dependencies - run: yarn --immutable - working-directory: '.github/scripts' + - name: Install dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: cd .github/scripts && yarn --immutable - name: Get the next semver version id: get-next-semver-version diff --git a/.github/workflows/bitrise-e2e-gate.yml b/.github/workflows/bitrise-e2e-gate.yml index 63eeeb8c0ec1..17e87539a3c4 100644 --- a/.github/workflows/bitrise-e2e-gate.yml +++ b/.github/workflows/bitrise-e2e-gate.yml @@ -27,9 +27,13 @@ jobs: with: node-version-file: '.nvmrc' - - name: Install dependencies - run: yarn --immutable - working-directory: '.github/scripts' + - name: Install dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: cd .github/scripts && yarn --immutable - name: Check Bitrise E2E Gate env: diff --git a/.github/workflows/build-android-e2e.yml b/.github/workflows/build-android-e2e.yml index ad87c62628cd..d8a6bb72cce7 100644 --- a/.github/workflows/build-android-e2e.yml +++ b/.github/workflows/build-android-e2e.yml @@ -50,11 +50,18 @@ jobs: restore-keys: | gradle-${{ runner.os }}- + - name: Setup project dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: | + echo "🚀 Setting up project..." + yarn setup:github-ci --no-build-ios + - name: Build Android E2E APKs run: | - echo "🚀 Setting up project..." - yarn setup:github-ci --no-build-ios - echo "🏗 Building Android E2E APKs..." export NODE_OPTIONS="--max-old-space-size=8192" cp android/gradle.properties.github android/gradle.properties diff --git a/.github/workflows/build-ios-e2e.yml b/.github/workflows/build-ios-e2e.yml index 992e93c05012..7633081b9624 100644 --- a/.github/workflows/build-ios-e2e.yml +++ b/.github/workflows/build-ios-e2e.yml @@ -93,11 +93,20 @@ jobs: - name: Clean iOS plist files run: find ios -name "*.plist" -exec xattr -c {} \; - # Run project setup and build the iOS E2E app for simulator + # Run project setup with retry for better resilience + - name: Setup project dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: | + echo "🚀 Setting up project..." + yarn setup:github-ci --build-ios --no-build-android + + # Build the iOS E2E app for simulator - name: Build iOS E2E App run: | - echo "🚀 Setting up project..." - yarn setup:github-ci --build-ios --no-build-android echo "🏗 Building iOS E2E App..." export NODE_OPTIONS="--max-old-space-size=8192" yarn build:ios:main:e2e diff --git a/.github/workflows/check-attributions.yml b/.github/workflows/check-attributions.yml index 22d6fa63e9e9..2cabcfe8dbed 100644 --- a/.github/workflows/check-attributions.yml +++ b/.github/workflows/check-attributions.yml @@ -15,7 +15,12 @@ jobs: with: node-version-file: '.nvmrc' cache: 'yarn' - - name: Install dependencies from cache - run: yarn --immutable + - name: Install dependencies from cache with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn --immutable - name: Check attributions changes run: yarn test:attribution-check diff --git a/.github/workflows/check-pr-labels.yml b/.github/workflows/check-pr-labels.yml index e17447c27116..31f8aa4c9675 100644 --- a/.github/workflows/check-pr-labels.yml +++ b/.github/workflows/check-pr-labels.yml @@ -27,9 +27,13 @@ jobs: with: node-version-file: '.nvmrc' - - name: Install dependencies - run: yarn --immutable - working-directory: '.github/scripts' + - name: Install dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: cd .github/scripts && yarn --immutable - name: Check PR has required labels id: check-pr-has-required-labels diff --git a/.github/workflows/check-template-and-add-labels.yml b/.github/workflows/check-template-and-add-labels.yml index 2846cebf2234..07a8e7d99d71 100644 --- a/.github/workflows/check-template-and-add-labels.yml +++ b/.github/workflows/check-template-and-add-labels.yml @@ -20,9 +20,13 @@ jobs: with: node-version-file: '.nvmrc' - - name: Install dependencies - run: yarn --immutable - working-directory: '.github/scripts' + - name: Install dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: cd .github/scripts && yarn --immutable - name: Check template and add labels id: check-template-and-add-labels diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8c56c0a299d..df7c09bc679c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,13 @@ jobs: ruby-version: '3.1.6' env: BUNDLE_GEMFILE: ios/Gemfile - - run: yarn setup + - 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 setup - name: Require clean working directory shell: bash run: | @@ -46,9 +52,20 @@ jobs: with: node-version-file: '.nvmrc' cache: yarn - - run: yarn setup --node - - name: Deduplicate dependencies - run: yarn deduplicate + - 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 setup --node + - name: Deduplicate dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn deduplicate - name: Print error if duplicates found shell: bash run: | @@ -64,9 +81,20 @@ jobs: with: node-version-file: '.nvmrc' cache: yarn - - run: yarn setup --node - - name: Run @lavamoat/git-safe-dependencies - run: yarn git-safe-dependencies + - 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 setup --node + - name: Run @lavamoat/git-safe-dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn git-safe-dependencies scripts: runs-on: ubuntu-latest strategy: @@ -86,7 +114,13 @@ jobs: with: node-version-file: '.nvmrc' cache: yarn - - run: yarn setup --node + - 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 setup --node - run: yarn ${{ matrix['scripts'] }} - name: Require clean working directory shell: bash @@ -108,7 +142,13 @@ jobs: with: node-version-file: '.nvmrc' cache: yarn - - run: yarn setup --node + - 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 setup --node # The "10" in this command is the total number of shards. It must be kept # in sync with the length of matrix.shard - run: yarn test:unit --shard=${{ matrix.shard }}/10 --forceExit --silent --coverageReporters=json @@ -141,7 +181,13 @@ jobs: with: node-version-file: '.nvmrc' cache: yarn - - run: yarn setup --node + - 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 setup --node - uses: actions/download-artifact@v4 with: path: tests/coverage/ @@ -197,8 +243,13 @@ jobs: with: node-version-file: '.nvmrc' cache: yarn - - name: Install dependencies - run: yarn setup --no-build-android + - 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 setup --no-build-android - name: Generate iOS bundle run: yarn gen-bundle:ios diff --git a/.github/workflows/close-bug-report.yml b/.github/workflows/close-bug-report.yml index 9907a836386b..5804a3a392e2 100644 --- a/.github/workflows/close-bug-report.yml +++ b/.github/workflows/close-bug-report.yml @@ -22,9 +22,13 @@ jobs: with: node-version-file: '.nvmrc' - - name: Install dependencies - run: yarn --immutable - working-directory: '.github/scripts' + - name: Install dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: cd .github/scripts && yarn --immutable - name: Close release bug report issue id: close-release-bug-report-issue diff --git a/.github/workflows/create-bug-report.yml b/.github/workflows/create-bug-report.yml index e989e37bffcb..3b264e827594 100644 --- a/.github/workflows/create-bug-report.yml +++ b/.github/workflows/create-bug-report.yml @@ -27,10 +27,14 @@ jobs: with: node-version-file: '.nvmrc' - - name: Install dependencies + - name: Install dependencies with retry if: steps.extract_version.outputs.version - run: yarn --immutable - working-directory: '.github/scripts' + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: cd .github/scripts && yarn --immutable - name: Create bug report issue on planning repo if: steps.extract_version.outputs.version diff --git a/.github/workflows/fitness-functions.yml b/.github/workflows/fitness-functions.yml index 6e621537271a..7a44c97b5cc4 100644 --- a/.github/workflows/fitness-functions.yml +++ b/.github/workflows/fitness-functions.yml @@ -19,9 +19,13 @@ jobs: with: node-version-file: '.nvmrc' - - name: Install dependencies - run: yarn --immutable - working-directory: '.github/scripts' + - name: Install dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: cd .github/scripts && yarn --immutable - name: Run fitness functions env: diff --git a/.github/workflows/run-bitrise-e2e-check.yml b/.github/workflows/run-bitrise-e2e-check.yml index 4828d651ba20..56405aca350b 100644 --- a/.github/workflows/run-bitrise-e2e-check.yml +++ b/.github/workflows/run-bitrise-e2e-check.yml @@ -29,9 +29,13 @@ jobs: with: node-version-file: '.nvmrc' - - name: Install dependencies - run: yarn --immutable - working-directory: '.github/scripts' + - name: Install dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: cd .github/scripts && yarn --immutable - name: Check Bitrise E2E Status env: diff --git a/.github/workflows/run-bitrise-flask-e2e-check.yml b/.github/workflows/run-bitrise-flask-e2e-check.yml index 7d58790aa9d1..812842200d47 100644 --- a/.github/workflows/run-bitrise-flask-e2e-check.yml +++ b/.github/workflows/run-bitrise-flask-e2e-check.yml @@ -28,9 +28,13 @@ jobs: with: node-version-file: '.nvmrc' - - name: Install dependencies - run: yarn --immutable - working-directory: '.github/scripts' + - name: Install dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: cd .github/scripts && yarn --immutable - name: Check Bitrise Flask E2E Status env: diff --git a/.github/workflows/run-e2e-api-specs.yml b/.github/workflows/run-e2e-api-specs.yml index 53d0d3923899..812e40a1e4ce 100644 --- a/.github/workflows/run-e2e-api-specs.yml +++ b/.github/workflows/run-e2e-api-specs.yml @@ -48,13 +48,23 @@ jobs: corepack enable corepack prepare yarn@1.22.22 --activate - - name: Install JavaScript dependencies - run: yarn install --frozen-lockfile + - name: Install JavaScript dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 env: NODE_OPTIONS: --max-old-space-size=4096 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn install --frozen-lockfile - - name: Install Detox CLI - run: yarn global add detox-cli + - name: Install Detox CLI with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn global add detox-cli - name: Setup Xcode run: sudo xcode-select -s /Applications/Xcode_16.2.app diff --git a/.github/workflows/run-e2e-smoke-tests-android.yml b/.github/workflows/run-e2e-smoke-tests-android.yml index ae739d809b2f..b504e9631c3e 100644 --- a/.github/workflows/run-e2e-smoke-tests-android.yml +++ b/.github/workflows/run-e2e-smoke-tests-android.yml @@ -106,34 +106,6 @@ jobs: total_splits: 2 secrets: inherit - # performance-android-smoke: - # strategy: - # matrix: - # split: [1, 2] - # fail-fast: false - # uses: ./.github/workflows/run-e2e-workflow.yml - # with: - # test-suite-name: performance-android-smoke-${{ matrix.split }} - # platform: android - # test_suite_tag: "SmokePerformance" - # split_number: ${{ matrix.split }} - # total_splits: 2 - # secrets: inherit - - card-android-smoke: - strategy: - matrix: - split: [1] - fail-fast: false - uses: ./.github/workflows/run-e2e-workflow.yml - with: - test-suite-name: card-android-smoke-${{ matrix.split }} - platform: android - test_suite_tag: "SmokeCard" - split_number: ${{ matrix.split }} - total_splits: 1 - secrets: inherit - confirmations-redesigned-android-smoke: strategy: matrix: @@ -160,8 +132,6 @@ jobs: - accounts-android-smoke - network-abstraction-android-smoke - network-expansion-android-smoke - # - performance-android-smoke - - card-android-smoke - confirmations-redesigned-android-smoke steps: diff --git a/.github/workflows/run-e2e-smoke-tests-ios.yml b/.github/workflows/run-e2e-smoke-tests-ios.yml index b43b2ee04c6a..781ff329d07d 100644 --- a/.github/workflows/run-e2e-smoke-tests-ios.yml +++ b/.github/workflows/run-e2e-smoke-tests-ios.yml @@ -120,22 +120,6 @@ jobs: total_splits: 2 secrets: inherit - # performance-ios-smoke: - # uses: ./.github/workflows/run-e2e-workflow.yml - # with: - # test-suite-name: performance-ios-smoke - # platform: ios - # test_suite_tag: "SmokePerformance" - # secrets: inherit - - card-ios-smoke: - uses: ./.github/workflows/run-e2e-workflow.yml - with: - test-suite-name: card-ios-smoke - platform: ios - test_suite_tag: "SmokeCard" - secrets: inherit - report-ios-smoke-tests: name: Report iOS Smoke Tests runs-on: ubuntu-latest @@ -149,8 +133,6 @@ jobs: - accounts-ios-smoke - network-abstraction-ios-smoke - network-expansion-ios-smoke - # - performance-ios-smoke - - card-ios-smoke steps: - name: Checkout diff --git a/.github/workflows/run-e2e-workflow.yml b/.github/workflows/run-e2e-workflow.yml index 1e8c9c0dfdd6..c635b241d0b8 100644 --- a/.github/workflows/run-e2e-workflow.yml +++ b/.github/workflows/run-e2e-workflow.yml @@ -92,7 +92,6 @@ jobs: setup-simulator: ${{ inputs.platform == 'ios' }} android-avd-name: emulator configure-keystores: false - sd-card-size: 8192 - name: Build Detox framework cache (iOS) if: ${{ inputs.platform == 'ios' }} @@ -171,36 +170,40 @@ jobs: echo "✅ iOS environment cleaned" - name: Run E2E tests - timeout-minutes: ${{ inputs.test-timeout-minutes }} - run: | - platform="${{ inputs.platform }}" - test_suite_tag="${{ inputs.test_suite_tag }}" - - echo "🚀 Running ${{ inputs.test-suite-name }} tests on $platform" - - # Validate required test suite tag - if [[ -z "$test_suite_tag" ]]; then - echo "❌ Error: test_suite_tag is required for non-api-specs tests" - exit 1 - fi - - export TEST_SUITE_TAG="$test_suite_tag" - echo "Using TEST_SUITE_TAG: $TEST_SUITE_TAG" - - # Run tests (Detox/Jest handle retries internally) - echo "🚀 Starting E2E tests..." - if [[ "$platform" == "ios" ]]; then - export BITRISE_TRIGGERED_WORKFLOW_ID="ios_workflow" - else - export BITRISE_TRIGGERED_WORKFLOW_ID="android_workflow" - fi - - # Always use the splitting script (handles both split and non-split cases) - echo "Running split ${{ inputs.split_number }} of ${{ inputs.total_splits }}" - - ./scripts/run-e2e-tags-gha.sh - - echo "✅ Test execution completed" + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: ${{ inputs.test-timeout-minutes }} + max_attempts: 3 + retry_wait_seconds: 30 + command: | + platform="${{ inputs.platform }}" + test_suite_tag="${{ inputs.test_suite_tag }}" + + echo "🚀 Running ${{ inputs.test-suite-name }} tests on $platform" + + # Validate required test suite tag + if [[ -z "$test_suite_tag" ]]; then + echo "❌ Error: test_suite_tag is required for non-api-specs tests" + exit 1 + fi + + export TEST_SUITE_TAG="$test_suite_tag" + echo "Using TEST_SUITE_TAG: $TEST_SUITE_TAG" + + # Run tests (Detox/Jest handle retries internally) + echo "🚀 Starting E2E tests..." + if [[ "$platform" == "ios" ]]; then + export BITRISE_TRIGGERED_WORKFLOW_ID="ios_workflow" + else + export BITRISE_TRIGGERED_WORKFLOW_ID="android_workflow" + fi + + # Always use the splitting script (handles both split and non-split cases) + echo "Running split ${{ inputs.split_number }} of ${{ inputs.total_splits }}" + + ./scripts/run-e2e-tags-gha.sh + + echo "✅ Test execution completed" env: JOB_NAME: ${{ inputs.test-suite-name }} RUN_ID: ${{ github.run_id }} diff --git a/.github/workflows/run-performance-e2e.yml b/.github/workflows/run-performance-e2e.yml index e95664724cdf..4a3b8a4f01f4 100644 --- a/.github/workflows/run-performance-e2e.yml +++ b/.github/workflows/run-performance-e2e.yml @@ -91,8 +91,13 @@ jobs: node-version-file: '.nvmrc' cache: 'yarn' - - name: Install dependencies - run: yarn setup + - name: Install dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn setup - name: Cache node_modules uses: actions/cache@v4 @@ -374,9 +379,14 @@ jobs: node-version-file: '.nvmrc' cache: 'yarn' - - name: Install dependencies (if cache miss) + - name: Install dependencies with retry (if cache miss) if: steps.cache.outputs.cache-hit != 'true' - run: yarn setup + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn setup - name: BrowserStack Env Setup uses: browserstack/github-actions/setup-env@4478e16186f38e5be07721931642e65a028713c3 @@ -417,13 +427,13 @@ jobs: echo "Either provide browserstack_app_url_android as input or ensure trigger-qa-builds-and-upload job runs successfully" exit 1 fi - + # Use app version from trigger job if available, otherwise default APP_VERSION="${{ needs.trigger-qa-builds-and-upload.outputs.android-version }}" if [ -z "$APP_VERSION" ]; then APP_VERSION="Manual-Input" fi - + { echo "BROWSERSTACK_DEVICE=${{ matrix.device.name }}" echo "BROWSERSTACK_OS_VERSION=${{ matrix.device.os_version }}" @@ -432,7 +442,7 @@ jobs: echo "QA_APP_VERSION=$APP_VERSION" echo "BROWSERSTACK_BUILD_NAME=Android-Performance-${{ github.ref_name }}-Branch" } >> "$GITHUB_ENV" - + - name: Run Android Tests on ${{ matrix.device.name }} env: BROWSERSTACK_LOCAL: true @@ -444,9 +454,9 @@ jobs: echo "Branch: ${{ github.ref_name }}" echo "QA App Version: $QA_APP_VERSION" echo "BrowserStack Android App URL: $BROWSERSTACK_ANDROID_APP_URL" - + yarn run-appwright:android-bs - + - name: Upload Android Test Results uses: actions/upload-artifact@v4 if: always() @@ -472,7 +482,7 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Restore node_modules cache id: cache uses: actions/cache@v4 @@ -484,41 +494,46 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - + - name: Set up Node.js uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'yarn' - - - name: Install dependencies (if cache miss) + + - name: Install dependencies with retry (if cache miss) if: steps.cache.outputs.cache-hit != 'true' - run: yarn setup - + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn setup + - name: BrowserStack Env Setup uses: browserstack/github-actions/setup-env@4478e16186f38e5be07721931642e65a028713c3 with: username: ${{ env.BROWSERSTACK_USERNAME }} access-key: ${{ env.BROWSERSTACK_ACCESS_KEY }} project-name: ${{ github.repository }} - + - name: Setup BrowserStack Local uses: browserstack/github-actions/setup-local@4478e16186f38e5be07721931642e65a028713c3 with: local-testing: start local-identifier: ${{ github.run_id }} local-args: --force-local --verbose - + - name: Wait for BrowserStack Local run: | echo "Waiting for BrowserStack Local to be ready..." sleep 30 echo "BrowserStack Local should be ready now" - + - name: Set iOS Test Environment run: | echo "Setting test environment for device: ${{ matrix.device.name }}" - + # Use BrowserStack URL from trigger job if it ran, otherwise from input IOS_APP_URL="${{ needs.trigger-qa-builds-and-upload.outputs.browserstack-ios-url }}" if [ -z "$IOS_APP_URL" ]; then diff --git a/.github/workflows/test-android-build-app.yml b/.github/workflows/test-android-build-app.yml index dd7749d1ddf6..1af96ba89bcd 100644 --- a/.github/workflows/test-android-build-app.yml +++ b/.github/workflows/test-android-build-app.yml @@ -90,11 +90,20 @@ jobs: - # Run project setup and build the Android QA app (APK and AAB) - - name: Setup and Build Android App + # Run project setup with retry for better resilience + - name: Setup project dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: | + echo "🚀 Finishing Android Setup..." + yarn setup:github-ci --no-build-ios + + # Build the Android QA app (APK and AAB) + - name: Build Android App run: | - echo "🚀 Finishing Android Setup..." - yarn setup:github-ci --no-build-ios echo "🏗 Building Android APP..." export NODE_OPTIONS="--max-old-space-size=8192" cp android/gradle.properties.github android/gradle.properties diff --git a/.github/workflows/test-ios-build-app.yml b/.github/workflows/test-ios-build-app.yml index be1e10237595..48c92800f571 100644 --- a/.github/workflows/test-ios-build-app.yml +++ b/.github/workflows/test-ios-build-app.yml @@ -71,11 +71,20 @@ jobs: xcrun simctl list | grep Booted || echo "No booted simulators found" shell: bash - # Run project setup and build the iOS QA app for simulator - - name: Setup iOS Environment + # Run project setup with retry for better resilience + - name: Setup project dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: | + echo "🚀 Finishing iOS Setup..." + yarn setup:github-ci --build-ios --no-build-android + + # Build the iOS QA app for simulator + - name: Build iOS App run: | - echo "🚀 Finishing iOS Setup..." - yarn setup:github-ci --build-ios --no-build-android echo "🏗 Building iOS APP..." yarn build:ios:qa shell: bash diff --git a/.github/workflows/trigger-performance-e2e.yml b/.github/workflows/trigger-performance-e2e.yml index 9d0d5192f36a..c2fd610374bd 100644 --- a/.github/workflows/trigger-performance-e2e.yml +++ b/.github/workflows/trigger-performance-e2e.yml @@ -124,8 +124,13 @@ jobs: node-version-file: '.nvmrc' cache: 'yarn' - - name: Install dependencies - run: yarn setup + - name: Install dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn setup - name: BrowserStack Env Setup uses: browserstack/github-actions/setup-env@4478e16186f38e5be07721931642e65a028713c3 @@ -199,8 +204,13 @@ jobs: node-version-file: '.nvmrc' cache: 'yarn' - - name: Install dependencies - run: yarn setup + - name: Install dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn setup - name: BrowserStack Env Setup uses: browserstack/github-actions/setup-env@4478e16186f38e5be07721931642e65a028713c3 diff --git a/.github/workflows/update-attributions.yml b/.github/workflows/update-attributions.yml index 54f026106896..28aa4642b2e6 100644 --- a/.github/workflows/update-attributions.yml +++ b/.github/workflows/update-attributions.yml @@ -63,8 +63,13 @@ jobs: with: node-version-file: '.nvmrc' cache: 'yarn' - - name: Install Yarn dependencies - run: yarn --immutable + - 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 --immutable - name: Get commit SHA id: commit-sha run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" @@ -90,8 +95,13 @@ jobs: with: node-version-file: '.nvmrc' cache: 'yarn' - - name: Install dependencies from cache - run: yarn --immutable --immutable-cache + - name: Install dependencies from cache with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn --immutable --immutable-cache - name: Generate Attributions run: yarn build:attribution - name: Cache attributions file diff --git a/app/actions/user/index.ts b/app/actions/user/index.ts index 3ceade56e5a5..5620eda34c5c 100644 --- a/app/actions/user/index.ts +++ b/app/actions/user/index.ts @@ -24,6 +24,7 @@ import { type SetAppServicesReadyAction, type SetExistingUserAction, type SetIsConnectionRemovedAction, + type SetMultichainAccountsIntroModalSeenAction, UserActionType, } from './types'; @@ -200,3 +201,15 @@ export function setIsConnectionRemoved( payload: { isConnectionRemoved }, }; } + +/** + * Action to set multichain accounts intro modal as seen + */ +export function setMultichainAccountsIntroModalSeen( + seen: boolean, +): SetMultichainAccountsIntroModalSeenAction { + return { + type: UserActionType.SET_MULTICHAIN_ACCOUNTS_INTRO_MODAL_SEEN, + payload: { seen }, + }; +} diff --git a/app/actions/user/types.ts b/app/actions/user/types.ts index f98c55ad1a25..4af31fa01b6f 100644 --- a/app/actions/user/types.ts +++ b/app/actions/user/types.ts @@ -27,6 +27,7 @@ export enum UserActionType { SET_APP_SERVICES_READY = 'SET_APP_SERVICES_READY', SET_EXISTING_USER = 'SET_EXISTING_USER', SET_IS_CONNECTION_REMOVED = 'SET_IS_CONNECTION_REMOVED', + SET_MULTICHAIN_ACCOUNTS_INTRO_MODAL_SEEN = 'SET_MULTICHAIN_ACCOUNTS_INTRO_MODAL_SEEN', } // User actions @@ -103,6 +104,11 @@ export type SetIsConnectionRemovedAction = payload: { isConnectionRemoved: boolean }; }; +export type SetMultichainAccountsIntroModalSeenAction = + Action & { + payload: { seen: boolean }; + }; + /** * User actions union type */ @@ -130,4 +136,5 @@ export type UserAction = | CheckedAuthAction | SetAppServicesReadyAction | SetExistingUserAction - | SetIsConnectionRemovedAction; + | SetIsConnectionRemovedAction + | SetMultichainAccountsIntroModalSeenAction; diff --git a/app/components/Nav/App/App.tsx b/app/components/Nav/App/App.tsx index c7c152b2da85..b3c34c719c0b 100644 --- a/app/components/Nav/App/App.tsx +++ b/app/components/Nav/App/App.tsx @@ -140,6 +140,8 @@ import DeleteAccount from '../../Views/MultichainAccounts/sheets/DeleteAccount'; import RevealPrivateKey from '../../Views/MultichainAccounts/sheets/RevealPrivateKey'; import RevealSRP from '../../Views/MultichainAccounts/sheets/RevealSRP'; import { DeepLinkModal } from '../../UI/DeepLinkModal'; +import MultichainAccountsIntroModal from '../../Views/MultichainAccounts/IntroModal'; +import LearnMoreBottomSheet from '../../Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet'; import { WalletDetails } from '../../Views/MultichainAccounts/WalletDetails/WalletDetails'; import { AddressList as MultichainAccountAddressList } from '../../Views/MultichainAccounts/AddressList'; import { PrivateKeyList as MultichainAccountPrivateKeyList } from '../../Views/MultichainAccounts/PrivateKeyList'; @@ -532,6 +534,16 @@ const RootModalFlow = (props: RootModalFlowProps) => ( name={Routes.MODAL.DEEP_LINK_MODAL} component={DeepLinkModal} /> + + ); diff --git a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.styles.ts b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.styles.ts new file mode 100644 index 000000000000..d10f7d6584b9 --- /dev/null +++ b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.styles.ts @@ -0,0 +1,39 @@ +import { StyleSheet } from 'react-native'; +import { Theme } from '../../../../util/theme/models'; + +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + const { colors } = theme; + + return StyleSheet.create({ + container: { + backgroundColor: colors.background.default, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 8, + }, + title: { + flex: 1, + textAlign: 'center', + marginHorizontal: 8, + }, + content: { + paddingHorizontal: 16, + paddingVertical: 24, + }, + description: { + marginBottom: 24, + lineHeight: 24, + }, + footer: { + paddingHorizontal: 16, + paddingVertical: 24, + paddingTop: 0, + }, + }); +}; + +export default styleSheet; diff --git a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.test.tsx b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.test.tsx new file mode 100644 index 000000000000..d5b070245c75 --- /dev/null +++ b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.test.tsx @@ -0,0 +1,281 @@ +import React from 'react'; +import { fireEvent } from '@testing-library/react-native'; +import LearnMoreBottomSheet from './LearnMoreBottomSheet'; +import renderWithProvider from '../../../../util/test/renderWithProvider'; +import { strings } from '../../../../../locales/i18n'; +import { LEARN_MORE_BOTTOM_SHEET_TEST_IDS } from './testIds'; + +const mockOnClose = jest.fn(); +const mockNavigation = { + goBack: jest.fn(), + navigate: jest.fn(), +}; +const mockDispatch = jest.fn(); + +// Mock the BottomSheet component +const mockOnCloseBottomSheet = jest.fn(); +// eslint-disable-next-line import/no-commonjs +jest.mock( + '../../../../component-library/components/BottomSheets/BottomSheet', + () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-commonjs, @typescript-eslint/no-var-requires + const ReactMock = require('react'); + return { + __esModule: true, + default: ReactMock.forwardRef( + ( + { children }: { children: React.ReactNode }, + ref: React.Ref<{ onCloseBottomSheet: () => void }>, + ) => { + ReactMock.useImperativeHandle(ref, () => ({ + onCloseBottomSheet: mockOnCloseBottomSheet, + })); + return ReactMock.createElement( + 'View', + { testID: 'bottom-sheet' }, + children, + ); + }, + ), + }; + }, +); + +// Mock React Navigation +jest.mock('@react-navigation/native', () => { + const actualReactNavigation = jest.requireActual('@react-navigation/native'); + return { + ...actualReactNavigation, + useNavigation: () => mockNavigation, + useTheme: () => ({}), + }; +}); + +// Mock Redux hooks +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, + useSelector: jest.fn(), +})); + +describe('LearnMoreBottomSheet', () => { + const { useSelector } = jest.requireMock('react-redux'); + + beforeEach(() => { + jest.clearAllMocks(); + // Mock useSelector to return basic functionality disabled by default + useSelector.mockImplementation((selector: (state: unknown) => unknown) => { + const mockState = { + settings: { + basicFunctionalityEnabled: false, + }, + }; + return selector(mockState); + }); + }); + + it('renders correctly with all elements', () => { + const { getByTestId } = renderWithProvider( + , + ); + + expect( + getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.BOTTOM_SHEET), + ).toBeOnTheScreen(); + expect( + getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.TITLE), + ).toBeOnTheScreen(); + expect( + getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.BACK_BUTTON), + ).toBeOnTheScreen(); + expect( + getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CLOSE_BUTTON), + ).toBeOnTheScreen(); + expect( + getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.DESCRIPTION), + ).toBeOnTheScreen(); + expect( + getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CHECKBOX), + ).toBeOnTheScreen(); + expect( + getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CONFIRM_BUTTON), + ).toBeOnTheScreen(); + }); + + it('displays correct title', () => { + const { getByTestId } = renderWithProvider( + , + ); + + expect( + getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.TITLE), + ).toHaveTextContent(strings('multichain_accounts.learn_more.title')); + }); + + it('displays correct description', () => { + const { getByTestId } = renderWithProvider( + , + ); + + expect( + getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.DESCRIPTION), + ).toHaveTextContent(strings('multichain_accounts.learn_more.description')); + }); + + it('displays correct checkbox label', () => { + const { getByTestId } = renderWithProvider( + , + ); + + expect( + getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CHECKBOX), + ).toHaveTextContent( + strings('multichain_accounts.learn_more.checkbox_label'), + ); + }); + + it('displays correct confirm button label', () => { + const { getByTestId } = renderWithProvider( + , + ); + + expect( + getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CONFIRM_BUTTON), + ).toHaveTextContent( + strings('multichain_accounts.learn_more.confirm_button'), + ); + }); + + it('renders confirm button', () => { + const { getByTestId } = renderWithProvider( + , + ); + + const confirmButton = getByTestId( + LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CONFIRM_BUTTON, + ); + expect(confirmButton).toBeOnTheScreen(); + }); + + it('handles back button press', () => { + const { getByTestId } = renderWithProvider( + , + ); + + const backButton = getByTestId( + LEARN_MORE_BOTTOM_SHEET_TEST_IDS.BACK_BUTTON, + ); + fireEvent.press(backButton); + + expect(mockOnCloseBottomSheet).toHaveBeenCalledTimes(1); + }); + + it('handles close button press', () => { + const { getByTestId } = renderWithProvider( + , + ); + + const closeButton = getByTestId( + LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CLOSE_BUTTON, + ); + fireEvent.press(closeButton); + + expect(mockOnCloseBottomSheet).toHaveBeenCalledTimes(1); + }); + + it('handles checkbox press and toggles state', () => { + const { getByTestId } = renderWithProvider( + , + ); + + const checkbox = getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CHECKBOX); + const confirmButton = getByTestId( + LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CONFIRM_BUTTON, + ); + + // Initially checkbox should be unchecked and confirm button disabled + expect(confirmButton).toHaveProp('disabled', true); + + // Press checkbox to check it + fireEvent.press(checkbox); + + // Confirm button should now be enabled + expect(confirmButton).toHaveProp('disabled', false); + }); + + it('handles confirm button press when checkbox is checked', () => { + const { getByTestId } = renderWithProvider( + , + ); + + const checkbox = getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CHECKBOX); + const confirmButton = getByTestId( + LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CONFIRM_BUTTON, + ); + + // First check the checkbox + fireEvent.press(checkbox); + + // Then press confirm button + fireEvent.press(confirmButton); + + // Should call navigation.goBack twice (close bottom sheet and modal) + expect(mockNavigation.goBack).toHaveBeenCalledTimes(2); + }); + + it('does not navigate when confirm button pressed without checkbox checked', () => { + const { getByTestId } = renderWithProvider( + , + ); + + const confirmButton = getByTestId( + LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CONFIRM_BUTTON, + ); + + // Press confirm button without checking checkbox + fireEvent.press(confirmButton); + + // Should not call navigation methods + expect(mockNavigation.goBack).not.toHaveBeenCalled(); + expect(mockNavigation.navigate).not.toHaveBeenCalled(); + }); + + it('navigates to basic functionality when enabled and checkbox is checked', () => { + // Mock useSelector to return basic functionality enabled + useSelector.mockImplementation((selector: (state: unknown) => unknown) => { + const mockState = { + settings: { + basicFunctionalityEnabled: true, + }, + }; + return selector(mockState); + }); + + const { getByTestId } = renderWithProvider( + , + ); + + const checkbox = getByTestId(LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CHECKBOX); + const confirmButton = getByTestId( + LEARN_MORE_BOTTOM_SHEET_TEST_IDS.CONFIRM_BUTTON, + ); + + // Check the checkbox + fireEvent.press(checkbox); + + // Press confirm button + fireEvent.press(confirmButton); + + // Should call navigation.goBack twice and navigate to basic functionality + expect(mockNavigation.goBack).toHaveBeenCalledTimes(2); + expect(mockNavigation.navigate).toHaveBeenCalledWith('RootModalFlow', { + screen: 'BasicFunctionality', + }); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'SET_MULTICHAIN_ACCOUNTS_INTRO_MODAL_SEEN', + payload: { seen: true }, + }), + ); + }); +}); diff --git a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.testIds.ts b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.testIds.ts new file mode 100644 index 000000000000..5f93c00c1efa --- /dev/null +++ b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.testIds.ts @@ -0,0 +1,21 @@ +/** + * Test IDs for LearnMoreBottomSheet component + * Centralized test identifiers to avoid hardcoded values in tests + */ +export const LEARN_MORE_BOTTOM_SHEET_TEST_IDS = { + // Main container + BOTTOM_SHEET: 'bottom-sheet', + + // Header elements + TITLE: 'learn-more-title', + BACK_BUTTON: 'learn-more-back-button', + CLOSE_BUTTON: 'learn-more-close-button', + + // Content elements + DESCRIPTION: 'learn-more-description', + CHECKBOX: 'learn-more-checkbox', + CHECKBOX_LABEL: 'learn-more-checkbox-label', + + // Action buttons + CONFIRM_BUTTON: 'learn-more-confirm-button', +} as const; diff --git a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx new file mode 100644 index 000000000000..b4e597c2760a --- /dev/null +++ b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx @@ -0,0 +1,127 @@ +import React, { useState, useCallback, useRef } from 'react'; +import { View } from 'react-native'; +import { + Text, + ButtonIcon, + TextVariant, + IconName, + TextColor, +} from '@metamask/design-system-react-native'; +import Button, { + ButtonVariants, + ButtonWidthTypes, + ButtonSize, +} from '../../../../component-library/components/Buttons/Button'; +import Checkbox from '../../../../component-library/components/Checkbox'; +import BottomSheet, { + BottomSheetRef, +} from '../../../../component-library/components/BottomSheets/BottomSheet'; +import { useNavigation, useTheme } from '@react-navigation/native'; +import { strings } from '../../../../../locales/i18n'; +import { useStyles } from '../../../../component-library/hooks'; +import styleSheet from './LearnMoreBottomSheet.styles'; +import Routes from '../../../../constants/navigation/Routes'; +import { RootState } from '../../../../reducers'; +import { useDispatch, useSelector } from 'react-redux'; +import { setMultichainAccountsIntroModalSeen } from '../../../../actions/user'; + +interface LearnMoreBottomSheetProps { + onClose: () => void; +} + +const LearnMoreBottomSheet: React.FC = ({ + onClose, +}) => { + const { styles } = useStyles(styleSheet, { theme: useTheme() }); + const [isCheckboxChecked, setIsCheckboxChecked] = useState(false); + const sheetRef = useRef(null); + const navigation = useNavigation(); + const dispatch = useDispatch(); + + const isBasicFunctionalityEnabled = useSelector( + (state: RootState) => state?.settings?.basicFunctionalityEnabled, + ); + + const handleBack = useCallback(() => { + sheetRef.current?.onCloseBottomSheet(); + }, []); + + const handleClose = useCallback(() => { + sheetRef.current?.onCloseBottomSheet(); + }, []); + + const handleCheckboxToggle = useCallback(() => { + setIsCheckboxChecked(!isCheckboxChecked); + }, [isCheckboxChecked]); + + const handleConfirm = useCallback(() => { + if (isCheckboxChecked) { + navigation.goBack(); // close bottom sheet + navigation.goBack(); // close modal + if (isBasicFunctionalityEnabled) { + dispatch(setMultichainAccountsIntroModalSeen(true)); + navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { + screen: Routes.SHEET.BASIC_FUNCTIONALITY, + }); + } + } + }, [isCheckboxChecked, navigation, isBasicFunctionalityEnabled, dispatch]); + + return ( + + + + + + {strings('multichain_accounts.learn_more.title')} + + + + + + + {strings('multichain_accounts.learn_more.description')} + + + + + + +