diff --git a/.env.template b/.env.template
index 7997be1e7c..a3ae6dc6bf 100644
--- a/.env.template
+++ b/.env.template
@@ -21,4 +21,5 @@ ARBISCAN_API_KEY=
OPSCAN_API_KEY=
BASESCAN_API_KEY=
STATIC_CONTENT_CARDS_ENABLED=
-TEST_MODE_NETWORK=
\ No newline at end of file
+TEST_MODE_NETWORK=
+WALLET_PHRASE=
\ No newline at end of file
diff --git a/.github/workflows/android-espresso-sandbox.yml b/.github/workflows/android-espresso-sandbox.yml
new file mode 100644
index 0000000000..051be6d52b
--- /dev/null
+++ b/.github/workflows/android-espresso-sandbox.yml
@@ -0,0 +1,283 @@
+name: Android Espresso E2E Sandbox
+
+on:
+ workflow_dispatch:
+ inputs:
+ environment:
+ description: "Select the environment (Sandbox or Automation)"
+ required: true
+ default: "Sandbox"
+ type: choice
+ options:
+ - Sandbox
+ - Main_Workspace
+
+concurrency:
+ group: e2e-android-espresso-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ android-espresso-tests:
+ name: Android Espresso UI Tests
+ runs-on: ubuntu-latest
+ timeout-minutes: 120
+ env:
+ ALLURE_RESULTS_DIR: ${{ github.workspace }}/allure-results-raw
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Force HTTPS for GitHub git dependencies
+ run: |
+ git config --global url."https://github.com/".insteadOf "git@github.com:"
+ git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
+ git config --global url."https://github.com/".insteadOf "git+ssh://git@github.com/"
+ git config --global url."https://github.com/".insteadOf "git://github.com/"
+
+ - name: Rewrite yarn.lock git+ssh URLs to HTTPS
+ run: |
+ if [ -f yarn.lock ]; then
+ sed -i 's#ssh://git@github.com/#https://github.com/#g' yarn.lock
+ sed -i 's#git+ssh://git@github.com/#https://github.com/#g' yarn.lock
+ sed -i 's#git@github.com:#https://github.com/#g' yarn.lock
+ fi
+
+ - name: Free disk space
+ run: |
+ echo "=== Disk before cleanup ==="
+ df -h
+
+ sudo rm -rf /usr/share/dotnet
+ sudo rm -rf /usr/local/lib/android/sdk/ndk/2[0-6]*
+ sudo rm -rf /opt/ghc
+ sudo rm -rf /usr/local/share/boost
+ sudo rm -rf "$AGENT_TOOLSDIRECTORY"
+ sudo rm -rf /usr/local/share/powershell
+ sudo rm -rf /usr/share/swift
+ sudo rm -rf /opt/az
+ sudo rm -rf /usr/local/share/chromium
+ sudo rm -rf /usr/share/miniconda
+
+ # Docker preloads several GB of images on GitHub-hosted runners
+ # that this workflow never uses.
+ docker image prune -af 2>/dev/null || true
+ docker system prune -af --volumes 2>/dev/null || true
+
+ echo "=== Disk after cleanup ==="
+ df -h
+
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.node-version'
+ cache: 'yarn'
+
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Install dependencies
+ env:
+ GIT_TERMINAL_PROMPT: "0"
+ run: yarn install --frozen-lockfile --network-timeout 600000
+
+ - name: Check disk space after dependency install
+ run: |
+ echo "=== Disk after yarn install ==="
+ df -h
+ echo "=== Largest node_modules subdirs ==="
+ du -sh node_modules/* 2>/dev/null | sort -rh | head -15
+
+ - name: Create env file
+ env:
+ ENV_FILE: ${{ secrets.ENV_DEVELOPMENT }}
+ run: |
+ echo "$ENV_FILE" > .env.development
+ # E2E stability: redux-logger serializes the ENTIRE Redux state tree
+ # to logcat on every dispatched action. A recovery-phrase import fires
+ # hundreds of actions (per wallet, per chain, per token) and the
+ # serialization pins the JS thread long enough to blow past the
+ # Espresso wait budgets, so force dev logging off for the automated
+ # run regardless of what the secret carries.
+ sed -i '/^DISABLE_DEVELOPMENT_LOGGING=/d' .env.development
+ echo "DISABLE_DEVELOPMENT_LOGGING=true" >> .env.development
+ # E2E stability: index.js only calls LogBox.ignoreAllLogs() when
+ # IS_MAESTRO === 'true'. Without it, any console.error during bootstrap
+ # (e.g. "zenledgerInitialize: credentials not found" when the secret
+ # carries no ZenLedger creds) throws up the RN LogBox "Console Error"
+ # overlay, which covers the onboarding screen and makes every test
+ # fail on a missing skip-button. Force the flag on for the automated run.
+ sed -i '/^IS_MAESTRO=/d' .env.development
+ echo "IS_MAESTRO=true" >> .env.development
+
+ - name: Set Wallet phrase
+ env:
+ WALLET_PHRASE: ${{ secrets.WALLET_PHRASE }}
+ run: yarn set:WalletPhrase
+
+ - name: Create Sentry properties
+ run: |
+ echo "${{ secrets.SENTRY_PROPERTIES }}" > sentry.properties
+ echo "${{ secrets.SENTRY_PROPERTIES }}" > android/sentry.properties
+
+ - name: Set native config values
+ run: yarn set:dev
+
+ - name: Cache Gradle
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
+ restore-keys: gradle-
+
+ - name: Enable KVM for emulator
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+
+ - name: Start Metro bundler
+ run: |
+ npx react-native start &
+ echo $! > /tmp/metro.pid
+ npx wait-on http://localhost:8081/status --timeout 60000
+
+ - name: Build debug APK and test APK
+ run: |
+ cd android
+ ./gradlew assembleDebug assembleDebugAndroidTest \
+ -PreactNativeArchitectures=x86_64 \
+ --no-daemon
+
+ - name: Check disk space before emulator start
+ run: |
+ echo "=== Disk after APK build, before emulator ==="
+ df -h
+ echo "=== Gradle cache size ==="
+ du -sh ~/.gradle/caches 2>/dev/null || true
+ echo "=== Android build outputs size ==="
+ du -sh android/app/build 2>/dev/null || true
+
+ - name: Start Android emulator and run Espresso tests
+ uses: reactivecircus/android-emulator-runner@v2
+ timeout-minutes: 75
+ with:
+ api-level: 34
+ arch: x86_64
+ profile: pixel_6
+ avd-name: e2e-emulator
+ emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -memory 4096
+ disable-animations: true
+ script: |
+ adb logcat -c
+ adb logcat > /home/runner/work/_temp/device-logcat.txt &
+ echo $! > /tmp/logcat.pid
+ adb reverse tcp:8081 tcp:8081
+ bash scripts/run-tests.sh
+ kill $(cat /tmp/logcat.pid) || true
+
+ - name: Stop Metro bundler
+ if: always()
+ run: kill $(cat /tmp/metro.pid) || true
+
+ - name: Copy Allure results to workspace
+ if: always()
+ run: |
+ mkdir -p allure-results
+ cp -r "$ALLURE_RESULTS_DIR/." allure-results/ || true
+ find allure-results -type f || echo "nothing"
+
+ - name: Check Allure results were produced
+ if: always()
+ run: |
+ if [ -z "$(find allure-results -name '*.json' 2>/dev/null)" ]; then
+ echo "::error::No Allure result JSON files found under allure-results/ — failing the job instead of uploading an empty archive to Testlio."
+ exit 1
+ else
+ echo "Allure results found:"
+ find allure-results -name '*.json'
+ fi
+
+ - name: Upload raw Espresso test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: espresso-android-results
+ path: android/app/build/outputs/androidTest-results/connected/
+ retention-days: 7
+
+ - name: Install Allure CLI
+ if: always()
+ run: |
+ sudo apt-get update -q
+ sudo apt-get install -y -q default-jre-headless
+ curl -fLo allure.tgz https://github.com/allure-framework/allure2/releases/download/2.29.0/allure-2.29.0.tgz
+ tar -xzf allure.tgz
+ sudo mv allure-2.29.0 /opt/allure
+ sudo ln -sf /opt/allure/bin/allure /usr/local/bin/allure
+
+ - name: Zip Allure Results
+ if: always()
+ run: |
+ rm -f allure-results.zip
+ zip -r allure-results.zip allure-results/
+ echo "=== Zip contents ==="
+ unzip -l allure-results.zip | head -30
+
+ - name: Upload Allure Results Artifact
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: allure-results
+ path: allure-results.zip
+ retention-days: 7
+
+ - name: Generate Allure HTML Report
+ if: always()
+ run: allure generate allure-results --clean -o allure-report
+
+ - name: Upload Allure HTML Report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: allure-html-report
+ path: allure-report
+ retention-days: 7
+
+ - name: Install Testlio CLI
+ if: always()
+ run: npm install -g @testlio/cli
+
+ - name: Upload Results to Testlio
+ if: always()
+ env:
+ TESTLIO_DEVICE_ID: ${{ secrets.TESTLIO_ANDROID_DEVICE_ID }}
+ RUN_API_TOKEN: ${{ inputs.environment == 'Sandbox' && secrets.TESTLIO_RUN_TOKEN || secrets.TESTLIO_RUN_TOKEN_BITPAY }}
+ run: |
+ if [ "${{ inputs.environment }}" = "Sandbox" ]; then
+ export TESTLIO_PROJECT_CONFIG="testlio-cli/project-config-sandbox.json"
+ else
+ export TESTLIO_PROJECT_CONFIG="testlio-cli/project-config.json"
+ fi
+ testlio create-run \
+ --testConfig testlio-cli/test-config.json \
+ --projectConfig "$TESTLIO_PROJECT_CONFIG" \
+ --externalResults true \
+ --resultProvider local \
+ --automatedDeviceIds "$TESTLIO_DEVICE_ID"
+
+ testlio parse-run-results \
+ --projectConfig "$TESTLIO_PROJECT_CONFIG" \
+ --path allure-results.zip \
+ --automatedDeviceId "$TESTLIO_DEVICE_ID"
+
+ testlio finalize-results \
+ --projectConfig "$TESTLIO_PROJECT_CONFIG"
diff --git a/.github/workflows/android-espresso.yml b/.github/workflows/android-espresso.yml
new file mode 100644
index 0000000000..ff1a16c881
--- /dev/null
+++ b/.github/workflows/android-espresso.yml
@@ -0,0 +1,274 @@
+name: Android Espresso E2E
+
+on:
+ workflow_run:
+ workflows: ["iOS XCUITest"]
+ types:
+ - completed
+
+concurrency:
+ group: e2e-android-espresso-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ android-espresso-tests:
+ name: Android Espresso UI Tests
+ runs-on: ubuntu-latest
+ timeout-minutes: 120
+ env:
+ ALLURE_RESULTS_DIR: ${{ github.workspace }}/allure-results-raw
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.workflow_run.head_sha }}
+
+ - name: Force HTTPS for GitHub git dependencies
+ run: |
+ git config --global url."https://github.com/".insteadOf "git@github.com:"
+ git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
+ git config --global url."https://github.com/".insteadOf "git+ssh://git@github.com/"
+ git config --global url."https://github.com/".insteadOf "git://github.com/"
+
+ - name: Rewrite yarn.lock git+ssh URLs to HTTPS
+ run: |
+ if [ -f yarn.lock ]; then
+ sed -i 's#ssh://git@github.com/#https://github.com/#g' yarn.lock
+ sed -i 's#git+ssh://git@github.com/#https://github.com/#g' yarn.lock
+ sed -i 's#git@github.com:#https://github.com/#g' yarn.lock
+ fi
+
+ - name: Free disk space
+ run: |
+ echo "=== Disk before cleanup ==="
+ df -h
+
+ sudo rm -rf /usr/share/dotnet
+ sudo rm -rf /usr/local/lib/android/sdk/ndk/2[0-6]*
+ sudo rm -rf /opt/ghc
+ sudo rm -rf /usr/local/share/boost
+ sudo rm -rf "$AGENT_TOOLSDIRECTORY"
+ sudo rm -rf /usr/local/share/powershell
+ sudo rm -rf /usr/share/swift
+ sudo rm -rf /opt/az
+ sudo rm -rf /usr/local/share/chromium
+ sudo rm -rf /usr/share/miniconda
+
+ # Docker preloads several GB of images on GitHub-hosted runners
+ # that this workflow never uses.
+ docker image prune -af 2>/dev/null || true
+ docker system prune -af --volumes 2>/dev/null || true
+
+ echo "=== Disk after cleanup ==="
+ df -h
+
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.node-version'
+ cache: 'yarn'
+
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Install dependencies
+ env:
+ GIT_TERMINAL_PROMPT: "0"
+ run: yarn install --frozen-lockfile --network-timeout 600000
+
+ - name: Check disk space after dependency install
+ run: |
+ echo "=== Disk after yarn install ==="
+ df -h
+ echo "=== Largest node_modules subdirs ==="
+ du -sh node_modules/* 2>/dev/null | sort -rh | head -15
+
+ - name: Create env file
+ env:
+ ENV_FILE: ${{ secrets.ENV_DEVELOPMENT }}
+ run: |
+ echo "$ENV_FILE" > .env.development
+ # E2E stability: redux-logger serializes the ENTIRE Redux state tree
+ # to logcat on every dispatched action. A recovery-phrase import fires
+ # hundreds of actions (per wallet, per chain, per token) and the
+ # serialization pins the JS thread long enough to blow past the
+ # Espresso wait budgets, so force dev logging off for the automated
+ # run regardless of what the secret carries.
+ sed -i '/^DISABLE_DEVELOPMENT_LOGGING=/d' .env.development
+ echo "DISABLE_DEVELOPMENT_LOGGING=true" >> .env.development
+ # E2E stability: index.js only calls LogBox.ignoreAllLogs() when
+ # IS_MAESTRO === 'true'. Without it, any console.error during bootstrap
+ # (e.g. "zenledgerInitialize: credentials not found" when the secret
+ # carries no ZenLedger creds) throws up the RN LogBox "Console Error"
+ # overlay, which covers the onboarding screen and makes every test
+ # fail on a missing skip-button. Force the flag on for the automated run.
+ sed -i '/^IS_MAESTRO=/d' .env.development
+ echo "IS_MAESTRO=true" >> .env.development
+
+ - name: Set Wallet phrase
+ env:
+ WALLET_PHRASE: ${{ secrets.WALLET_PHRASE }}
+ run: yarn set:WalletPhrase
+
+ - name: Create Sentry properties
+ run: |
+ echo "${{ secrets.SENTRY_PROPERTIES }}" > sentry.properties
+ echo "${{ secrets.SENTRY_PROPERTIES }}" > android/sentry.properties
+
+ - name: Set native config values
+ run: yarn set:dev
+
+ - name: Cache Gradle
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
+ restore-keys: gradle-
+
+ - name: Enable KVM for emulator
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+
+ - name: Start Metro bundler
+ run: |
+ npx react-native start &
+ echo $! > /tmp/metro.pid
+ npx wait-on http://localhost:8081/status --timeout 60000
+
+ - name: Build debug APK and test APK
+ run: |
+ cd android
+ ./gradlew assembleDebug assembleDebugAndroidTest \
+ -PreactNativeArchitectures=x86_64 \
+ --no-daemon
+
+ - name: Check disk space before emulator start
+ run: |
+ echo "=== Disk after APK build, before emulator ==="
+ df -h
+ echo "=== Gradle cache size ==="
+ du -sh ~/.gradle/caches 2>/dev/null || true
+ echo "=== Android build outputs size ==="
+ du -sh android/app/build 2>/dev/null || true
+
+ - name: Start Android emulator and run Espresso tests
+ uses: reactivecircus/android-emulator-runner@v2
+ timeout-minutes: 75
+ with:
+ api-level: 34
+ arch: x86_64
+ profile: pixel_6
+ avd-name: e2e-emulator
+ emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -memory 4096
+ disable-animations: true
+ script: |
+ adb logcat -c
+ adb logcat > /home/runner/work/_temp/device-logcat.txt &
+ echo $! > /tmp/logcat.pid
+ adb reverse tcp:8081 tcp:8081
+ bash scripts/run-tests.sh
+ kill $(cat /tmp/logcat.pid) || true
+
+ - name: Stop Metro bundler
+ if: always()
+ run: kill $(cat /tmp/metro.pid) || true
+
+ - name: Copy Allure results to workspace
+ if: always()
+ run: |
+ mkdir -p allure-results
+ cp -r "$ALLURE_RESULTS_DIR/." allure-results/ || true
+ find allure-results -type f || echo "nothing"
+
+ - name: Check Allure results were produced
+ if: always()
+ run: |
+ if [ -z "$(find allure-results -name '*.json' 2>/dev/null)" ]; then
+ echo "::error::No Allure result JSON files found under allure-results/ — failing the job instead of uploading an empty archive to Testlio."
+ exit 1
+ else
+ echo "Allure results found:"
+ find allure-results -name '*.json'
+ fi
+
+ - name: Upload raw Espresso test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: espresso-android-results
+ path: android/app/build/outputs/androidTest-results/connected/
+ retention-days: 7
+
+ - name: Install Allure CLI
+ if: always()
+ run: |
+ sudo apt-get update -q
+ sudo apt-get install -y -q default-jre-headless
+ curl -fLo allure.tgz https://github.com/allure-framework/allure2/releases/download/2.29.0/allure-2.29.0.tgz
+ tar -xzf allure.tgz
+ sudo mv allure-2.29.0 /opt/allure
+ sudo ln -sf /opt/allure/bin/allure /usr/local/bin/allure
+
+ - name: Zip Allure Results
+ if: always()
+ run: |
+ rm -f allure-results.zip
+ zip -r allure-results.zip allure-results/
+ echo "=== Zip contents ==="
+ unzip -l allure-results.zip | head -30
+
+ - name: Upload Allure Results Artifact
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: allure-results
+ path: allure-results.zip
+ retention-days: 7
+
+ - name: Generate Allure HTML Report
+ if: always()
+ run: allure generate allure-results --clean -o allure-report
+
+ - name: Upload Allure HTML Report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: allure-html-report
+ path: allure-report
+ retention-days: 7
+
+ - name: Install Testlio CLI
+ if: always()
+ run: npm install -g @testlio/cli
+
+ - name: Upload Results to Testlio
+ if: always()
+ env:
+ RUN_API_TOKEN: ${{ secrets.TESTLIO_RUN_TOKEN_BITPAY }}
+ TESTLIO_DEVICE_ID: ${{ secrets.TESTLIO_ANDROID_DEVICE_ID }}
+ run: |
+ testlio create-run \
+ --testConfig testlio-cli/test-config.json \
+ --projectConfig testlio-cli/project-config.json \
+ --externalResults true \
+ --resultProvider local \
+ --automatedDeviceIds "$TESTLIO_DEVICE_ID"
+
+ testlio parse-run-results \
+ --projectConfig testlio-cli/project-config.json \
+ --path allure-results.zip \
+ --automatedDeviceId "$TESTLIO_DEVICE_ID"
+
+ testlio finalize-results \
+ --projectConfig testlio-cli/project-config.json
\ No newline at end of file
diff --git a/.github/workflows/ios-xcuitest-sandbox.yml b/.github/workflows/ios-xcuitest-sandbox.yml
new file mode 100644
index 0000000000..cbe750c50d
--- /dev/null
+++ b/.github/workflows/ios-xcuitest-sandbox.yml
@@ -0,0 +1,279 @@
+name: iOS XCUITest Sandbox
+
+on:
+ workflow_dispatch:
+ inputs:
+ environment:
+ description: "Select the environment (Sandbox or Automation)"
+ required: true
+ default: "Sandbox"
+ type: choice
+ options:
+ - Sandbox
+ - Main_Workspace
+
+concurrency:
+ group: e2e-ios-xcui-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ ios-ui-tests:
+ runs-on: macos-latest
+ timeout-minutes: 180
+
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+
+ - name: Cache node_modules
+ id: cache-node-modules
+ uses: actions/cache@v4
+ with:
+ path: node_modules
+ key: node-modules-${{ hashFiles('yarn.lock') }}
+
+ - name: Configure git to use HTTPS instead of SSH
+ run: git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
+
+ - name: Install JS dependencies
+ if: steps.cache-node-modules.outputs.cache-hit != 'true'
+ run: yarn install --frozen-lockfile
+
+ - name: Generate DKLS vendor files
+ run: node ./scripts/generate-dkls-vendor.js
+
+ - name: Create local test environment file
+ run: |
+ cp .env.template .env.development
+ echo "DISABLE_DEVELOPMENT_LOGGING=true" >> .env.development
+ echo "IS_MAESTRO=true" >> .env.development
+
+ - name: Set Wallet phrase
+ env:
+ WALLET_PHRASE: ${{ secrets.WALLET_PHRASE }}
+ run: yarn set:WalletPhrase
+
+ - name: Create local Sentry properties
+ run: |
+ touch sentry.properties ios/sentry.properties
+
+ - name: Set native config values
+ run: yarn set:dev
+
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '2.7.6'
+ bundler-cache: true
+
+ - name: Select Xcode 26.2
+ run: sudo xcode-select -switch /Applications/Xcode_26.2.app
+
+ - name: Debug - installed runtimes
+ run: xcrun simctl list runtimes
+
+ - name: Debug - Xcode and OS version
+ run: |
+ echo "=== Xcode version ==="
+ xcodebuild -version
+ echo "=== macOS version ==="
+ sw_vers
+ echo "=== Available Xcode installations ==="
+ ls /Applications/ | grep Xcode
+
+ - name: List available simulators
+ run: xcrun simctl list devices available
+
+ - name: Clean Xcode caches
+ run: |
+ rm -rf ios/build
+ rm -rf ~/Library/Developer/Xcode/DerivedData || true
+
+ - name: Disable Sentry upload
+ run: echo "SENTRY_DISABLE_AUTO_UPLOAD=true" >> $GITHUB_ENV
+
+ - name: Install Pods
+ run: cd ios && bundle exec pod install
+
+ - name: Debug - Braze pod version
+ run: grep -A2 "Braze" ios/Podfile.lock
+
+ - name: Generate React Native Bundle
+ run: |
+ npx react-native bundle \
+ --entry-file index.js \
+ --platform ios \
+ --dev false \
+ --bundle-output ios/main.jsbundle \
+ --assets-dest ios
+
+ - name: Boot Simulator
+ run: |
+ UDID=$(xcrun simctl list devices available | grep "iPhone 17 (" | grep -v "Pro\|Plus\|Max\|Air\|e (" | head -1 | grep -E -o '\([0-9A-F-]+\)' | tr -d '()')
+ if [ -z "$UDID" ]; then
+ echo "ERROR: Could not find iPhone 17 UDID"
+ exit 1
+ fi
+ echo "Booting simulator UDID: $UDID"
+ xcrun simctl boot "$UDID" || true
+ xcrun simctl bootstatus "$UDID" -b
+ echo "SIMULATOR_UDID=$UDID" >> $GITHUB_ENV
+
+ # Dynamically get the iOS version of the booted simulator
+ IOS_VERSION=$(xcrun simctl list devices available | grep "$UDID" | grep -E -o 'OS:[0-9]+\.[0-9]+' | cut -d: -f2)
+ if [ -z "$IOS_VERSION" ]; then
+ # fallback: get version from runtime list
+ IOS_VERSION=$(xcrun simctl list runtimes | grep "iOS" | head -1 | grep -E -o '[0-9]+\.[0-9]+' | head -1)
+ fi
+ echo "iOS version: $IOS_VERSION"
+ echo "IOS_VERSION=$IOS_VERSION" >> $GITHUB_ENV
+
+ - name: Build for testing
+ env:
+ NODE_OPTIONS: "--max_old_space_size=8192"
+ SENTRY_DISABLE_AUTO_UPLOAD: true
+ run: |
+ export NODE_BINARY=$(which node)
+ export PATH="$(dirname "$NODE_BINARY"):$PATH"
+ set -o pipefail
+ xcodebuild \
+ -workspace ios/BitPayApp.xcworkspace \
+ -scheme BitPayApp \
+ -configuration Release \
+ -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \
+ -derivedDataPath Build \
+ -only-testing:BitPayAppUITests \
+ VALIDATE_WORKSPACE=NO \
+ VALIDATE_PRODUCT=NO \
+ IPHONEOS_DEPLOYMENT_TARGET=${{ env.IOS_VERSION }} \
+ NODE_BINARY="$NODE_BINARY" \
+ build-for-testing 2>&1 | tee build-output.log
+ BUILD_EXIT=${PIPESTATUS[0]}
+ if [ $BUILD_EXIT -ne 0 ]; then
+ echo "=== BUILD ERRORS ==="
+ grep -E "error:" build-output.log | grep -v "warning\|note" | head -50
+ fi
+ exit $BUILD_EXIT
+
+ - name: Upload build log
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-log
+ path: build-output.log
+ retention-days: 7
+
+ - name: Run XCUITest in order
+ run: |
+ set -o pipefail
+ rm -rf TestResults.xcresult
+ xcodebuild \
+ -workspace ios/BitPayApp.xcworkspace \
+ -scheme BitPayApp \
+ -configuration Release \
+ -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \
+ -derivedDataPath Build \
+ -resultBundlePath TestResults.xcresult \
+ -parallel-testing-enabled NO \
+ -only-testing:BitPayAppUITests/Test591_OnboardingCreateWallet \
+ -only-testing:BitPayAppUITests/Test592_ImportWalletRecoveryPhrase \
+ -only-testing:BitPayAppUITests/Test593_BTCConfirmPaymentScreen \
+ -only-testing:BitPayAppUITests/Test594_BasicSwapBTC \
+ -only-testing:BitPayAppUITests/Test595_BasicSellBTC \
+ -only-testing:BitPayAppUITests/Test596_BasicBuyBTC \
+ test-without-building 2>&1 | tee test-output.log
+ TEST_EXIT=${PIPESTATUS[0]}
+ if [ $TEST_EXIT -ne 0 ]; then
+ echo "=== TEST FAILURES ==="
+ grep -E "failed:|error:" test-output.log | head -50
+ fi
+ exit $TEST_EXIT
+
+ - name: Upload XCUITest Results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: xcuitest-results
+ path: TestResults.xcresult
+ retention-days: 7
+
+ - name: Install Allure CLI
+ if: always()
+ run: brew install allure || true
+
+ - name: Install allure-xcresult
+ if: always()
+ run: |
+ if ! command -v allure-xcresult &> /dev/null; then
+ curl -L -o allure-xcresult https://github.com/kvld/allure-xcresult/releases/download/v1.5.0/allure-xcresult
+ chmod +x allure-xcresult
+ sudo mv allure-xcresult /usr/local/bin/
+ fi
+
+ - name: Convert XCResult to Allure Results
+ if: always()
+ run: |
+ allure-xcresult \
+ --input TestResults.xcresult \
+ --output allure-results
+
+ - name: Zip Allure Results
+ if: always()
+ run: |
+ rm -f allure-results.zip
+ zip -r allure-results.zip allure-results
+
+ - name: Upload Allure Results Artifact
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: allure-results
+ path: allure-results.zip
+ retention-days: 7
+
+ - name: Generate Allure HTML Report
+ if: always()
+ run: allure generate allure-results --clean -o allure-report
+
+ - name: Upload Allure HTML Report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: allure-html-report
+ path: allure-report
+ retention-days: 7
+
+ - name: Install Testlio CLI
+ if: always()
+ run: npm install -g @testlio/cli
+
+ - name: Upload Results to Testlio
+ if: always()
+ env:
+ RUN_API_TOKEN: ${{ inputs.environment == 'Sandbox' && secrets.TESTLIO_RUN_TOKEN || secrets.TESTLIO_RUN_TOKEN_BITPAY }}
+ TESTLIO_DEVICE_ID: ${{ secrets.TESTLIO_IOS_DEVICE_ID }}
+ run: |
+ if [ "${{ inputs.environment }}" = "Sandbox" ]; then
+ export TESTLIO_PROJECT_CONFIG="testlio-cli/project-config-sandbox.json"
+ else
+ export TESTLIO_PROJECT_CONFIG="testlio-cli/project-config.json"
+ fi
+ testlio create-run \
+ --testConfig testlio-cli/test-config.json \
+ --projectConfig "$TESTLIO_PROJECT_CONFIG" \
+ --externalResults true \
+ --resultProvider local \
+ --automatedDeviceIds "$TESTLIO_DEVICE_ID"
+
+ testlio parse-run-results \
+ --projectConfig "$TESTLIO_PROJECT_CONFIG" \
+ --path allure-results.zip \
+ --automatedDeviceId "$TESTLIO_DEVICE_ID"
+
+ testlio finalize-results \
+ --projectConfig "$TESTLIO_PROJECT_CONFIG"
\ No newline at end of file
diff --git a/.github/workflows/ios-xcuitest.yml b/.github/workflows/ios-xcuitest.yml
new file mode 100644
index 0000000000..fcb2b3253e
--- /dev/null
+++ b/.github/workflows/ios-xcuitest.yml
@@ -0,0 +1,269 @@
+name: iOS XCUITest
+
+on:
+ schedule:
+ - cron: '0 08 * * *' # every day at 08:00 UTC
+
+concurrency:
+ group: e2e-ios-xcui-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ ios-ui-tests:
+ if: ${{ github.event.schedule == '0 08 * * *' }}
+ runs-on: macos-latest
+ timeout-minutes: 180
+
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@v4
+ with:
+ ref: develop
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+
+ - name: Cache node_modules
+ id: cache-node-modules
+ uses: actions/cache@v4
+ with:
+ path: node_modules
+ key: node-modules-${{ hashFiles('yarn.lock') }}
+
+ - name: Configure git to use HTTPS instead of SSH
+ run: git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
+
+ - name: Install JS dependencies
+ if: steps.cache-node-modules.outputs.cache-hit != 'true'
+ run: yarn install --frozen-lockfile
+
+ - name: Generate DKLS vendor files
+ run: node ./scripts/generate-dkls-vendor.js
+
+ - name: Create local test environment file
+ run: |
+ cp .env.template .env.development
+ echo "DISABLE_DEVELOPMENT_LOGGING=true" >> .env.development
+ echo "IS_MAESTRO=true" >> .env.development
+
+ - name: Set Wallet phrase
+ env:
+ WALLET_PHRASE: ${{ secrets.WALLET_PHRASE }}
+ run: yarn set:WalletPhrase
+
+ - name: Create local Sentry properties
+ run: |
+ touch sentry.properties ios/sentry.properties
+
+ - name: Set native config values
+ run: yarn set:dev
+
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '2.7.6'
+ bundler-cache: true
+
+ - name: Select Xcode 26.2
+ run: sudo xcode-select -switch /Applications/Xcode_26.2.app
+
+ - name: Debug - installed runtimes
+ run: xcrun simctl list runtimes
+
+ - name: Debug - Xcode and OS version
+ run: |
+ echo "=== Xcode version ==="
+ xcodebuild -version
+ echo "=== macOS version ==="
+ sw_vers
+ echo "=== Available Xcode installations ==="
+ ls /Applications/ | grep Xcode
+
+ - name: List available simulators
+ run: xcrun simctl list devices available
+
+ - name: Clean Xcode caches
+ run: |
+ rm -rf ios/build
+ rm -rf ~/Library/Developer/Xcode/DerivedData || true
+
+ - name: Disable Sentry upload
+ run: echo "SENTRY_DISABLE_AUTO_UPLOAD=true" >> $GITHUB_ENV
+
+ - name: Install Pods
+ run: cd ios && bundle exec pod install
+
+ - name: Debug - Braze pod version
+ run: grep -A2 "Braze" ios/Podfile.lock
+
+ - name: Generate React Native Bundle
+ run: |
+ npx react-native bundle \
+ --entry-file index.js \
+ --platform ios \
+ --dev false \
+ --bundle-output ios/main.jsbundle \
+ --assets-dest ios
+
+ - name: Boot Simulator
+ run: |
+ UDID=$(xcrun simctl list devices available | grep "iPhone 17 (" | grep -v "Pro\|Plus\|Max\|Air\|e (" | head -1 | grep -E -o '\([0-9A-F-]+\)' | tr -d '()')
+ if [ -z "$UDID" ]; then
+ echo "ERROR: Could not find iPhone 17 UDID"
+ exit 1
+ fi
+ echo "Booting simulator UDID: $UDID"
+ xcrun simctl boot "$UDID" || true
+ xcrun simctl bootstatus "$UDID" -b
+ echo "SIMULATOR_UDID=$UDID" >> $GITHUB_ENV
+
+ # Dynamically get the iOS version of the booted simulator
+ IOS_VERSION=$(xcrun simctl list devices available | grep "$UDID" | grep -E -o 'OS:[0-9]+\.[0-9]+' | cut -d: -f2)
+ if [ -z "$IOS_VERSION" ]; then
+ # fallback: get version from runtime list
+ IOS_VERSION=$(xcrun simctl list runtimes | grep "iOS" | head -1 | grep -E -o '[0-9]+\.[0-9]+' | head -1)
+ fi
+ echo "iOS version: $IOS_VERSION"
+ echo "IOS_VERSION=$IOS_VERSION" >> $GITHUB_ENV
+
+ - name: Build for testing
+ env:
+ NODE_OPTIONS: "--max_old_space_size=8192"
+ SENTRY_DISABLE_AUTO_UPLOAD: true
+ run: |
+ export NODE_BINARY=$(which node)
+ export PATH="$(dirname "$NODE_BINARY"):$PATH"
+ set -o pipefail
+ xcodebuild \
+ -workspace ios/BitPayApp.xcworkspace \
+ -scheme BitPayApp \
+ -configuration Release \
+ -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \
+ -derivedDataPath Build \
+ -only-testing:BitPayAppUITests \
+ VALIDATE_WORKSPACE=NO \
+ VALIDATE_PRODUCT=NO \
+ IPHONEOS_DEPLOYMENT_TARGET=${{ env.IOS_VERSION }} \
+ NODE_BINARY="$NODE_BINARY" \
+ build-for-testing 2>&1 | tee build-output.log
+ BUILD_EXIT=${PIPESTATUS[0]}
+ if [ $BUILD_EXIT -ne 0 ]; then
+ echo "=== BUILD ERRORS ==="
+ grep -E "error:" build-output.log | grep -v "warning\|note" | head -50
+ fi
+ exit $BUILD_EXIT
+
+ - name: Upload build log
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-log
+ path: build-output.log
+ retention-days: 7
+
+ - name: Run XCUITest in order
+ run: |
+ set -o pipefail
+ rm -rf TestResults.xcresult
+ xcodebuild \
+ -workspace ios/BitPayApp.xcworkspace \
+ -scheme BitPayApp \
+ -configuration Release \
+ -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \
+ -derivedDataPath Build \
+ -resultBundlePath TestResults.xcresult \
+ -parallel-testing-enabled NO \
+ -only-testing:BitPayAppUITests/Test591_OnboardingCreateWallet \
+ -only-testing:BitPayAppUITests/Test592_ImportWalletRecoveryPhrase \
+ -only-testing:BitPayAppUITests/Test593_BTCConfirmPaymentScreen \
+ -only-testing:BitPayAppUITests/Test594_BasicSwapBTC \
+ -only-testing:BitPayAppUITests/Test595_BasicSellBTC \
+ -only-testing:BitPayAppUITests/Test596_BasicBuyBTC \
+ test-without-building 2>&1 | tee test-output.log
+ TEST_EXIT=${PIPESTATUS[0]}
+ if [ $TEST_EXIT -ne 0 ]; then
+ echo "=== TEST FAILURES ==="
+ grep -E "failed:|error:" test-output.log | head -50
+ fi
+ exit $TEST_EXIT
+
+ - name: Upload XCUITest Results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: xcuitest-results
+ path: TestResults.xcresult
+ retention-days: 7
+
+ - name: Install Allure CLI
+ if: always()
+ run: brew install allure || true
+
+ - name: Install allure-xcresult
+ if: always()
+ run: |
+ if ! command -v allure-xcresult &> /dev/null; then
+ curl -L -o allure-xcresult https://github.com/kvld/allure-xcresult/releases/download/v1.5.0/allure-xcresult
+ chmod +x allure-xcresult
+ sudo mv allure-xcresult /usr/local/bin/
+ fi
+
+ - name: Convert XCResult to Allure Results
+ if: always()
+ run: |
+ allure-xcresult \
+ --input TestResults.xcresult \
+ --output allure-results
+
+ - name: Zip Allure Results
+ if: always()
+ run: |
+ rm -f allure-results.zip
+ zip -r allure-results.zip allure-results
+
+ - name: Upload Allure Results Artifact
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: allure-results
+ path: allure-results.zip
+ retention-days: 7
+
+ - name: Generate Allure HTML Report
+ if: always()
+ run: allure generate allure-results --clean -o allure-report
+
+ - name: Upload Allure HTML Report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: allure-html-report
+ path: allure-report
+ retention-days: 7
+
+ - name: Install Testlio CLI
+ if: always()
+ run: npm install -g @testlio/cli
+
+ - name: Upload Results to Testlio
+ if: always()
+ env:
+ RUN_API_TOKEN: ${{ secrets.TESTLIO_RUN_TOKEN_BITPAY }}
+ TESTLIO_DEVICE_ID: ${{ secrets.TESTLIO_IOS_DEVICE_ID }}
+ run: |
+ testlio create-run \
+ --testConfig testlio-cli/test-config.json \
+ --projectConfig testlio-cli/project-config.json \
+ --externalResults true \
+ --resultProvider local \
+ --automatedDeviceIds "$TESTLIO_DEVICE_ID"
+
+ testlio parse-run-results \
+ --projectConfig testlio-cli/project-config.json \
+ --path allure-results.zip \
+ --automatedDeviceId "$TESTLIO_DEVICE_ID"
+
+ testlio finalize-results \
+ --projectConfig testlio-cli/project-config.json
\ No newline at end of file
diff --git a/README.md b/README.md
index 7e44a57190..0bcf15c648 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,11 @@
+# Tests
+
# BitPay App v2
+
Welcome to BitPay App v2!
## Get Started
+
Install dependencies with `yarn` >= 14.15.0
## Translations
@@ -17,55 +21,64 @@ and the review workflow. Run `yarn translation:check` before submitting changes.
3. Build and deploy to simulator `yarn ios` or device `yarn ios:device`
### Android
+
1. `yarn start` to start dev server
2. Build and deploy to simulator or device `yarn android`
#### Accessing your local server
+
To make requests to your local server, first take your local BitPay server cert and copy it into `android/app/src/main/res/raw` folder in either .pem or .der format.
+
- To convert .crt to .der, run: `openssl x509 -in your_cert_name.crt -out your_cert_name.der -outform DER`
Then open `android/app/src/main/res/xml/network_security_config.xml` and add your local IP to the domain-config and your cert to the trust-anchors (without the file extension).
-For example if your local IP is `123.0.0.7` and your cert is in `res/raw/lbitpay.der`:
+For example if your local IP is `123.0.0.7` and your cert is in `res/raw/lbitpay.der`:
-
+
...
123.0.0.7
-
-
-
-
-
+
### Redux Dev Tools
+
This project uses `react-redux` https://react-redux.js.org/ for state management. To take advantage of the tooling available, go to https://github.com/jhen0409/react-native-debugger and install the debugger.
-1. To enable - make sure debugger is open - tap `D` in the hosting terminal window or shake the device
+1. To enable - make sure debugger is open - tap `D` in the hosting terminal window or shake the device
2. A menu will popup - tap `Debug with Chrome`(IOS) or `Debug`(Android)
3. The logs should move from the terminal to the debugger
### Storybook
+
1. In `src/contants/config.ts` change `APP_LOAD_STORY_BOOK=false` to `APP_LOAD_STORY_BOOK=true`
2. Run `yarn ` ex: `yarn ios`. Since we set `APP_LOAD_STORY_BOOK=true`, this runs Storybook instead of your actual app.
## Deeplinking
-Test deeplinking via command line with these commands (note: ampersand must be escaped for multiple params):
+
+Test deeplinking via command line with these commands (note: ampersand must be escaped for multiple params):
#### iOS
+
`npx uri-scheme open "bitpay://your/deeplink/path?params1=foo1\¶m2=foo2" --ios`
#### Android
+
`npx uri-scheme open "bitpay://your/deeplink/path?param1=foo1\¶m2=foo2" --android`
### Modifying the intent prefix
+
If you want to associate the app with a different intent prefix eg. `myapp://`:
1. Open `src/constants/config.ts`
2. Modify `APP_DEEPLINK_PREFIX` to your desired prefix eg. `myapp://` (with colon and slashes)
3. Update the OS specific configs:
- #### iOS
+
+#### iOS
+
1. Open `ios/BitPayApp/info.plist`
2. Find `CFBundleURLSchemes` and modify the value to your desired prefix (without colon and slashes) eg. `myapp`
- #### Android
+
+#### Android
+
1. Open `android/app/src/main/AndroidManifest.xml`
2. Locate `` and modify the value to your desired prefix (without colon and slashes) eg. `myapp`
diff --git a/android/app/build.gradle b/android/app/build.gradle
index d238784d62..2252b5357e 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -150,6 +150,7 @@ android {
versionCode 91212433
versionName "14.46.0"
missingDimensionStrategy 'react-native-camera', 'mlkit'
+ testInstrumentationRunner "io.qameta.allure.android.runners.AllureAndroidJUnitRunner"
ndk {
abiFilters "armeabi-v7a", "x86_64", "arm64-v8a"
}
@@ -196,6 +197,22 @@ android {
}
dependencies {
+ // Espresso libraries
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
+ androidTestImplementation 'androidx.test:runner:1.5.2'
+ androidTestImplementation 'androidx.test:rules:1.5.0'
+ androidTestImplementation 'androidx.test:core:1.5.0'
+ androidTestImplementation 'org.hamcrest:hamcrest:2.2'
+ androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
+
+ // Allure reporting
+ androidTestImplementation "io.qameta.allure:allure-kotlin-model:2.4.0"
+ androidTestImplementation "io.qameta.allure:allure-kotlin-commons:2.4.0"
+ androidTestImplementation "io.qameta.allure:allure-kotlin-junit4:2.4.0"
+ androidTestImplementation "io.qameta.allure:allure-kotlin-android:2.4.0"
+
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation platform('com.google.firebase:firebase-bom:29.1.0')
@@ -205,6 +222,10 @@ dependencies {
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
implementation("androidx.core:core-splashscreen:1.0.0")
+ // Test Orchestrator + TestStorage
+ androidTestUtil "androidx.test:orchestrator:1.5.1"
+ androidTestImplementation "androidx.test.services:test-services:1.5.0"
+
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
diff --git a/android/app/src/androidTest/AndroidManifest.xml b/android/app/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000000..343a7b66c4
--- /dev/null
+++ b/android/app/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/TestConfig.kt b/android/app/src/androidTest/java/com/bitpay/wallet/TestConfig.kt
new file mode 100644
index 0000000000..abb03af63b
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/TestConfig.kt
@@ -0,0 +1,5 @@
+package com.bitpay.wallet
+
+object TestConfig {
+ const val WALLET_PHRASE = "WALLET_PHRASE_REPLACE_ME"
+}
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/base/BaseTest.kt b/android/app/src/androidTest/java/com/bitpay/wallet/base/BaseTest.kt
new file mode 100644
index 0000000000..65ead01ce7
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/base/BaseTest.kt
@@ -0,0 +1,195 @@
+package com.bitpay.wallet.base
+
+import android.content.Intent
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.bitpay.wallet.pages.OnboardingPage
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import com.bitpay.wallet.utils.allureScreenshot
+import com.bitpay.wallet.utils.WaitUtils
+import org.junit.Rule
+import androidx.test.uiautomator.Until
+import androidx.test.uiautomator.By
+import com.bitpay.wallet.utils.RetryRule
+import org.junit.After
+import org.junit.rules.TestName
+
+open class BaseTest {
+
+ companion object {
+ var skipRelaunch: Boolean = false
+ var skipOnboardingHandling: Boolean = false
+ }
+
+ /**
+ * Number of retry attempts for this test class. Defaults to 3.
+ *
+ * IMPORTANT: Retries here happen IN-PROCESS — the same app instance is
+ * reused across attempts, launchApp() only clears the Activity stack
+ * (FLAG_ACTIVITY_CLEAR_TASK), not persisted app data (prefs, DB, wallet
+ * state). A true "pm clear"-style reset can't safely run from inside
+ * the same process hosting the instrumentation (it would kill the test
+ * harness itself), so retries are only safe for read-only/idempotent
+ * tests (navigation checks, text assertions).
+ *
+ * For state-mutating flows (e.g. wallet creation, onboarding), override
+ * this to 1 in the test class so a genuine failure surfaces immediately
+ * instead of retrying against an app that's now in a different state
+ * than attempt 1 expected:
+ *
+ * override val retryCount = 1
+ */
+ protected open val retryCount: Int = 3
+
+ // Outer rule (order 0): captures a screenshot only once all retries
+ // are exhausted and the test has truly failed.
+ @get:Rule(order = 0)
+ val screenshotOnFailureRule = object : TestWatcher() {
+ override fun failed(e: Throwable?, description: Description?) {
+ allureScreenshot("Failure - ${description?.methodName}")
+ }
+ }
+
+ // Inner rule (order 1): re-runs @Before -> test -> @After up to
+ // retryCount times before letting a failure propagate outward.
+ @get:Rule(order = 1)
+ val retryRule by lazy { RetryRule(retryCount) }
+
+ val testName = TestName()
+
+ private val onboardingPage = OnboardingPage()
+
+ @Before
+ fun setup() {
+ if (!skipRelaunch) launchApp()
+ dismissNearbyDevicesPrompt(allow = true)
+ dismissLogboxIfPresent()
+ dismissDebuggerNotificationIfPresent()
+ if (!skipOnboardingHandling) handleOnboardingIfPresent()
+ }
+
+ @After
+ fun stopRecording() {
+ // val video = ScreenRecorder.stop()
+ // allureVideo("Screen Recording - ${testName.methodName}", video)
+ }
+
+ private fun launchApp() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ val context = instrumentation.targetContext
+
+ val device: UiDevice? = try {
+ UiDevice.getInstance(instrumentation)
+ } catch (e: IllegalStateException) {
+ android.util.Log.w("BaseTest", "UiAutomation already registered: ${e.message}")
+ null
+ }
+
+ device?.let { if (!it.isScreenOn) it.wakeUp() }
+
+ val intent = context.packageManager.getLaunchIntentForPackage("com.bitpay.wallet")
+ intent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ context.startActivity(intent)
+
+ // CI emulators are typically slower than local hardware to load and
+ // render the JS bundle. Give RN's startup a generous window before any
+ // test attempts its first onView() lookup, rather than racing ahead.
+ device?.wait(Until.hasObject(By.pkg("com.bitpay.wallet").depth(0)), 20000)
+ Thread.sleep(2000)
+ }
+
+ /**
+ * Dismisses the React Native Logbox error overlay if it is present.
+ * The overlay (shown only in DEV builds) blocks all UI interaction, so it
+ * must be cleared before any test can proceed. Loops up to 5 times to
+ * handle multiple stacked errors (e.g. "Log 2 of 2").
+ */
+ private fun dismissLogboxIfPresent() {
+ val logboxIndicator = withText("Console Error")
+ val dismissButton = withText("Dismiss")
+
+ repeat(5) {
+ try {
+ WaitUtils.waitForView(logboxIndicator, timeoutMs = 3000)
+ android.util.Log.w("BaseTest", "Logbox overlay detected — dismissing")
+ onView(dismissButton).perform(click())
+ Thread.sleep(500)
+ } catch (_: Throwable) {
+ return
+ }
+ }
+ }
+
+ /**
+ * Dismisses React Native's "Open debugger to view warnings." notification if
+ * it is showing. In dev builds a console warning surfaces this pill pinned to
+ * the bottom of the screen, where it overlays — and swallows taps meant for —
+ * bottom-aligned buttons such as "Continue without an account". RN only shows
+ * it once per session, so a single dismissal holds. No-op if it isn't present.
+ */
+ private fun dismissDebuggerNotificationIfPresent() {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ val selector = By.descContains("Open debugger to view warnings")
+ val pill = device.wait(Until.findObject(selector), 5000) ?: return
+
+ android.util.Log.w("BaseTest", "Debugger-warnings notification present — dismissing")
+
+ val bounds = pill.visibleBounds
+ // The dismiss "X" is the right-most clickable child; fall back to a tap at
+ // the notification's trailing edge if it can't be resolved.
+ val dismiss = pill.findObjects(By.clickable(true))
+ .filter { it.visibleBounds != bounds }
+ .maxByOrNull { it.visibleBounds.centerX() }
+ dismiss?.click() ?: device.click(bounds.right - 40, bounds.centerY())
+ device.waitForIdle()
+
+ if (device.hasObject(selector)) {
+ device.click(bounds.right - 40, bounds.centerY())
+ device.waitForIdle()
+ }
+ Thread.sleep(300)
+ }
+
+ /**
+ * If the onboarding "Continue without an account" screen is currently
+ * displayed, walk through the standard onboarding flow once so every
+ * test starts from a consistent post-onboarding state.
+ * If that screen isn't present (e.g. app is already past onboarding,
+ * or skipRelaunch left it on a different screen), this is a no-op.
+ */
+ private fun handleOnboardingIfPresent() {
+ if (!onboardingPage.isContinueWithoutAccountDisplayed()) {
+ return
+ }
+
+ onboardingPage.waitForPageToLoad()
+ dismissDebuggerNotificationIfPresent()
+ onboardingPage.clickContinueWithoutAccount()
+ onboardingPage.clickSkip() // Skip turn on notifications
+
+ assertTrue(
+ "Protect Your Wallet was not displayed",
+ onboardingPage.verifyProtectYourWalletIsDisplayed()
+ )
+ onboardingPage.clickSkip()
+ }
+
+ private fun dismissNearbyDevicesPrompt(allow: Boolean) {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ val buttonId = if (allow) "permission_allow_button" else "permission_deny_button"
+ val button = listOf(
+ "com.android.permissioncontroller",
+ "com.google.android.permissioncontroller",
+ ).firstNotNullOfOrNull { pkg ->
+ device.wait(Until.findObject(By.res(pkg, buttonId)), 2000)
+ } ?: return
+ button.click()
+ device.waitForIdle()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/AddYourCryptoOptionPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/AddYourCryptoOptionPage.kt
new file mode 100644
index 0000000000..3c0e0d1f9f
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/AddYourCryptoOptionPage.kt
@@ -0,0 +1,27 @@
+package com.bitpay.wallet.pages
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import com.bitpay.wallet.utils.WaitUtils
+import com.bitpay.wallet.utils.WaitUtils.withTestId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import org.hamcrest.Matchers.allOf
+
+class AddYourCryptoOptionPage {
+
+ // ---- Locators ----
+ private val importKeyButton = withTestId("creation-options-import-button")
+
+
+ // ---- Actions ----
+
+
+ fun clickImportKey() {
+ WaitUtils.waitForView(importKeyButton)
+ onView(importKeyButton).perform(click())
+ }
+
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/ConfirmPaymentPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/ConfirmPaymentPage.kt
new file mode 100644
index 0000000000..236d07b380
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/ConfirmPaymentPage.kt
@@ -0,0 +1,43 @@
+package com.bitpay.wallet.pages
+
+import androidx.test.espresso.Espresso.onView
+import com.bitpay.wallet.utils.WaitUtils
+import com.bitpay.wallet.utils.WaitUtils.withTestId
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.pressImeActionButton
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+
+class ConfirmPaymentPage {
+
+ // ---- Locators ----
+ private val confirmPaymentText = withText("Confirm Payment")
+
+ private val summaryText = withText("SUMMARY")
+
+
+ // ---- Actions ----
+
+ fun verifyConfirmPaymentTitleDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(confirmPaymentText)
+ onView(confirmPaymentText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifySummaryTextDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(summaryText)
+ onView(summaryText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/HomePage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/HomePage.kt
new file mode 100644
index 0000000000..01a15db9ef
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/HomePage.kt
@@ -0,0 +1,95 @@
+package com.bitpay.wallet.pages
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import com.bitpay.wallet.utils.WaitUtils
+import com.bitpay.wallet.utils.WaitUtils.withTestId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import java.util.regex.Pattern
+
+class HomePage {
+
+ // ---- Locators ----
+ private val portfolioBalanceText = withTestId("portfolio-balance-info-button")
+ private val myCryptoAddButton = withTestId("my-crypto-add-button")
+ private val buyButton = withTestId("buy-button")
+ private val sellButton = withTestId("sell-button")
+ private val swapButton = withTestId("swap-button")
+ private val receiveButton = withTestId("receive-button")
+ private val sendButton = withTestId("send-button")
+
+
+ // ---- Actions ----
+
+ fun waitForPageToLoad() {
+ WaitUtils.waitForView(
+ portfolioBalanceText,
+ timeoutMs = 120000
+ )
+ }
+
+ fun verifyPortfolioBalanceTextDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(portfolioBalanceText)
+ onView(portfolioBalanceText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun clickAddYourCrypto() {
+ WaitUtils.waitForView(myCryptoAddButton)
+ onView(myCryptoAddButton).perform(click())
+ }
+
+ fun clickBuy() {
+ WaitUtils.waitForView(buyButton)
+ onView(buyButton).perform(click())
+ }
+
+ fun clickSell() {
+ WaitUtils.waitForView(sellButton)
+ onView(sellButton).perform(click())
+ }
+
+ fun clickSwap() {
+ WaitUtils.waitForView(swapButton)
+ onView(swapButton).perform(click())
+ }
+
+ fun clickReceive() {
+ WaitUtils.waitForView(receiveButton)
+ onView(receiveButton).perform(click())
+ }
+
+ fun clickSend() {
+ // WaitUtils.waitForView(sendButton)
+ WaitUtils.waitForViewEnabled(sendButton)
+ onView(sendButton).perform(click())
+ }
+
+
+ fun waitForFundedBalance(timeoutMs: Long = 180000, intervalMs: Long = 1000) {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ val amount = Pattern.compile("^\\$[\\d,]+(\\.\\d+)?$")
+ val end = System.currentTimeMillis() + timeoutMs
+
+ while (System.currentTimeMillis() < end) {
+ // The portfolio balance is the top-most dollar amount on Home.
+ val balance = device.findObjects(By.text(amount))
+ .minByOrNull { it.visibleBounds.top }
+ ?.text
+ if (balance != null && balance.any { it in '1'..'9' }) {
+ return
+ }
+ Thread.sleep(intervalMs)
+ }
+ }
+}
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/ImportWalletPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/ImportWalletPage.kt
new file mode 100644
index 0000000000..e651c6495f
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/ImportWalletPage.kt
@@ -0,0 +1,132 @@
+package com.bitpay.wallet.pages
+
+import androidx.test.espresso.Espresso.onView
+import com.bitpay.wallet.utils.WaitUtils
+import com.bitpay.wallet.utils.WaitUtils.withTestId
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.pressImeActionButton
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+
+class ImportWalletPage {
+
+ // ---- Locators ----
+ private val recoveryPhraseInputField = withTestId("import-text-input")
+ private val importWalletButton = withTestId("import-wallet-button")
+
+ // The import pins the app's main thread for minutes; Espresso onView() polling
+ // blocks on idle-sync and starves it further, so the wait below uses
+ // UiAutomator (no idle-sync) and matches by text / content-description.
+ private val ongoingProcessDesc = By.desc("ongoing-process-message")
+ private val importInProgressText = By.textStartsWith("Loading ")
+ private val importWalletButtonDesc = By.desc("Import wallet")
+
+ // Any one of these means the import has moved off the Import screen.
+ private val firstTermCheckboxDesc = By.desc("first-term-checkbox")
+ private val termsHeadingText = By.text("I understand that:")
+ private val agreeAndContinueText = By.text("Agree and Continue")
+ private val backupPromptText = By.text("Would you like to backup your key?")
+ private val myKeyText = By.text("My Key")
+
+ private val importAdvanceBudgetMs = 600_000L
+ private val resubmitCooldownMs = 30_000L
+
+ private val device: UiDevice
+ get() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+
+ // ---- Actions ----
+
+ fun pressKeyboardTick() {
+ onView(recoveryPhraseInputField).perform(pressImeActionButton())
+ }
+
+ fun enterRecoveryPhrase(recoveryPhrase: String) {
+ WaitUtils.waitForView(recoveryPhraseInputField)
+ onView(recoveryPhraseInputField).perform(
+ ViewActions.click(),
+ ViewActions.typeText(recoveryPhrase),
+ ViewActions.closeSoftKeyboard()
+ )
+ }
+
+ fun clickImportWallet() {
+ WaitUtils.waitForView(importWalletButton)
+ submitImport()
+
+ if (waitForImportFlowAdvance(importAdvanceBudgetMs)) {
+ return
+ }
+
+ throw RuntimeException(
+ "Import did not progress to backup/terms/home screen within " +
+ "${importAdvanceBudgetMs / 1000}s (still stuck on import/loading)."
+ )
+ }
+
+ // First tap while the app is still idle — Espresso is fine here.
+ private fun submitImport(): Boolean {
+ return try {
+ onView(importWalletButton).perform(click())
+ true
+ } catch (_: Throwable) {
+ try {
+ onView(importWalletButton).perform(WaitUtils.forceClick)
+ true
+ } catch (_: Throwable) {
+ false
+ }
+ }
+ }
+
+ private fun resubmitViaUiAutomator(): Boolean {
+ val btn = device.findObject(importWalletButtonDesc)
+ ?: device.findObject(By.text("Import Wallet"))
+ ?: return false
+ return try {
+ btn.click()
+ true
+ } catch (_: Throwable) {
+ false
+ }
+ }
+
+ private fun waitForImportFlowAdvance(timeoutMs: Long): Boolean {
+ val end = System.currentTimeMillis() + timeoutMs
+ var lastResubmitAt = System.currentTimeMillis()
+
+ while (System.currentTimeMillis() < end) {
+ if (device.hasObject(firstTermCheckboxDesc) ||
+ device.hasObject(termsHeadingText) ||
+ device.hasObject(agreeAndContinueText) ||
+ device.hasObject(backupPromptText) ||
+ device.hasObject(myKeyText)
+ ) {
+ return true
+ }
+
+ if (device.hasObject(ongoingProcessDesc) ||
+ device.hasObject(importInProgressText)
+ ) {
+ Thread.sleep(1_500)
+ continue
+ }
+
+ // Back on the Import screen and idle — the first tap may have been
+ // dropped. Re-submit, at most once per cooldown.
+ if (device.hasObject(importWalletButtonDesc) &&
+ System.currentTimeMillis() - lastResubmitAt > resubmitCooldownMs
+ ) {
+ resubmitViaUiAutomator()
+ lastResubmitAt = System.currentTimeMillis()
+ }
+
+ Thread.sleep(1_000)
+ }
+
+ return device.wait(Until.hasObject(firstTermCheckboxDesc), 5_000) != null
+ }
+}
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/KeyboardPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/KeyboardPage.kt
new file mode 100644
index 0000000000..4f8fb51e34
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/KeyboardPage.kt
@@ -0,0 +1,122 @@
+package com.bitpay.wallet.pages
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.click
+import com.bitpay.wallet.utils.WaitUtils
+import com.bitpay.wallet.utils.WaitUtils.withTestId
+import androidx.test.espresso.action.CoordinatesProvider
+import androidx.test.espresso.action.GeneralClickAction
+import androidx.test.espresso.action.Press
+import androidx.test.espresso.action.Tap
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.withParent
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import android.view.View
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers.allOf
+
+class KeyboardPage {
+
+ // ---- Locators ----
+
+ private val continueButton = withText("Continue")
+
+
+ // ---- Actions ----
+ fun enterAmount(amount: String = "0") {
+ for (char in amount) {
+ // Key labels are nested under per-key containers; sibling checks must
+ // be applied at the parent level, not directly between text views.
+ val key: Matcher = when (char) {
+ '0' -> allOf(
+ withText("0"),
+ withParent(hasSibling(hasDescendant(withText("."))))
+ )
+ else -> withText(char.toString())
+ }
+ WaitUtils.waitForView(key)
+ Thread.sleep(200)
+ onView(key).perform(click())
+ }
+ }
+
+ fun clickContinue() {
+ WaitUtils.waitForViewEnabled(continueButton)
+ onView(continueButton).perform(click())
+ }
+
+
+ /**
+ * Backspace key does not expose a unique accessibility identifier.
+ * Calculate its position dynamically using the relative spacing
+ * between the keypad keys "6" and "9".
+ */
+ fun clickBackspace(count: Int = 1) {
+ // The digit labels are not direct siblings; constrain via each label's
+ // parent being adjacent to the expected neighbor key container.
+ val sixKey = allOf(
+ withText("6"),
+ withParent(hasSibling(hasDescendant(withText("5"))))
+ )
+ val nineKey = allOf(
+ withText("9"),
+ withParent(hasSibling(hasDescendant(withText("8"))))
+ )
+
+ WaitUtils.waitForView(sixKey)
+ WaitUtils.waitForView(nineKey)
+
+ var sixView: View? = null
+ var nineView: View? = null
+
+ onView(sixKey).perform(object : androidx.test.espresso.ViewAction {
+ override fun getConstraints() = org.hamcrest.Matchers.any(View::class.java)
+ override fun getDescription() = "capture six key view"
+ override fun perform(uiController: androidx.test.espresso.UiController, view: View) {
+ sixView = view
+ }
+ })
+
+ onView(nineKey).perform(object : androidx.test.espresso.ViewAction {
+ override fun getConstraints() = org.hamcrest.Matchers.any(View::class.java)
+ override fun getDescription() = "capture nine key view"
+ override fun perform(uiController: androidx.test.espresso.UiController, view: View) {
+ nineView = view
+ }
+ })
+
+ val sixLocation = IntArray(2)
+ val nineLocation = IntArray(2)
+ sixView!!.getLocationOnScreen(sixLocation)
+ nineView!!.getLocationOnScreen(nineLocation)
+
+ val sixMidY = sixLocation[1] + (sixView!!.height / 2)
+ val nineMidY = nineLocation[1] + (nineView!!.height / 2)
+ val nineMidX = nineLocation[0] + (nineView!!.width / 2)
+
+ val rowHeight = Math.abs(nineMidY - sixMidY)
+
+ val backspaceX = nineMidX.toFloat()
+ val backspaceY = (nineMidY + rowHeight).toFloat()
+
+ val backspaceCoordinatesProvider = CoordinatesProvider { _ ->
+ floatArrayOf(backspaceX, backspaceY)
+ }
+
+ repeat(count) {
+ onView(nineKey).perform(
+ GeneralClickAction(
+ Tap.SINGLE,
+ backspaceCoordinatesProvider,
+ Press.FINGER,
+ android.view.InputDevice.SOURCE_UNKNOWN,
+ android.view.MotionEvent.BUTTON_PRIMARY
+ )
+ )
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/MyKeyPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/MyKeyPage.kt
new file mode 100644
index 0000000000..f50418dfb8
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/MyKeyPage.kt
@@ -0,0 +1,50 @@
+package com.bitpay.wallet.pages
+
+import androidx.test.espresso.Espresso.onView
+import com.bitpay.wallet.utils.WaitUtils
+import com.bitpay.wallet.utils.WaitUtils.withTestId
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.pressImeActionButton
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+
+class MyKeyPage {
+
+ // ---- Locators ----
+ private val myKeyText = withText("My Key")
+ private val bitcoinText = withText("Bitcoin")
+
+
+ // ---- Actions ----
+
+ fun waitForPageToLoad() {
+ WaitUtils.waitForView(
+ myKeyText,
+ timeoutMs = 900000
+ )
+ }
+
+ fun verifyMyKeyDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(myKeyText, 900000)
+ onView(myKeyText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifyBitcoinTextDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(bitcoinText)
+ onView(bitcoinText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/OnboardingPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/OnboardingPage.kt
new file mode 100644
index 0000000000..a36ecff2c9
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/OnboardingPage.kt
@@ -0,0 +1,403 @@
+package com.bitpay.wallet.pages
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import com.bitpay.wallet.utils.WaitUtils
+import com.bitpay.wallet.utils.WaitUtils.withTestId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.isClickable
+import android.view.View
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.anyOf
+import org.hamcrest.Matchers.containsString
+import org.hamcrest.Matchers.startsWith
+import org.hamcrest.Matcher
+import org.junit.Assert.assertTrue
+import com.bitpay.wallet.utils.WaitUtils.withIndex
+
+class OnboardingPage {
+
+ // ---- Locators ----
+ private val getStartedButton = withTestId("get-started-button")
+ private val continueWithoutAccountButton = withTestId("continue-without-an-account-button")
+ private val skipButton = withTestId("skip-button")
+ private val createKeyButton = withTestId("create-a-key-button")
+ private val alreadyHaveKeyButton = withTestId("i-already-have-a-key-button")
+ private val protectYourWalletText = withText("Protect your wallet")
+ private val backupKeyPromptText = withText("Would you like to backup your key?")
+ private val bottomSheetLaterButton = withTestId("bottom-notification-secondary-action-button")
+ private val bottomSheetBackupYourKeyButton =
+ withTestId("bottom-notification-primary-action-button")
+ private val backupRecoveryPhraseElement = withTestId("go-to-backup-button")
+ private val importTitleText = withText("Import")
+ private val importWalletButton = withTestId("import-wallet-button")
+
+ private val ongoingProcessMessage = withTestId("ongoing-process-message")
+ private val loadingTokensText = withText(
+ anyOf(
+ startsWith("Loading "),
+ startsWith("Adding "),
+ startsWith("Searching "),
+ startsWith("Checking "),
+ startsWith("Deriving "),
+ startsWith("Found "),
+ startsWith("No wallets"),
+ startsWith("No more wallets"),
+ startsWith("Getting wallet"),
+ containsString("Almost there"),
+ )
+ )
+ private val portfolioBalanceText = withTestId("portfolio-balance-info-button")
+ private val myKeyText = withText("My Key")
+
+ private fun iUnderstandCheckBox1Matchers(): List> {
+ return listOf(
+ allOf(withTestId("first-term-checkbox"), isClickable()),
+ withIndex(withTestId("first-term-checkbox"), 1),
+ withIndex(withTestId("first-term-checkbox"), 0)
+ )
+ }
+
+ private fun iUnderstandCheckBox2Matchers(): List> {
+ return listOf(
+ allOf(withTestId("second-term-checkbox"), isClickable()),
+ withIndex(withTestId("second-term-checkbox"), 1),
+ withIndex(withTestId("second-term-checkbox"), 0)
+ )
+ }
+
+ private fun iUnderstandCheckBox3Matchers(): List> {
+ return listOf(
+ allOf(withTestId("third-term-checkbox"), isClickable()),
+ withIndex(withTestId("third-term-checkbox"), 1),
+ withIndex(withTestId("third-term-checkbox"), 0)
+ )
+ }
+
+// private val iUnderstandCheckBox2 = withTestId("second-term-checkbox")
+// private val iUnderstandCheckBox3 = withTestId("third-term-checkbox")
+
+ // private val iUnderstandCheckBox2 = withTestId("second-term-checkbox")
+// private val iUnderstandCheckBox3 = withTestId("third-term-checkbox")
+ private val agreeAndContinueButton = withTestId("agree-and-continue-button")
+
+
+ // ---- Actions ----
+
+ fun waitForPageToLoad() {
+ WaitUtils.waitForView(
+ continueWithoutAccountButton,
+ timeoutMs = 120000
+ )
+ }
+
+ fun clickGetStarted() {
+ WaitUtils.waitForView(getStartedButton)
+ onView(getStartedButton).perform(click())
+ }
+
+ fun isContinueWithoutAccountDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(continueWithoutAccountButton, timeoutMs = 120000)
+ true
+ } catch (e: Throwable) {
+ android.util.Log.e("OnboardingPage", "isContinueWithoutAccountDisplayed failed", e)
+ false
+ }
+ }
+
+ fun clickContinueWithoutAccount() {
+ WaitUtils.waitForView(continueWithoutAccountButton)
+ onView(continueWithoutAccountButton).perform(click())
+ }
+
+ fun clickSkip() {
+ WaitUtils.waitForViewEffectivelyVisible(skipButton)
+ try {
+ onView(skipButton).perform(click())
+ } catch (e: Throwable) {
+ onView(skipButton).perform(WaitUtils.forceClick)
+ }
+ }
+
+ fun clickCreateKey() {
+ WaitUtils.waitForView(createKeyButton)
+ onView(createKeyButton).perform(click())
+ }
+
+ fun clickAlreadyHaveKey() {
+ if (isMatcherVisible(alreadyHaveKeyButton, timeoutMs = 3000)) {
+ WaitUtils.waitForView(alreadyHaveKeyButton)
+ onView(alreadyHaveKeyButton).perform(click())
+ return
+ }
+
+ // If onboarding is already completed or import screen is already open,
+ // this action is a no-op to keep reruns deterministic.
+ if (isPostOnboardingStateVisible(timeoutMs = 2500) ||
+ isMatcherVisible(importTitleText, timeoutMs = 1500)
+ ) {
+ return
+ }
+
+ WaitUtils.waitForView(alreadyHaveKeyButton)
+ onView(alreadyHaveKeyButton).perform(click())
+ }
+
+ fun verifyProtectYourWalletIsDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(protectYourWalletText)
+ onView(protectYourWalletText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifyBackupKeyPromptIsDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(backupKeyPromptText, timeoutMs = 120000)
+ onView(backupKeyPromptText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun clickBottomSheetBackUpYourKey() {
+ WaitUtils.waitForView(bottomSheetBackupYourKeyButton)
+ onView(bottomSheetBackupYourKeyButton).perform(click())
+ }
+
+ fun clickBottomSheetLater() {
+ WaitUtils.waitForViewEffectivelyVisible(bottomSheetLaterButton)
+ onView(bottomSheetLaterButton).perform(WaitUtils.forceClick)
+ }
+
+ fun verifyBackupRecoveryPhraseDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(backupRecoveryPhraseElement)
+ onView(backupRecoveryPhraseElement).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+ fun clickBackupRecoveryPhrase() {
+ WaitUtils.waitForView(backupRecoveryPhraseElement)
+ onView(backupRecoveryPhraseElement).perform(click())
+ }
+
+ fun verifyIUnderstandCheckbox1Displayed(): Boolean {
+ prepareTermsScreen()
+ if (skipTermsIfAlreadyOnboarded()) {
+ return false
+ }
+ return isAnyMatcherDisplayed(iUnderstandCheckBox1Matchers(), timeoutMs = 720000)
+ }
+
+ fun clickIUnderstandCheckbox1() {
+ if (skipTermsIfAlreadyOnboarded()) {
+ return
+ }
+ clickTermsCheckbox(iUnderstandCheckBox1Matchers(), "first-term-checkbox")
+ }
+
+ fun clickIUnderstandCheckbox2() {
+ if (skipTermsIfAlreadyOnboarded()) {
+ return
+ }
+ clickTermsCheckbox(iUnderstandCheckBox2Matchers(), "second-term-checkbox")
+ }
+
+ fun clickIUnderstandCheckbox3() {
+ if (skipTermsIfAlreadyOnboarded()) {
+ return
+ }
+ clickTermsCheckbox(iUnderstandCheckBox3Matchers(), "third-term-checkbox")
+ }
+
+ fun clickAgreeAndContinue() {
+ if (skipTermsIfAlreadyOnboarded()) {
+ return
+ }
+
+ // The third checkbox can occasionally miss taps; retry checkbox taps
+ // until CTA is enabled instead of moving on to a wrong screen state.
+ for (attempt in 1..3) {
+ if (isAgreeAndContinueEnabled()) {
+ onView(agreeAndContinueButton).perform(click())
+ return
+ }
+ clickIUnderstandCheckbox1()
+ clickIUnderstandCheckbox2()
+ clickIUnderstandCheckbox3()
+ }
+
+ assertTrue(
+ "Agree and Continue button did not enable after selecting all terms",
+ isAgreeAndContinueEnabled()
+ )
+ onView(agreeAndContinueButton).perform(click())
+ }
+
+ private fun clickTermsCheckbox(matchers: List>, name: String) {
+ prepareTermsScreen()
+ var lastError: Throwable? = null
+
+ for (matcher in matchers) {
+ try {
+ WaitUtils.waitForViewEffectivelyVisible(matcher, timeoutMs = 15000)
+ try {
+ onView(matcher).perform(click())
+ } catch (e: Throwable) {
+ onView(matcher).perform(WaitUtils.forceClick)
+ }
+ Thread.sleep(350)
+ return
+ } catch (e: Throwable) {
+ lastError = e
+ }
+ }
+
+ throw lastError ?: RuntimeException("Failed to click $name")
+ }
+
+ private fun prepareTermsScreen(timeoutMs: Long = 720000) {
+ val end = System.currentTimeMillis() + timeoutMs
+ var lastImportRetryAt = 0L
+
+ while (System.currentTimeMillis() < end) {
+ if (skipTermsIfAlreadyOnboarded()) {
+ return
+ }
+
+ if (isAnyMatcherDisplayed(iUnderstandCheckBox1Matchers(), timeoutMs = 1200)) {
+ return
+ }
+
+ var dismissedSomething = false
+
+ if (isMatcherVisible(ongoingProcessMessage, timeoutMs = 600) ||
+ isMatcherVisible(loadingTokensText, timeoutMs = 600)
+ ) {
+ Thread.sleep(1500)
+ continue
+ }
+
+ // Import flow may land on backup prompt before terms.
+ if (isMatcherVisible(backupKeyPromptText, timeoutMs = 800) ||
+ isMatcherVisible(backupRecoveryPhraseElement, timeoutMs = 800)
+ ) {
+ dismissedSomething = dismissedSomething or clickSkipIfVisible()
+ }
+
+ // After skipping backup, a bottom sheet can block terms.
+ if (isMatcherVisible(bottomSheetLaterButton, timeoutMs = 800)) {
+ dismissedSomething = dismissedSomething or clickBottomSheetLaterIfVisible()
+ }
+
+ // Imported-wallet path can remain on Import with "Loading ... tokens..."
+ // for a long time. Keep waiting and occasionally re-trigger import.
+ if (isMatcherVisible(importTitleText, timeoutMs = 600)) {
+ if (isMatcherVisible(loadingTokensText, timeoutMs = 600)) {
+ Thread.sleep(1200)
+ continue
+ }
+
+ if (System.currentTimeMillis() - lastImportRetryAt > 12000) {
+ dismissedSomething = dismissedSomething or clickImportWalletIfVisible()
+ lastImportRetryAt = System.currentTimeMillis()
+ }
+ }
+
+ if (!dismissedSomething) {
+ Thread.sleep(300)
+ }
+ }
+ }
+
+ private fun skipTermsIfAlreadyOnboarded(): Boolean =
+ isPostOnboardingStateVisible(timeoutMs = 1500)
+
+ private fun isPostOnboardingStateVisible(timeoutMs: Long): Boolean {
+ return isMatcherVisible(portfolioBalanceText, timeoutMs) ||
+ isMatcherVisible(myKeyText, timeoutMs)
+ }
+
+ private fun clickSkipIfVisible(): Boolean {
+ return try {
+ WaitUtils.waitForViewEffectivelyVisible(skipButton, timeoutMs = 2000, intervalMs = 200)
+ try {
+ onView(skipButton).perform(click())
+ } catch (e: Throwable) {
+ onView(skipButton).perform(WaitUtils.forceClick)
+ }
+ Thread.sleep(300)
+ true
+ } catch (_: Throwable) {
+ false
+ }
+ }
+
+ private fun clickBottomSheetLaterIfVisible(): Boolean {
+ return try {
+ WaitUtils.waitForViewEffectivelyVisible(bottomSheetLaterButton, timeoutMs = 2000, intervalMs = 200)
+ onView(bottomSheetLaterButton).perform(WaitUtils.forceClick)
+ Thread.sleep(300)
+ true
+ } catch (_: Throwable) {
+ false
+ }
+ }
+
+ private fun clickImportWalletIfVisible(): Boolean {
+ return try {
+ WaitUtils.waitForViewEffectivelyVisible(importWalletButton, timeoutMs = 2000, intervalMs = 200)
+ try {
+ onView(importWalletButton).perform(click())
+ } catch (e: Throwable) {
+ onView(importWalletButton).perform(WaitUtils.forceClick)
+ }
+ Thread.sleep(500)
+ true
+ } catch (_: Throwable) {
+ false
+ }
+ }
+
+ private fun isMatcherVisible(matcher: Matcher, timeoutMs: Long): Boolean {
+ return try {
+ WaitUtils.waitForViewEffectivelyVisible(matcher, timeoutMs = timeoutMs, intervalMs = 200)
+ true
+ } catch (_: Throwable) {
+ false
+ }
+ }
+
+ private fun isAnyMatcherDisplayed(matchers: List>, timeoutMs: Long): Boolean {
+ val slice = (timeoutMs / matchers.size).coerceAtLeast(3000)
+ for (matcher in matchers) {
+ try {
+ WaitUtils.waitForViewEffectivelyVisible(matcher, timeoutMs = slice)
+ return true
+ } catch (_: Throwable) {
+ // Try next matcher.
+ }
+ }
+ return false
+ }
+
+ private fun isAgreeAndContinueEnabled(): Boolean {
+ return try {
+ WaitUtils.waitForViewEnabled(agreeAndContinueButton, timeoutMs = 3000, intervalMs = 250)
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/pages/SelectCurrencyPage.kt b/android/app/src/androidTest/java/com/bitpay/wallet/pages/SelectCurrencyPage.kt
new file mode 100644
index 0000000000..e628fa62ab
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/pages/SelectCurrencyPage.kt
@@ -0,0 +1,278 @@
+package com.bitpay.wallet.pages
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.click
+import com.bitpay.wallet.utils.WaitUtils
+import com.bitpay.wallet.utils.WaitUtils.withTestId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.withSubstring
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import com.bitpay.wallet.utils.WaitUtils.withIndex
+import org.hamcrest.CoreMatchers.containsString
+import org.hamcrest.Matchers.allOf
+
+class SelectCurrencyPage {
+
+ // ---- Locators ----
+ private val selectCurrencyText = withText("Select a Currency")
+ private val bitcoinText = withSubstring("Bitcoin")
+ private val btcBitcoinText = allOf(
+ withSubstring("Bitcoin"),
+ hasSibling(withText(containsString("BTC")))
+ )
+ private val ethereumText = withSubstring("Ethereum")
+ private val sendToText = withText("Send To")
+ private val sendToAddressInput = withTestId("send-to-address-input")
+ private val swapText = withText("Swap")
+ private val swapFrom = withTestId("swap-crypto-from-wallet-selector")
+ private val swapTo = withTestId("swap-crypto-to-wallet-selector")
+ private val swapToText = withText("Swap To")
+ private val selectCryptoText = withText("Select Crypto")
+ private val selectKeyToDepositToText = withText("Select Key to Deposit to")
+ private val cryptoToSwapText = withText("Crypto to Swap")
+ private val selectAccountToDepositToText = withText("Select Account to Deposit to")
+ private val myKeyWallet = withIndex(withTestId("wallet-card-My Key"), 1)
+ private val evmAccount = withText("EVM Account")
+ private val enterSwapAmountButton =withTestId("swap-crypto-enter-amount-button")
+ private val minButton = withText("MIN")
+ private val swapChangellyTermsCheckbox =withTestId("swap-crypto-changelly-terms-checkbox")
+ private val slideToSwipeButton = withContentDescription("Slide to swap")
+ private val buyText = withText("Buy")
+ private val sellText = withText("Sell")
+ private val buyContinueButton = withTestId("button")
+ private val chooseCrypto = withText("Choose Crypto")
+
+
+ // ---- Actions ----
+ fun verifySelectCurrencyDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(selectCurrencyText)
+ onView(selectCurrencyText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifyBitcoinTextDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(bitcoinText, timeoutMs = 300000)
+ onView(bitcoinText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun clickBitcoin() {
+ WaitUtils.waitForView(bitcoinText, timeoutMs = 30000)
+ onView(bitcoinText).perform(click())
+ }
+
+ fun clickBTCBitcoin() {
+ WaitUtils.waitForView(btcBitcoinText, timeoutMs = 30000)
+ onView(btcBitcoinText).perform(click())
+ }
+
+ fun clickEthereum() {
+ WaitUtils.waitForView(ethereumText)
+ onView(ethereumText).perform(click())
+ }
+
+ fun verifySendToDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(sendToText)
+ onView(sendToText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun enterSendToAddress(sendToAddress: String) {
+ WaitUtils.waitForView(sendToAddressInput)
+ onView(sendToAddressInput).perform(
+ ViewActions.click(),
+ ViewActions.typeText(sendToAddress),
+ ViewActions.closeSoftKeyboard()
+ )
+ }
+
+ fun verifySwapTitleDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForViewEffectivelyVisible(swapFrom, timeoutMs = 60 * 1000)
+ onView(swapFrom).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifyBuyTitleDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(buyText)
+ onView(buyText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifySellTitleDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(sellText)
+ onView(sellText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifySelectCryptoTitleDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(selectCryptoText, timeoutMs = 30 * 1000)
+ onView(selectCryptoText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifyCryptoToSwapTitleDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(cryptoToSwapText, timeoutMs = 30 * 1000)
+ onView(cryptoToSwapText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifyCryptoTopSwapDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(cryptoToSwapText, timeoutMs = 30 * 1000)
+ onView(cryptoToSwapText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifySwapFromOptionDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForViewEffectivelyVisible(swapFrom, timeoutMs = 60 * 1000)
+ onView(swapFrom).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun clickSwapFromOption() {
+ WaitUtils.waitForViewEffectivelyVisible(swapFrom, timeoutMs = 60 * 1000)
+ try {
+ onView(swapFrom).perform(click())
+ } catch (e: Throwable) {
+ onView(swapFrom).perform(WaitUtils.forceClick)
+ }
+ }
+
+ fun verifySwapToTitleDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(swapToText, timeoutMs = 60 * 1000)
+ onView(swapToText).check(matches(isDisplayed()))
+ true
+ } catch (swapToMissing: Throwable) {
+ // Some builds render the same picker with a generic title.
+ try {
+ WaitUtils.waitForView(selectCryptoText, timeoutMs = 20 * 1000)
+ onView(selectCryptoText).check(matches(isDisplayed()))
+ true
+ } catch (selectCryptoMissing: Throwable) {
+ false
+ }
+ }
+ }
+
+ fun clickSwapToOption() {
+ WaitUtils.waitForViewEffectivelyVisible(swapTo, timeoutMs = 30 * 1000)
+ try {
+ onView(swapTo).perform(click())
+ } catch (e: Throwable) {
+ onView(swapTo).perform(WaitUtils.forceClick)
+ }
+ }
+
+ fun verifySelectKeyToDepositToDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(selectKeyToDepositToText)
+ onView(selectKeyToDepositToText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifySelectAccountToDepositToDisplayed(): Boolean {
+ return try {
+ WaitUtils.waitForView(selectAccountToDepositToText)
+ onView(selectAccountToDepositToText).check(matches(isDisplayed()))
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun clickEnterSwapAmount() {
+ WaitUtils.waitForView(enterSwapAmountButton)
+ onView(enterSwapAmountButton).perform(click())
+ }
+
+ fun clickMinSwapAmount() {
+ WaitUtils.waitForView(minButton)
+ onView(minButton).perform(click())
+ }
+
+ fun clickSecondMyKeyWallet() {
+ WaitUtils.waitForView(myKeyWallet)
+ onView(myKeyWallet).perform(click())
+ }
+
+ fun clickEVMAccount() {
+ WaitUtils.waitForView(evmAccount)
+ onView(evmAccount).perform(click())
+ }
+
+ fun clickChangellyTermsCheckbox() {
+ WaitUtils.waitForView(swapChangellyTermsCheckbox, timeoutMs = 30*1000)
+ onView(swapChangellyTermsCheckbox).perform(click())
+ }
+
+ fun verifySlideToSwapButtonEnabled(): Boolean {
+ return try {
+ WaitUtils.waitForViewEnabled(slideToSwipeButton)
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun verifyBuyContinueButtonEnabled(): Boolean {
+ return try {
+ WaitUtils.waitForViewEnabled(buyContinueButton, timeoutMs = 30 * 1000)
+ true
+ } catch (e: Throwable) {
+ false
+ }
+ }
+
+ fun clickChooseCrypto() {
+ WaitUtils.waitForView(chooseCrypto)
+ onView(chooseCrypto).perform(click())
+ }
+
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test591OnboardingCreateWallet.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test591OnboardingCreateWallet.kt
new file mode 100644
index 0000000000..bb5cf4a131
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test591OnboardingCreateWallet.kt
@@ -0,0 +1,70 @@
+package com.bitpay.wallet.tests
+
+import com.bitpay.wallet.base.BaseTest
+import com.bitpay.wallet.pages.HomePage
+import com.bitpay.wallet.pages.OnboardingPage
+import com.bitpay.wallet.utils.allureStep
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.bitpay.wallet.utils.allureScreenshot
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+@RunWith(AndroidJUnit4::class)
+class Test591OnboardingCreateWallet : BaseTest() {
+ private val onboardingPage = OnboardingPage()
+ private val homePage = HomePage()
+
+ @Test
+ fun testOnboardingCreateWallet() {
+ allureStep("Click Create Key on onboarding screen") {
+ onboardingPage.clickCreateKey()
+ allureScreenshot("After Create Key clicked")
+ }
+
+ allureStep("Verify backup key prompt is displayed") {
+ assertTrue(
+ "Backup key prompt was not displayed",
+ onboardingPage.verifyBackupKeyPromptIsDisplayed()
+ )
+ assertTrue(
+ "Backup your recovery phrase not displayed",
+ onboardingPage.verifyBackupRecoveryPhraseDisplayed()
+ )
+
+ allureScreenshot("Backup key page displayed")
+ }
+
+ allureStep("Skip backup prompt") {
+ onboardingPage.clickSkip() // Skip "Would you like to backup"
+ }
+
+ allureStep("Dismiss bottom sheet (Later)") {
+ onboardingPage.clickBottomSheetLater()
+ }
+
+ allureStep("Check all three 'I understand' checkboxes") {
+ onboardingPage.clickIUnderstandCheckbox1()
+ onboardingPage.clickIUnderstandCheckbox2()
+ onboardingPage.clickIUnderstandCheckbox3()
+ }
+
+ allureStep("Click Agree and Continue") {
+ onboardingPage.clickAgreeAndContinue()
+ }
+
+ allureStep("Wait for Home page to load") {
+ homePage.waitForPageToLoad()
+ }
+
+ allureStep("Verify portfolio balance is displayed on Home page") {
+ assertTrue(
+ "Home Page - Portfolio balance text not displayed",
+ homePage.verifyPortfolioBalanceTextDisplayed()
+ )
+ allureScreenshot("Final - Portfolio balance verified")
+ }
+
+ }
+}
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test592ImportWalletRecoveryPhrase.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test592ImportWalletRecoveryPhrase.kt
new file mode 100644
index 0000000000..c992d583b5
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test592ImportWalletRecoveryPhrase.kt
@@ -0,0 +1,104 @@
+package com.bitpay.wallet.tests
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.bitpay.wallet.base.BaseTest
+import com.bitpay.wallet.pages.AddYourCryptoOptionPage
+import com.bitpay.wallet.pages.HomePage
+import com.bitpay.wallet.pages.ImportWalletPage
+import com.bitpay.wallet.pages.MyKeyPage
+import com.bitpay.wallet.pages.OnboardingPage
+import com.bitpay.wallet.utils.allureScreenshot
+import com.bitpay.wallet.utils.allureStep
+import org.junit.Assert.assertTrue
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import com.bitpay.wallet.TestConfig
+
+@RunWith(AndroidJUnit4::class)
+class Test592ImportWalletRecoveryPhrase : BaseTest() {
+
+ private val onboardingPage = OnboardingPage()
+ private val homePage = HomePage()
+ private val addYourCryptoOptionPage = AddYourCryptoOptionPage()
+ private val importWalletPage = ImportWalletPage()
+ private val myKeyPage = MyKeyPage()
+ private val walletPhrase = TestConfig.WALLET_PHRASE
+
+
+ @Test
+ fun testImportWalletRecoveryPhrase() {
+
+ allureStep("Click Create Key on onboarding screen") {
+ onboardingPage.clickCreateKey()
+ }
+
+ allureStep("Verify backup key prompt is displayed") {
+ assertTrue(
+ "Backup key prompt was not displayed",
+ onboardingPage.verifyBackupKeyPromptIsDisplayed()
+ )
+ allureScreenshot("Backup key prompt displayed")
+ }
+
+ allureStep("Skip backup prompt") {
+ onboardingPage.clickSkip() // Skip "Would you like to backup"
+ }
+
+ allureStep("Dismiss bottom sheet (Later)") {
+ onboardingPage.clickBottomSheetLater()
+ }
+
+ allureStep("Check all three 'I understand' checkboxes") {
+ onboardingPage.clickIUnderstandCheckbox1()
+ onboardingPage.clickIUnderstandCheckbox2()
+ onboardingPage.clickIUnderstandCheckbox3()
+ }
+
+ allureStep("Click Agree and Continue") {
+ onboardingPage.clickAgreeAndContinue()
+ }
+
+ allureStep("Wait for Home page to load") {
+ homePage.waitForPageToLoad()
+ allureScreenshot("Home page loaded")
+ }
+
+ allureStep("Verify portfolio balance is displayed on Home page") {
+ assertTrue(
+ "Home Page - Portfolio balance text not displayed",
+ homePage.verifyPortfolioBalanceTextDisplayed()
+ )
+ }
+
+ allureStep("Click Add Your Crypto on Home page") {
+ homePage.clickAddYourCrypto()
+ }
+
+ allureStep("Click Import Key option") {
+ addYourCryptoOptionPage.clickImportKey()
+ }
+
+ allureStep("Enter recovery phrase and import wallet") {
+ importWalletPage.enterRecoveryPhrase(walletPhrase)
+ allureScreenshot("Recovery phrase entered")
+ importWalletPage.clickImportWallet()
+ }
+
+ allureStep("Verify wallet import succeeded - My Key page displayed") {
+ assertTrue(
+ "Wallet Import Failed - My Key not displayed",
+ myKeyPage.verifyMyKeyDisplayed()
+ )
+ allureScreenshot("My Key page displayed after import")
+ }
+
+ allureStep("Verify Bitcoin is displayed in My Key page") {
+ assertTrue(
+ "Bitcoin not displayed in My Key page",
+ myKeyPage.verifyBitcoinTextDisplayed()
+ )
+ allureScreenshot("Final - Bitcoin shown in My Key page")
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test593SendBTC.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test593SendBTC.kt
new file mode 100644
index 0000000000..14e6ac7df0
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test593SendBTC.kt
@@ -0,0 +1,128 @@
+package com.bitpay.wallet.tests
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.bitpay.wallet.base.BaseTest
+import com.bitpay.wallet.pages.AddYourCryptoOptionPage
+import com.bitpay.wallet.pages.ConfirmPaymentPage
+import com.bitpay.wallet.pages.HomePage
+import com.bitpay.wallet.pages.ImportWalletPage
+import com.bitpay.wallet.pages.KeyboardPage
+import com.bitpay.wallet.pages.MyKeyPage
+import com.bitpay.wallet.pages.OnboardingPage
+import com.bitpay.wallet.pages.SelectCurrencyPage
+import com.bitpay.wallet.utils.allureScreenshot
+import com.bitpay.wallet.utils.allureStep
+import org.junit.Assert.assertTrue
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import com.bitpay.wallet.TestConfig
+
+@RunWith(AndroidJUnit4::class)
+class Test593SendBTC : BaseTest() {
+
+ private val onboardingPage = OnboardingPage()
+ private val homePage = HomePage()
+ private val addYourCryptoOptionPage = AddYourCryptoOptionPage()
+ private val importWalletPage = ImportWalletPage()
+ private val myKeyPage = MyKeyPage()
+ private val selectCurrencyPage = SelectCurrencyPage()
+ private val keyboardPage = KeyboardPage()
+ private val confirmPaymentPage = ConfirmPaymentPage()
+ private val walletPhrase = TestConfig.WALLET_PHRASE
+
+ @Test
+ fun testSendBTC() {
+ allureStep("Click 'Already have a key' on onboarding screen") {
+ onboardingPage.clickAlreadyHaveKey()
+ }
+
+ allureStep("Enter recovery phrase and import wallet") {
+ importWalletPage.enterRecoveryPhrase(walletPhrase)
+ allureScreenshot("Recovery phrase entered")
+ importWalletPage.clickImportWallet()
+ }
+
+ allureStep("Verify 'I understand' checkbox 1 is displayed") {
+ assertTrue(
+ "Important screen - checkbox 1 not displayed",
+ onboardingPage.verifyIUnderstandCheckbox1Displayed()
+ )
+ }
+
+ allureStep("Check all three 'I understand' checkboxes") {
+ onboardingPage.clickIUnderstandCheckbox1()
+ onboardingPage.clickIUnderstandCheckbox2()
+ onboardingPage.clickIUnderstandCheckbox3()
+ }
+
+ allureStep("Click Agree and Continue") {
+ onboardingPage.clickAgreeAndContinue()
+ }
+
+ allureStep("Wait for Home page to load") {
+ homePage.waitForPageToLoad()
+ homePage.waitForFundedBalance()
+ allureScreenshot("Home page loaded")
+ }
+
+ allureStep("Verify portfolio balance is displayed on Home page") {
+ assertTrue(
+ "Home Page - Portfolio balance text not displayed",
+ homePage.verifyPortfolioBalanceTextDisplayed()
+ )
+ }
+
+ allureStep("Click Send on Home page") {
+ homePage.clickSend()
+ }
+
+ allureStep("Verify Select Currency page is displayed") {
+ assertTrue(
+ "Select Currency page not displayed",
+ selectCurrencyPage.verifySelectCurrencyDisplayed()
+ )
+ assertTrue(
+ "Select Currency page not displayed",
+ selectCurrencyPage.verifySelectCurrencyDisplayed()
+ )
+ allureScreenshot("Select Currency page displayed")
+ }
+
+ allureStep("Click Bitcoin currency option") {
+ selectCurrencyPage.clickBitcoin()
+ }
+
+ allureStep("Verify Send To page is displayed") {
+ assertTrue(
+ "Send To page not displayed",
+ selectCurrencyPage.verifySendToDisplayed()
+ )
+ }
+
+ allureStep("Enter Send To address") {
+ selectCurrencyPage.enterSendToAddress("bc1q0effzahtsn685tyjppgukpvfhv37hrlm4g67ws")
+ }
+
+ allureStep("Enter amount and click Continue") {
+ keyboardPage.enterAmount("0.00005")
+ keyboardPage.clickContinue()
+ }
+
+ allureStep("Verify Confirm Payment page is displayed") {
+ assertTrue(
+ "Confirm Payment page not displayed",
+ confirmPaymentPage.verifyConfirmPaymentTitleDisplayed(),
+ )
+ allureScreenshot("Confirm Payment page displayed")
+ }
+
+ allureStep("Verify Summary text is displayed on Confirm Payment page") {
+ assertTrue(
+ "Confirm Payment - Summary page not displayed",
+ confirmPaymentPage.verifySummaryTextDisplayed()
+ )
+ allureScreenshot("Final - Summary displayed before sending")
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test594SwapBTC.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test594SwapBTC.kt
new file mode 100644
index 0000000000..35a896c693
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test594SwapBTC.kt
@@ -0,0 +1,152 @@
+package com.bitpay.wallet.tests
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.bitpay.wallet.base.BaseTest
+import com.bitpay.wallet.pages.AddYourCryptoOptionPage
+import com.bitpay.wallet.pages.HomePage
+import com.bitpay.wallet.pages.ImportWalletPage
+import com.bitpay.wallet.pages.MyKeyPage
+import com.bitpay.wallet.pages.OnboardingPage
+import com.bitpay.wallet.pages.SelectCurrencyPage
+import com.bitpay.wallet.utils.allureScreenshot
+import com.bitpay.wallet.utils.allureStep
+import org.junit.Assert.assertTrue
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import com.bitpay.wallet.TestConfig
+
+@RunWith(AndroidJUnit4::class)
+class Test594SwapBTC : BaseTest() {
+
+ private val onboardingPage = OnboardingPage()
+ private val homePage = HomePage()
+ private val addYourCryptoOptionPage = AddYourCryptoOptionPage()
+ private val importWalletPage = ImportWalletPage()
+ private val myKeyPage = MyKeyPage()
+ private val selectCurrencyPage = SelectCurrencyPage()
+ private val walletPhrase = TestConfig.WALLET_PHRASE
+
+ @Test
+ fun testBTCSwap() {
+ allureStep("Click 'Already have a key' on onboarding screen") {
+ onboardingPage.clickAlreadyHaveKey()
+ }
+
+ allureStep("Enter recovery phrase and import wallet") {
+ importWalletPage.enterRecoveryPhrase(walletPhrase)
+ allureScreenshot("Recovery phrase entered")
+ importWalletPage.clickImportWallet()
+ }
+
+ allureStep("Verify 'I understand' checkbox 1 is displayed") {
+ assertTrue(
+ "Important screen - checkbox 1 not displayed",
+ onboardingPage.verifyIUnderstandCheckbox1Displayed()
+ )
+ }
+
+ allureStep("Check all three 'I understand' checkboxes") {
+ onboardingPage.clickIUnderstandCheckbox1()
+ onboardingPage.clickIUnderstandCheckbox2()
+ onboardingPage.clickIUnderstandCheckbox3()
+ }
+
+ allureStep("Click Agree and Continue") {
+ onboardingPage.clickAgreeAndContinue()
+ }
+
+ allureStep("Wait for Home page to load") {
+ homePage.waitForPageToLoad()
+ homePage.waitForFundedBalance()
+ allureScreenshot("Home page loaded")
+ }
+
+ allureStep("Verify portfolio balance is displayed on Home page") {
+ assertTrue(
+ "Home Page - Portfolio balance text not displayed",
+ homePage.verifyPortfolioBalanceTextDisplayed()
+ )
+ }
+
+ allureStep("Click Swap on Home page") {
+ homePage.clickSwap()
+ }
+
+ allureStep("Verify Swap page is displayed") {
+ assertTrue(
+ "Swap page not displayed",
+ selectCurrencyPage.verifySwapTitleDisplayed()
+ )
+ assertTrue(
+ "Swap page - Swap From option not displayed",
+ selectCurrencyPage.verifySwapFromOptionDisplayed()
+ )
+ assertTrue(
+ "Swap page - Bitcoin text not displayed",
+ selectCurrencyPage.verifyBitcoinTextDisplayed()
+ )
+ allureScreenshot("Swap page displayed")
+ }
+
+ allureStep("Select Bitcoin as Swap From currency") {
+ selectCurrencyPage.clickSwapFromOption()
+ allureScreenshot("Swap from Option")
+
+ assertTrue(
+ "Swap page - Crypto To Swap text not displayed",
+ selectCurrencyPage.verifyCryptoToSwapTitleDisplayed()
+ )
+ assertTrue(
+ "Swap page - Bitcoin text not displayed",
+ selectCurrencyPage.verifyBitcoinTextDisplayed()
+ )
+
+ allureScreenshot("Swap from Option - Before Bitcoin click")
+ selectCurrencyPage.clickBTCBitcoin()
+ assertTrue(
+ "Swap page - Bitcoin text not displayed",
+ selectCurrencyPage.verifyBitcoinTextDisplayed()
+ )
+ allureScreenshot("Swap from Option - After Bitcoin click")
+ }
+ allureStep("Select Ethereum as Swap To currency") {
+ selectCurrencyPage.clickSwapToOption()
+ assertTrue(
+ "Swap To - Page not displayed",
+ selectCurrencyPage.verifySwapToTitleDisplayed()
+ )
+ allureScreenshot("Swap To click")
+ selectCurrencyPage.clickEthereum()
+ }
+
+ allureStep("Verify Select Account To Deposit To option is displayed") {
+ assertTrue(
+ "Select Account To Deposit To option not displayed",
+ selectCurrencyPage.verifySelectAccountToDepositToDisplayed()
+ )
+ }
+
+ allureStep("Select second My Key wallet and EVM account") {
+ selectCurrencyPage.clickEVMAccount()
+ }
+
+ allureStep("Enter minimum swap amount") {
+ selectCurrencyPage.clickEnterSwapAmount()
+ selectCurrencyPage.clickMinSwapAmount()
+ allureScreenshot("Minimum swap amount entered")
+ }
+
+ allureStep("Accept Changelly terms checkbox") {
+ selectCurrencyPage.clickChangellyTermsCheckbox()
+ }
+
+ allureStep("Verify Slide to Swap button is enabled") {
+ assertTrue(
+ "Slide to Swap button not enabled",
+ selectCurrencyPage.verifySlideToSwapButtonEnabled()
+ )
+ allureScreenshot("Final - Slide to Swap button enabled")
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test595SellBTC.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test595SellBTC.kt
new file mode 100644
index 0000000000..91f754aca0
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test595SellBTC.kt
@@ -0,0 +1,104 @@
+package com.bitpay.wallet.tests
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.bitpay.wallet.base.BaseTest
+import com.bitpay.wallet.pages.AddYourCryptoOptionPage
+import com.bitpay.wallet.pages.HomePage
+import com.bitpay.wallet.pages.ImportWalletPage
+import com.bitpay.wallet.pages.KeyboardPage
+import com.bitpay.wallet.pages.MyKeyPage
+import com.bitpay.wallet.pages.OnboardingPage
+import com.bitpay.wallet.pages.SelectCurrencyPage
+import com.bitpay.wallet.utils.allureScreenshot
+import com.bitpay.wallet.utils.allureStep
+import org.junit.Assert.assertTrue
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import com.bitpay.wallet.TestConfig
+
+@RunWith(AndroidJUnit4::class)
+class Test595SellBTC : BaseTest() {
+
+ private val onboardingPage = OnboardingPage()
+ private val homePage = HomePage()
+ private val addYourCryptoOptionPage = AddYourCryptoOptionPage()
+ private val importWalletPage = ImportWalletPage()
+ private val myKeyPage = MyKeyPage()
+ private val selectCurrencyPage = SelectCurrencyPage()
+ private val keyboardPage = KeyboardPage()
+ private val walletPhrase = TestConfig.WALLET_PHRASE
+
+ @Test
+ fun testBTCSell() {
+ allureStep("Click 'Already have a key' on onboarding screen") {
+ onboardingPage.clickAlreadyHaveKey()
+ }
+
+ allureStep("Enter recovery phrase and import wallet") {
+ importWalletPage.enterRecoveryPhrase(walletPhrase)
+ allureScreenshot("Recovery phrase entered")
+ importWalletPage.clickImportWallet()
+ }
+
+ allureStep("Verify 'I understand' checkbox 1 is displayed") {
+ assertTrue(
+ "Important screen - checkbox 1 not displayed",
+ onboardingPage.verifyIUnderstandCheckbox1Displayed()
+ )
+ }
+
+ allureStep("Check all three 'I understand' checkboxes") {
+ onboardingPage.clickIUnderstandCheckbox1()
+ onboardingPage.clickIUnderstandCheckbox2()
+ onboardingPage.clickIUnderstandCheckbox3()
+ }
+
+ allureStep("Click Agree and Continue") {
+ onboardingPage.clickAgreeAndContinue()
+ }
+
+ allureStep("Wait for Home page to load") {
+ homePage.waitForPageToLoad()
+ homePage.waitForFundedBalance()
+ allureScreenshot("Home page loaded")
+ }
+
+ allureStep("Verify portfolio balance is displayed on Home page") {
+ assertTrue(
+ "Home Page - Portfolio balance text not displayed",
+ homePage.verifyPortfolioBalanceTextDisplayed()
+ )
+ }
+
+ allureStep("Click Sell on Home page") {
+ homePage.clickSell()
+ }
+
+ allureStep("Verify Sell page is displayed") {
+ assertTrue(
+ "Sell page not displayed",
+ selectCurrencyPage.verifySellTitleDisplayed()
+ )
+ allureScreenshot("Sell page displayed")
+ }
+
+ allureStep("Enter sell amount") {
+ keyboardPage.enterAmount("0.0007")
+ }
+
+ allureStep("Choose Bitcoin as the crypto to sell") {
+ selectCurrencyPage.clickChooseCrypto()
+ selectCurrencyPage.clickBitcoin()
+ allureScreenshot("Bitcoin selected for sell")
+ }
+
+ allureStep("Verify Continue button is enabled") {
+ assertTrue(
+ "Buy - Continue button not enabled",
+ selectCurrencyPage.verifyBuyContinueButtonEnabled()
+ )
+ allureScreenshot("Final - Continue button enabled")
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test596BuyBTC.kt b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test596BuyBTC.kt
new file mode 100644
index 0000000000..1ce7dec4c5
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/tests/Test596BuyBTC.kt
@@ -0,0 +1,119 @@
+package com.bitpay.wallet.tests
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.bitpay.wallet.base.BaseTest
+import com.bitpay.wallet.pages.AddYourCryptoOptionPage
+import com.bitpay.wallet.pages.HomePage
+import com.bitpay.wallet.pages.ImportWalletPage
+import com.bitpay.wallet.pages.KeyboardPage
+import com.bitpay.wallet.pages.MyKeyPage
+import com.bitpay.wallet.pages.OnboardingPage
+import com.bitpay.wallet.pages.SelectCurrencyPage
+import com.bitpay.wallet.utils.allureScreenshot
+import com.bitpay.wallet.utils.allureStep
+import org.junit.Assert.assertTrue
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import com.bitpay.wallet.TestConfig
+
+@RunWith(AndroidJUnit4::class)
+class Test596BuyBTC : BaseTest() {
+
+ private val onboardingPage = OnboardingPage()
+ private val homePage = HomePage()
+ private val addYourCryptoOptionPage = AddYourCryptoOptionPage()
+ private val importWalletPage = ImportWalletPage()
+ private val myKeyPage = MyKeyPage()
+ private val selectCurrencyPage = SelectCurrencyPage()
+ private val keyboardPage = KeyboardPage()
+ private val walletPhrase = TestConfig.WALLET_PHRASE
+
+ @Test
+ fun testBTCBuy() {
+ allureStep("Click 'Already have a key' on onboarding screen") {
+ onboardingPage.clickAlreadyHaveKey()
+ }
+
+ allureStep("Enter recovery phrase and import wallet") {
+ importWalletPage.enterRecoveryPhrase(walletPhrase)
+ allureScreenshot("Recovery phrase entered")
+ importWalletPage.clickImportWallet()
+ }
+
+ allureStep("Verify 'I understand' checkbox 1 is displayed") {
+ assertTrue(
+ "Important screen - checkbox 1 not displayed",
+ onboardingPage.verifyIUnderstandCheckbox1Displayed()
+ )
+ }
+
+ allureStep("Check all three 'I understand' checkboxes") {
+ onboardingPage.clickIUnderstandCheckbox1()
+ onboardingPage.clickIUnderstandCheckbox2()
+ onboardingPage.clickIUnderstandCheckbox3()
+ }
+
+ allureStep("Click Agree and Continue") {
+ onboardingPage.clickAgreeAndContinue()
+ }
+
+ allureStep("Wait for Home page to load") {
+ homePage.waitForPageToLoad()
+ allureScreenshot("Home page loaded")
+ }
+
+ allureStep("Verify portfolio balance is displayed on Home page") {
+ assertTrue(
+ "Home Page - Portfolio balance text not displayed",
+ homePage.verifyPortfolioBalanceTextDisplayed()
+ )
+ }
+
+ allureStep("Click Buy on Home page") {
+ homePage.clickBuy()
+ }
+
+ allureStep("Verify Buy page is displayed") {
+ assertTrue(
+ "Buy page not displayed",
+ selectCurrencyPage.verifyBuyTitleDisplayed()
+ )
+ allureScreenshot("Buy page displayed")
+ }
+
+ allureStep("Clear default amount and enter buy amount") {
+ keyboardPage.clickBackspace(3)
+ keyboardPage.enterAmount("30")
+ }
+
+ allureStep("Select Bitcoin as the crypto to buy") {
+ selectCurrencyPage.clickBitcoin()
+ assertTrue(
+ "Select Crypto page not displayed",
+ selectCurrencyPage.verifySelectCryptoTitleDisplayed()
+ )
+ selectCurrencyPage.clickBitcoin() // Select Crypto page
+ allureScreenshot("Bitcoin selected for buy")
+ }
+
+ allureStep("Verify Select Key To Deposit To option is displayed") {
+ assertTrue(
+ "Select Key To Deposit To option not displayed",
+ selectCurrencyPage.verifySelectKeyToDepositToDisplayed()
+ )
+ }
+
+ allureStep("Select My Key wallet") {
+ selectCurrencyPage.clickSecondMyKeyWallet()
+ }
+
+ allureStep("Verify Continue button is enabled") {
+ assertTrue(
+ "Buy - Continue button not enabled",
+ selectCurrencyPage.verifyBuyContinueButtonEnabled()
+ )
+ allureScreenshot("Final - Continue button enabled")
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/utils/AllureUtils.kt b/android/app/src/androidTest/java/com/bitpay/wallet/utils/AllureUtils.kt
new file mode 100644
index 0000000000..b2589d6321
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/utils/AllureUtils.kt
@@ -0,0 +1,30 @@
+package com.bitpay.wallet.utils
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import io.qameta.allure.kotlin.Allure
+import java.io.File
+
+fun allureScreenshot(name: String = "Screenshot") {
+ if (System.getenv("GITHUB_ACTIONS") == "true") {
+ return
+ }
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val file = File(context.cacheDir, "screenshot_${System.currentTimeMillis()}.png")
+
+ val saved = device.takeScreenshot(file)
+ if (saved && file.exists()) {
+ Allure.attachment(name, file.inputStream())
+ }
+}
+
+inline fun allureStep(name: String, crossinline block: () -> Unit) {
+ Allure.step(name) { block() }
+}
+
+fun allureVideo(name: String, file: File?) {
+ if (file == null || !file.exists()) return
+ Allure.attachment(name, file.inputStream(), "video/mp4")
+ file.delete()
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/utils/RetryRule.kt b/android/app/src/androidTest/java/com/bitpay/wallet/utils/RetryRule.kt
new file mode 100644
index 0000000000..28595af4fc
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/utils/RetryRule.kt
@@ -0,0 +1,29 @@
+package com.bitpay.wallet.utils
+
+import android.util.Log
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+class RetryRule(private val retryCount: Int = 3) : TestRule {
+ override fun apply(base: Statement, description: Description): Statement =
+ object : Statement() {
+ override fun evaluate() {
+ var lastError: Throwable? = null
+ repeat(retryCount) { attempt ->
+ try {
+ base.evaluate()
+ return
+ } catch (t: Throwable) {
+ lastError = t
+ Log.e(
+ "RetryRule",
+ "${description.displayName}: attempt ${attempt + 1} failed",
+ t
+ )
+ }
+ }
+ throw lastError!!
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/androidTest/java/com/bitpay/wallet/utils/WaitUtils.kt b/android/app/src/androidTest/java/com/bitpay/wallet/utils/WaitUtils.kt
new file mode 100644
index 0000000000..f1882c0d44
--- /dev/null
+++ b/android/app/src/androidTest/java/com/bitpay/wallet/utils/WaitUtils.kt
@@ -0,0 +1,138 @@
+package com.bitpay.wallet.utils
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isEnabled
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import org.hamcrest.Matcher
+import android.view.View
+import androidx.test.espresso.matcher.BoundedMatcher
+import androidx.test.espresso.matcher.ViewMatchers
+import org.hamcrest.Description
+import org.hamcrest.TypeSafeMatcher
+
+
+object WaitUtils {
+
+ fun waitForView(
+ matcher: Matcher,
+ timeoutMs: Long = 30000,
+ intervalMs: Long = 500
+ ) {
+ val endTime = System.currentTimeMillis() + timeoutMs
+ var lastError: Throwable? = null
+
+ while (System.currentTimeMillis() < endTime) {
+ try {
+ onView(matcher).check(matches(isDisplayed()))
+ return // found it, exit
+ } catch (e: Throwable) {
+ // catch EVERYTHING - NoMatchingViewException, AssertionError,
+ // RootViewWithoutFocusException, anything else Espresso throws
+ lastError = e
+ Thread.sleep(intervalMs)
+ }
+ }
+
+ // timed out - throw the last real error so the failure message is useful
+ throw lastError ?: RuntimeException("View not found within ${timeoutMs}ms: $matcher")
+ }
+
+ fun waitForViewEnabled(
+ matcher: Matcher,
+ timeoutMs: Long = 120000,
+ intervalMs: Long = 500
+ ) {
+ val endTime = System.currentTimeMillis() + timeoutMs
+ var lastError: Throwable? = null
+
+ while (System.currentTimeMillis() < endTime) {
+ try {
+ onView(matcher).check(matches(isDisplayed())).check(matches(isEnabled()))
+ return
+ } catch (e: Throwable) {
+ lastError = e
+ Thread.sleep(intervalMs)
+ }
+ }
+
+ throw lastError ?: RuntimeException("View not enabled within ${timeoutMs}ms: $matcher")
+ }
+
+ /**
+ * React Native testIDs are surfaced to native Espresso as View.getTag(),
+ * NOT as an Android resource id. withResourceName()/withId() will never
+ * match them - this matcher checks the tag directly instead.
+ */
+ fun withTestId(testId: String): BoundedMatcher {
+ return object : BoundedMatcher(View::class.java) {
+ override fun describeTo(description: Description) {
+ description.appendText("with RN testID: $testId")
+ }
+
+ override fun matchesSafely(view: View): Boolean {
+ return testId == view.tag
+ }
+ }
+ }
+
+ /**
+ * Waits for a view to have effective visibility VISIBLE without requiring
+ * getGlobalVisibleRect() to be non-empty. Use this when a view is in the
+ * hierarchy and marked VISIBLE but is clipped (e.g. animating bottom sheets).
+ */
+ fun waitForViewEffectivelyVisible(
+ matcher: Matcher,
+ timeoutMs: Long = 30000,
+ intervalMs: Long = 500
+ ) {
+ val endTime = System.currentTimeMillis() + timeoutMs
+ var lastError: Throwable? = null
+ while (System.currentTimeMillis() < endTime) {
+ try {
+ onView(matcher).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ return
+ } catch (e: Throwable) {
+ lastError = e
+ Thread.sleep(intervalMs)
+ }
+ }
+ throw lastError ?: RuntimeException("View not visible within ${timeoutMs}ms: $matcher")
+ }
+
+ /**
+ * Clicks a view without the standard isDisplayed() precondition check.
+ * Use for elements that are VISIBLE but whose getGlobalVisibleRect() is empty
+ * (e.g. a bottom sheet button that is present but not yet in the visible rect).
+ */
+ val forceClick: ViewAction = object : ViewAction {
+ override fun getConstraints(): Matcher = isEnabled()
+ override fun getDescription() = "force click ignoring visibility"
+ override fun perform(uiController: UiController, view: View) {
+ view.performClick()
+ uiController.loopMainThreadUntilIdle()
+ }
+ }
+
+ fun withIndex(matcher: Matcher, index: Int): Matcher {
+ return object : TypeSafeMatcher() {
+ var currentIndex = 0
+
+ override fun describeTo(description: Description) {
+ description.appendText("with index: $index ")
+ matcher.describeTo(description)
+ }
+
+ override fun matchesSafely(item: T): Boolean {
+ val matched = matcher.matches(item)
+ if (matched && currentIndex++ == index) {
+ return true
+ }
+ return false
+ }
+ }
+ }
+}
diff --git a/android/app/src/androidTest/resources/allure.properties b/android/app/src/androidTest/resources/allure.properties
new file mode 100644
index 0000000000..890384fac0
--- /dev/null
+++ b/android/app/src/androidTest/resources/allure.properties
@@ -0,0 +1 @@
+allure.results.useTestStorage=true
\ No newline at end of file
diff --git a/android/app/src/main/java/com/bitpay/wallet/MainActivity.kt b/android/app/src/main/java/com/bitpay/wallet/MainActivity.kt
index 8ad8bd6c2f..6506a3782e 100644
--- a/android/app/src/main/java/com/bitpay/wallet/MainActivity.kt
+++ b/android/app/src/main/java/com/bitpay/wallet/MainActivity.kt
@@ -1,7 +1,10 @@
package com.bitpay.wallet
import android.content.Intent
+import android.graphics.Color
+import android.os.Build
import android.os.Bundle
+import android.view.View
import android.view.WindowManager
import com.braze.reactbridge.BrazeReactUtils
import com.braze.ui.inappmessage.BrazeInAppMessageManager
@@ -44,11 +47,41 @@ class MainActivity : ReactActivity() {
(application as MainApplication).addActivityToStack(this.javaClass)
window.apply {
- setFlags(
- WindowManager.LayoutParams.FLAG_SECURE,
- WindowManager.LayoutParams.FLAG_SECURE
- )
+ if (!BuildConfig.DEBUG) {
+ // Disable screenshots/screen recording only in production.
+ // Allow automation tools (Espresso/Appium/Layout Inspector)
+ // to capture screenshots in debug builds.
+ setFlags(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.FLAG_SECURE
+ )
+ }
+
+ if (Build.VERSION.SDK_INT in 19..20) {
+ setWindowFlag(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, true)
+ }
+
+ if (Build.VERSION.SDK_INT >= 19) {
+ decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ }
+
+ // make fully Android Transparent Status bar
+ if (Build.VERSION.SDK_INT >= 21) {
+ setWindowFlag(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, false)
+ statusBarColor = Color.TRANSPARENT
+ }
+ }
+ }
+
+ private fun setWindowFlag(bits: Int, on: Boolean) {
+ val win = window
+ val winParams = win.attributes
+ if (on) {
+ winParams.flags = winParams.flags or bits
+ } else {
+ winParams.flags = winParams.flags and bits.inv()
}
+ win.attributes = winParams
}
override fun onNewIntent(intent: Intent) {
diff --git a/android/build.gradle b/android/build.gradle
index d197f00d2c..343a3b0e2a 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -73,4 +73,45 @@ subprojects { subproject ->
} else {
subproject.afterEvaluate { injectPrefixMap() }
}
+
+ gradle.afterProject { proj ->
+ if (proj != rootProject && proj.hasProperty('android')) {
+ proj.android {
+ if (defaultConfig.minSdkVersion == null || defaultConfig.minSdkVersion.apiLevel < 24) {
+ defaultConfig {
+ minSdkVersion 24
+ }
+ }
+
+ if (compileSdkVersion == null || (compileSdkVersion as String).replace("android-", "").toInteger() < 34) {
+ compileSdkVersion 36
+ }
+
+ if (proj.name != 'app') {
+ packagingOptions {
+ pickFirst 'lib/*/libfbjni.so'
+ pickFirst 'lib/*/libc++_shared.so'
+ pickFirst 'lib/*/libjsi.so'
+ pickFirst 'lib/*/libreactnative.so'
+ pickFirst 'lib/*/libhermes.so'
+
+ resources {
+ pickFirsts += 'META-INF/**/coroutines.pro'
+ excludes += [
+ 'META-INF/MANIFEST.MF',
+ 'META-INF/LICENSE',
+ 'META-INF/LICENSE.txt',
+ 'META-INF/LICENSE.md',
+ 'META-INF/LICENSE-notice.md',
+ 'META-INF/NOTICE',
+ 'META-INF/NOTICE.txt',
+ 'META-INF/DEPENDENCIES',
+ 'META-INF/*.kotlin_module'
+ ]
+ }
+ }
+ }
+ }
+ }
+}
}
diff --git a/ios/BitPayApp.xcodeproj/project.pbxproj b/ios/BitPayApp.xcodeproj/project.pbxproj
index 75a3600f96..5716b19b77 100644
--- a/ios/BitPayApp.xcodeproj/project.pbxproj
+++ b/ios/BitPayApp.xcodeproj/project.pbxproj
@@ -22,10 +22,31 @@
5BCF1C352740B8B2006D872E /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5BCF1C342740B8B2006D872E /* BootSplash.storyboard */; };
5BEA4BF02838402700AB2121 /* RCTPaymentPass.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5BEA4BEF2838402700AB2121 /* RCTPaymentPass.mm */; };
6D615C4929725FD70063DB8E /* RCTTimer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6D615C4729725FD70063DB8E /* RCTTimer.mm */; };
- CA5471E01A2B3C4D5E6F7001 /* RCTCrashTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = CA5471E01A2B3C4D5E6F7002 /* RCTCrashTest.mm */; };
6D7C95E72E9007E200F0D481 /* RCTPushPermissionManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6D7C95E62E9007E200F0D481 /* RCTPushPermissionManager.mm */; };
71BABDA4524A43E98A190C87 /* Archivo-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6D649751C2644FDCA18A5547 /* Archivo-Light.ttf */; };
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
+ A484AB14306E7F5A00FE2D63 /* TestConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A484AB13306E7F5A00FE2D63 /* TestConfig.swift */; };
+ CD1957ED2FB37AD40072A9AE /* Test595_BasicSellBTC_.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E92FB37AD40072A9AE /* Test595_BasicSellBTC_.swift */; };
+ CD1957EE2FB37AD40072A9AE /* Test592_ImportWalletRecoveryPhrase.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E62FB37AD40072A9AE /* Test592_ImportWalletRecoveryPhrase.swift */; };
+ CD1957EF2FB37AD40072A9AE /* MykeyPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957DB2FB37AD40072A9AE /* MykeyPage.swift */; };
+ CD1957F02FB37AD40072A9AE /* BuyPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D62FB37AD40072A9AE /* BuyPage.swift */; };
+ CD1957F12FB37AD40072A9AE /* SellPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E12FB37AD40072A9AE /* SellPage.swift */; };
+ CD1957F22FB37AD40072A9AE /* Test591_OnboardingCreateWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E52FB37AD40072A9AE /* Test591_OnboardingCreateWallet.swift */; };
+ CD1957F32FB37AD40072A9AE /* Test596_BasicBuyBTC_.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957EA2FB37AD40072A9AE /* Test596_BasicBuyBTC_.swift */; };
+ CD1957F62FB37AD40072A9AE /* ConfirmPaymentPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D72FB37AD40072A9AE /* ConfirmPaymentPage.swift */; };
+ CD1957F72FB37AD40072A9AE /* AllureXCTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D32FB37AD40072A9AE /* AllureXCTestSupport.swift */; };
+ CD1957F82FB37AD40072A9AE /* EnterAmountPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D82FB37AD40072A9AE /* EnterAmountPage.swift */; };
+ CD1957F92FB37AD40072A9AE /* Test594_BasicSwapBTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E82FB37AD40072A9AE /* Test594_BasicSwapBTC.swift */; };
+ CD1957FA2FB37AD40072A9AE /* AddCryptoOptionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D52FB37AD40072A9AE /* AddCryptoOptionPage.swift */; };
+ CD1957FB2FB37AD40072A9AE /* OnboardingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957DD2FB37AD40072A9AE /* OnboardingPage.swift */; };
+ CD1957FC2FB37AD40072A9AE /* ImportRecoveryPhrasePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957DA2FB37AD40072A9AE /* ImportRecoveryPhrasePage.swift */; };
+ CD1957FD2FB37AD40072A9AE /* PortfolioBalancePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957DE2FB37AD40072A9AE /* PortfolioBalancePage.swift */; };
+ CD1957FE2FB37AD40072A9AE /* SwapPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E22FB37AD40072A9AE /* SwapPage.swift */; };
+ CD1957FF2FB37AD40072A9AE /* Test593_BTCConfirmPaymentScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E72FB37AD40072A9AE /* Test593_BTCConfirmPaymentScreen.swift */; };
+ CD1958002FB37AD40072A9AE /* SelectKeyToDepositPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957E02FB37AD40072A9AE /* SelectKeyToDepositPage.swift */; };
+ CD1958012FB37AD40072A9AE /* SelectCurrencyPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957DF2FB37AD40072A9AE /* SelectCurrencyPage.swift */; };
+ CD1958022FB37AD40072A9AE /* HomePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1957D92FB37AD40072A9AE /* HomePage.swift */; };
+ CD9683A82FB70CCC00192CFF /* AppFlows.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9683A72FB70CC700192CFF /* AppFlows.swift */; };
D34892F44CAB44409EC335A9 /* Archivo-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F4435EB64D2448EC885B3036 /* Archivo-Black.ttf */; };
D9739196BA804EA29143DA96 /* Archivo-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 715670CB346B45A7A9EC2044 /* Archivo-Bold.ttf */; };
E6807D9059A643129784A5C3 /* Archivo-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4DCF32F2D2364E70AD2F1E68 /* Archivo-Thin.ttf */; };
@@ -34,6 +55,16 @@
F64C0BF216CE4402A51A5ACC /* Archivo-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BCDAB70057C84109A9201936 /* Archivo-Regular.ttf */; };
/* End PBXBuildFile section */
+/* Begin PBXContainerItemProxy section */
+ CDF0850D2FACD8A500CA4C1B /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
+ remoteInfo = BitPayApp;
+ };
+/* End PBXContainerItemProxy section */
+
/* Begin PBXFileReference section */
B17FA8700A11CE0000000002 /* BitPayFabricModalPresenter.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BitPayFabricModalPresenter.mm; sourceTree = ""; };
B17FA8700A11CE0000000003 /* BitPayFabricModalPresenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BitPayFabricModalPresenter.h; sourceTree = ""; };
@@ -56,17 +87,38 @@
5BCF1C342740B8B2006D872E /* BootSplash.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = BootSplash.storyboard; path = BitPayApp/BootSplash.storyboard; sourceTree = ""; };
5BEA4BEE2838400700AB2121 /* RCTPaymentPass.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTPaymentPass.h; sourceTree = ""; };
5BEA4BEF2838402700AB2121 /* RCTPaymentPass.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTPaymentPass.mm; sourceTree = ""; };
- 5D5D40A7E7B5265D652187F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = BitPayApp/PrivacyInfo.xcprivacy; sourceTree = ""; };
+ 5D5D40A7E7B5265D652187F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = BitPayApp/PrivacyInfo.xcprivacy; sourceTree = ""; };
6D615C4729725FD70063DB8E /* RCTTimer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTimer.mm; sourceTree = ""; };
6D615C4829725FD70063DB8E /* RCTTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTimer.h; sourceTree = ""; };
- CA5471E01A2B3C4D5E6F7002 /* RCTCrashTest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCrashTest.mm; sourceTree = ""; };
- CA5471E01A2B3C4D5E6F7003 /* RCTCrashTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCrashTest.h; sourceTree = ""; };
6D649751C2644FDCA18A5547 /* Archivo-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Archivo-Light.ttf"; path = "../assets/fonts/Archivo-Light.ttf"; sourceTree = ""; };
6D7C95E62E9007E200F0D481 /* RCTPushPermissionManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTPushPermissionManager.mm; sourceTree = ""; };
715670CB346B45A7A9EC2044 /* Archivo-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Archivo-Bold.ttf"; path = "../assets/fonts/Archivo-Bold.ttf"; sourceTree = ""; };
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = BitPayApp/AppDelegate.swift; sourceTree = ""; };
+ A484AB13306E7F5A00FE2D63 /* TestConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConfig.swift; sourceTree = ""; };
B1AA7DD4226DBC3478258808 /* libPods-BitPayApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-BitPayApp.a"; sourceTree = BUILT_PRODUCTS_DIR; };
BCDAB70057C84109A9201936 /* Archivo-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Archivo-Regular.ttf"; path = "../assets/fonts/Archivo-Regular.ttf"; sourceTree = ""; };
+ CD1957D32FB37AD40072A9AE /* AllureXCTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllureXCTestSupport.swift; sourceTree = ""; };
+ CD1957D52FB37AD40072A9AE /* AddCryptoOptionPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCryptoOptionPage.swift; sourceTree = ""; };
+ CD1957D62FB37AD40072A9AE /* BuyPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyPage.swift; sourceTree = ""; };
+ CD1957D72FB37AD40072A9AE /* ConfirmPaymentPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmPaymentPage.swift; sourceTree = ""; };
+ CD1957D82FB37AD40072A9AE /* EnterAmountPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterAmountPage.swift; sourceTree = ""; };
+ CD1957D92FB37AD40072A9AE /* HomePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePage.swift; sourceTree = ""; };
+ CD1957DA2FB37AD40072A9AE /* ImportRecoveryPhrasePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportRecoveryPhrasePage.swift; sourceTree = ""; };
+ CD1957DB2FB37AD40072A9AE /* MykeyPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MykeyPage.swift; sourceTree = ""; };
+ CD1957DD2FB37AD40072A9AE /* OnboardingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPage.swift; sourceTree = ""; };
+ CD1957DE2FB37AD40072A9AE /* PortfolioBalancePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortfolioBalancePage.swift; sourceTree = ""; };
+ CD1957DF2FB37AD40072A9AE /* SelectCurrencyPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrencyPage.swift; sourceTree = ""; };
+ CD1957E02FB37AD40072A9AE /* SelectKeyToDepositPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectKeyToDepositPage.swift; sourceTree = ""; };
+ CD1957E12FB37AD40072A9AE /* SellPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SellPage.swift; sourceTree = ""; };
+ CD1957E22FB37AD40072A9AE /* SwapPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPage.swift; sourceTree = ""; };
+ CD1957E52FB37AD40072A9AE /* Test591_OnboardingCreateWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test591_OnboardingCreateWallet.swift; sourceTree = ""; };
+ CD1957E62FB37AD40072A9AE /* Test592_ImportWalletRecoveryPhrase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test592_ImportWalletRecoveryPhrase.swift; sourceTree = ""; };
+ CD1957E72FB37AD40072A9AE /* Test593_BTCConfirmPaymentScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test593_BTCConfirmPaymentScreen.swift; sourceTree = ""; };
+ CD1957E82FB37AD40072A9AE /* Test594_BasicSwapBTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test594_BasicSwapBTC.swift; sourceTree = ""; };
+ CD1957E92FB37AD40072A9AE /* Test595_BasicSellBTC_.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test595_BasicSellBTC_.swift; sourceTree = ""; };
+ CD1957EA2FB37AD40072A9AE /* Test596_BasicBuyBTC_.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test596_BasicBuyBTC_.swift; sourceTree = ""; };
+ CD9683A72FB70CC700192CFF /* AppFlows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlows.swift; sourceTree = ""; };
+ CDF085072FACD8A500CA4C1B /* BitPayAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BitPayAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
E30DDDCF29E5E048D75CCF1A /* BundleHash.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BitPayApp/BundleHash.swift; sourceTree = ""; };
E639E0380D05420CD0D64119 /* Pods-BitPayApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BitPayApp.release.xcconfig"; path = "Target Support Files/Pods-BitPayApp/Pods-BitPayApp.release.xcconfig"; sourceTree = ""; };
EC0FDD692CC92E90009E1B35 /* InAppMessageModule.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InAppMessageModule.mm; sourceTree = ""; };
@@ -87,6 +139,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ CDF085042FACD8A500CA4C1B /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -106,8 +165,6 @@
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
6D615C4829725FD70063DB8E /* RCTTimer.h */,
6D615C4729725FD70063DB8E /* RCTTimer.mm */,
- CA5471E01A2B3C4D5E6F7003 /* RCTCrashTest.h */,
- CA5471E01A2B3C4D5E6F7002 /* RCTCrashTest.mm */,
5B31AAFC2786381C002765E1 /* RCTDosh.h */,
5B31AAFD2786387B002765E1 /* RCTDosh.mm */,
5B31AB5E2786515A002765E1 /* Dosh.swift */,
@@ -168,10 +225,12 @@
children = (
13B07FAE1A68108700A75B9A /* BitPayApp */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
+ CDF085082FACD8A500CA4C1B /* BitPayAppUITests */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
7950FCCDC90E6306EDD77BCD /* Pods */,
3A6F88D6BF7C4F7A8D93264A /* Resources */,
+ CDF085022FACD87900CA4C1B /* Recovered References */,
);
indentWidth = 2;
sourceTree = "";
@@ -182,10 +241,80 @@
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* BitPayApp.app */,
+ CDF085072FACD8A500CA4C1B /* BitPayAppUITests.xctest */,
);
name = Products;
sourceTree = "";
};
+ CD1957D42FB37AD40072A9AE /* AllureReport */ = {
+ isa = PBXGroup;
+ children = (
+ CD1957D32FB37AD40072A9AE /* AllureXCTestSupport.swift */,
+ );
+ path = AllureReport;
+ sourceTree = "";
+ };
+ CD1957E42FB37AD40072A9AE /* pages */ = {
+ isa = PBXGroup;
+ children = (
+ CD1957D52FB37AD40072A9AE /* AddCryptoOptionPage.swift */,
+ CD1957D62FB37AD40072A9AE /* BuyPage.swift */,
+ CD1957D72FB37AD40072A9AE /* ConfirmPaymentPage.swift */,
+ CD1957D82FB37AD40072A9AE /* EnterAmountPage.swift */,
+ CD1957D92FB37AD40072A9AE /* HomePage.swift */,
+ CD1957DA2FB37AD40072A9AE /* ImportRecoveryPhrasePage.swift */,
+ CD1957DB2FB37AD40072A9AE /* MykeyPage.swift */,
+ CD1957DD2FB37AD40072A9AE /* OnboardingPage.swift */,
+ CD1957DE2FB37AD40072A9AE /* PortfolioBalancePage.swift */,
+ CD1957DF2FB37AD40072A9AE /* SelectCurrencyPage.swift */,
+ CD1957E02FB37AD40072A9AE /* SelectKeyToDepositPage.swift */,
+ CD1957E12FB37AD40072A9AE /* SellPage.swift */,
+ CD1957E22FB37AD40072A9AE /* SwapPage.swift */,
+ );
+ path = pages;
+ sourceTree = "";
+ };
+ CD1957EC2FB37AD40072A9AE /* tests */ = {
+ isa = PBXGroup;
+ children = (
+ CD1957E52FB37AD40072A9AE /* Test591_OnboardingCreateWallet.swift */,
+ CD1957E62FB37AD40072A9AE /* Test592_ImportWalletRecoveryPhrase.swift */,
+ CD1957E72FB37AD40072A9AE /* Test593_BTCConfirmPaymentScreen.swift */,
+ CD1957E82FB37AD40072A9AE /* Test594_BasicSwapBTC.swift */,
+ CD1957E92FB37AD40072A9AE /* Test595_BasicSellBTC_.swift */,
+ CD1957EA2FB37AD40072A9AE /* Test596_BasicBuyBTC_.swift */,
+ );
+ path = tests;
+ sourceTree = "";
+ };
+ CD96839F2FB70CAF00192CFF /* flows */ = {
+ isa = PBXGroup;
+ children = (
+ CD9683A72FB70CC700192CFF /* AppFlows.swift */,
+ );
+ path = flows;
+ sourceTree = "";
+ };
+ CDF085022FACD87900CA4C1B /* Recovered References */ = {
+ isa = PBXGroup;
+ children = (
+ 6D7C95E62E9007E200F0D481 /* RCTPushPermissionManager.mm */,
+ );
+ name = "Recovered References";
+ sourceTree = "";
+ };
+ CDF085082FACD8A500CA4C1B /* BitPayAppUITests */ = {
+ isa = PBXGroup;
+ children = (
+ A484AB13306E7F5A00FE2D63 /* TestConfig.swift */,
+ CD1957D42FB37AD40072A9AE /* AllureReport */,
+ CD1957E42FB37AD40072A9AE /* pages */,
+ CD1957EC2FB37AD40072A9AE /* tests */,
+ CD96839F2FB70CAF00192CFF /* flows */,
+ );
+ path = BitPayAppUITests;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -212,17 +341,40 @@
productReference = 13B07F961A680F5B00A75B9A /* BitPayApp.app */;
productType = "com.apple.product-type.application";
};
+ CDF085062FACD8A500CA4C1B /* BitPayAppUITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = CDF085112FACD8A500CA4C1B /* Build configuration list for PBXNativeTarget "BitPayAppUITests" */;
+ buildPhases = (
+ CDF085032FACD8A500CA4C1B /* Sources */,
+ CDF085042FACD8A500CA4C1B /* Frameworks */,
+ CDF085052FACD8A500CA4C1B /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ CDF0850E2FACD8A500CA4C1B /* PBXTargetDependency */,
+ );
+ name = BitPayAppUITests;
+ productName = BitPayAppUITests;
+ productReference = CDF085072FACD8A500CA4C1B /* BitPayAppUITests.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
+ LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 1210;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
LastSwiftMigration = 1240;
};
+ CDF085062FACD8A500CA4C1B = {
+ CreatedOnToolsVersion = 26.0.1;
+ TestTargetID = 13B07F861A680F5B00A75B9A;
+ };
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "BitPayApp" */;
@@ -239,6 +391,7 @@
projectRoot = "";
targets = (
13B07F861A680F5B00A75B9A /* BitPayApp */,
+ CDF085062FACD8A500CA4C1B /* BitPayAppUITests */,
);
};
/* End PBXProject section */
@@ -262,6 +415,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ CDF085052FACD8A500CA4C1B /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -277,7 +437,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "set -e\n\nif [ -z \"$NODE_BINARY\" ]; then\n export NODE_BINARY=$(command -v node || true)\nfi\nif [ -z \"$NODE_BINARY\" ] || [ ! -x \"$NODE_BINARY\" ]; then\n echo \"error: NODE_BINARY not set and 'node' not found on PATH.\"\n exit 1\nfi\necho \"Using NODE_BINARY=$NODE_BINARY\"\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nSENTRY_XCODE=\"../node_modules/@sentry/react-native/scripts/sentry-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $SENTRY_XCODE\"\n";
+ shellScript = "export SENTRY_DISABLE_AUTO_UPLOAD=true\n\nset -e\n\nif [ -z \"$NODE_BINARY\" ]; then\n export NODE_BINARY=$(command -v node || true)\nfi\nif [ -z \"$NODE_BINARY\" ] || [ ! -x \"$NODE_BINARY\" ]; then\n echo \"error: NODE_BINARY not set and 'node' not found on PATH.\"\n exit 1\nfi\necho \"Using NODE_BINARY=$NODE_BINARY\"\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nSENTRY_XCODE=\"../node_modules/@sentry/react-native/scripts/sentry-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $SENTRY_XCODE\"\n";
};
3C1070649A3322DC1391852F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
@@ -383,22 +543,57 @@
6D7C95E72E9007E200F0D481 /* RCTPushPermissionManager.mm in Sources */,
5BEA4BF02838402700AB2121 /* RCTPaymentPass.mm in Sources */,
6D615C4929725FD70063DB8E /* RCTTimer.mm in Sources */,
- CA5471E01A2B3C4D5E6F7001 /* RCTCrashTest.mm in Sources */,
ECCB92782860B25D00F9CF8B /* SilentPushEvent.mm in Sources */,
5B31AB5F2786515A002765E1 /* Dosh.swift in Sources */,
52C9714B9004BCE190EA469D /* BundleHash.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+ CDF085032FACD8A500CA4C1B /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ CD1957ED2FB37AD40072A9AE /* Test595_BasicSellBTC_.swift in Sources */,
+ CD1957EE2FB37AD40072A9AE /* Test592_ImportWalletRecoveryPhrase.swift in Sources */,
+ CD1957EF2FB37AD40072A9AE /* MykeyPage.swift in Sources */,
+ CD1957F02FB37AD40072A9AE /* BuyPage.swift in Sources */,
+ CD1957F12FB37AD40072A9AE /* SellPage.swift in Sources */,
+ CD1957F22FB37AD40072A9AE /* Test591_OnboardingCreateWallet.swift in Sources */,
+ CD1957F32FB37AD40072A9AE /* Test596_BasicBuyBTC_.swift in Sources */,
+ CD9683A82FB70CCC00192CFF /* AppFlows.swift in Sources */,
+ CD1957F62FB37AD40072A9AE /* ConfirmPaymentPage.swift in Sources */,
+ CD1957F72FB37AD40072A9AE /* AllureXCTestSupport.swift in Sources */,
+ CD1957F82FB37AD40072A9AE /* EnterAmountPage.swift in Sources */,
+ CD1957F92FB37AD40072A9AE /* Test594_BasicSwapBTC.swift in Sources */,
+ CD1957FA2FB37AD40072A9AE /* AddCryptoOptionPage.swift in Sources */,
+ CD1957FB2FB37AD40072A9AE /* OnboardingPage.swift in Sources */,
+ CD1957FC2FB37AD40072A9AE /* ImportRecoveryPhrasePage.swift in Sources */,
+ CD1957FD2FB37AD40072A9AE /* PortfolioBalancePage.swift in Sources */,
+ CD1957FE2FB37AD40072A9AE /* SwapPage.swift in Sources */,
+ A484AB14306E7F5A00FE2D63 /* TestConfig.swift in Sources */,
+ CD1957FF2FB37AD40072A9AE /* Test593_BTCConfirmPaymentScreen.swift in Sources */,
+ CD1958002FB37AD40072A9AE /* SelectKeyToDepositPage.swift in Sources */,
+ CD1958012FB37AD40072A9AE /* SelectCurrencyPage.swift in Sources */,
+ CD1958022FB37AD40072A9AE /* HomePage.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
+/* Begin PBXTargetDependency section */
+ CDF0850E2FACD8A500CA4C1B /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 13B07F861A680F5B00A75B9A /* BitPayApp */;
+ targetProxy = CDF0850D2FACD8A500CA4C1B /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 4221E54904CA4996330CEB54 /* Pods-BitPayApp.debug.xcconfig */;
buildSettings = {
- ALLOWED_URL_PREFIXES = "ALLOWED-URL-PREFIXES-REPLACE-ME";
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
+ ALLOWED_URL_PREFIXES = "https://bitpay.com/,https://test.bitpay.com/,https://staging.bitpay.com/,https://bws.bitpay.com/,https://www.coinbase.com/,https://api.coinbase.com/,https://api.1inch.dev/swap/v5.2/,https://api.coingecko.com/api/v3/simple/token_price/,https://checkout.simplexcc.com/,https://sandbox.test-simplexcc.com,https://api.moonpay.com/,https://api.moonpay.dev/,https://api.zenledger.io/bitpay/wallets/,https://rest.iad-05.braze.com/,https://sdk.iad-05.braze.com/,https://cloudflare-eth.com/,https://sepolia.infura.io/v3/,https://polygon-rpc.com/,https://polygon-amoy.core.chainstack.com/,https://static.methodfi.com/,https://api.zenledger.io/,https://api.blockcypher.com/v1/,https://api.fullstack.cash/v5/,https://deep-index.moralis.io/api/v2.2/,https://api.bitcore.io/api,https://api-eth.bitcore.io/api,https://api-matic.bitcore.io/api,https://api-arb.bitcore.io/api,https://api-base.bitcore.io/api,https://api-op.bitcore.io/api,https://api-xrp.bitcore.io/api,https://api-sol.bitcore.io/api,https://portal.polygon.technology/bridge,https://bridge.arbitrum.io/,https://bridge.base.org/,https://app.optimism.io/bridge/,https://mainnet.optimism.io/,https://arb1.arbitrum.io/rpc,https://mainnet.base.org/,https://goerli.optimism.io,https://goerli-rollup.arbitrum.io/rpc,https://goerli.base.org,https://verify.walletconnect.com/,https://verify.walletconnect.org/,https://api.etherscan.io/v2/api,https://solana-gateway.moralis.io/,https://tokens-data.1inch.io/images/501/";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO;
CLANG_ENABLE_MODULES = YES;
@@ -443,8 +638,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = E639E0380D05420CD0D64119 /* Pods-BitPayApp.release.xcconfig */;
buildSettings = {
- ALLOWED_URL_PREFIXES = "ALLOWED-URL-PREFIXES-REPLACE-ME";
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
+ ALLOWED_URL_PREFIXES = "https://bitpay.com/,https://test.bitpay.com/,https://staging.bitpay.com/,https://bws.bitpay.com/,https://www.coinbase.com/,https://api.coinbase.com/,https://api.1inch.dev/swap/v5.2/,https://api.coingecko.com/api/v3/simple/token_price/,https://checkout.simplexcc.com/,https://sandbox.test-simplexcc.com,https://api.moonpay.com/,https://api.moonpay.dev/,https://api.zenledger.io/bitpay/wallets/,https://rest.iad-05.braze.com/,https://sdk.iad-05.braze.com/,https://cloudflare-eth.com/,https://sepolia.infura.io/v3/,https://polygon-rpc.com/,https://polygon-amoy.core.chainstack.com/,https://static.methodfi.com/,https://api.zenledger.io/,https://api.blockcypher.com/v1/,https://api.fullstack.cash/v5/,https://deep-index.moralis.io/api/v2.2/,https://api.bitcore.io/api,https://api-eth.bitcore.io/api,https://api-matic.bitcore.io/api,https://api-arb.bitcore.io/api,https://api-base.bitcore.io/api,https://api-op.bitcore.io/api,https://api-xrp.bitcore.io/api,https://api-sol.bitcore.io/api,https://portal.polygon.technology/bridge,https://bridge.arbitrum.io/,https://bridge.base.org/,https://app.optimism.io/bridge/,https://mainnet.optimism.io/,https://arb1.arbitrum.io/rpc,https://mainnet.base.org/,https://goerli.optimism.io,https://goerli-rollup.arbitrum.io/rpc,https://goerli.base.org,https://verify.walletconnect.com/,https://verify.walletconnect.org/,https://api.etherscan.io/v2/api,https://solana-gateway.moralis.io/,https://tokens-data.1inch.io/images/501/";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO;
CLANG_ENABLE_MODULES = YES;
@@ -673,6 +867,81 @@
};
name = Release;
};
+ CDF0850F2FACD8A500CA4C1B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.test.appium.BitPayAppUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = NO;
+ STRING_CATALOG_GENERATE_SYMBOLS = NO;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = BitPayApp;
+ };
+ name = Debug;
+ };
+ CDF085102FACD8A500CA4C1B /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.test.appium.BitPayAppUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = NO;
+ STRING_CATALOG_GENERATE_SYMBOLS = NO;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = BitPayApp;
+ };
+ name = Release;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -694,6 +963,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ CDF085112FACD8A500CA4C1B /* Build configuration list for PBXNativeTarget "BitPayAppUITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ CDF0850F2FACD8A500CA4C1B /* Debug */,
+ CDF085102FACD8A500CA4C1B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
diff --git a/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme b/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme
index 3874813c99..92d73acdf4 100644
--- a/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme
+++ b/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme
@@ -26,7 +26,9 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
- shouldUseLaunchSchemeArgsEnv = "YES">
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ systemAttachmentLifetime = "keepAlways"
+ userAttachmentLifetime = "keepAlways">
@@ -38,6 +40,17 @@
ReferencedContainer = "container:BitPayApp.xcodeproj">
+
+
+
+
:` convention.
+ static func addLabel(_ name: String, value: String) {
+ XCTContext.runActivity(named: "allure.label.\(name):\(value)") { _ in }
+ }
+
+ /// Sets a custom test name visible in the report.
+ static func setDisplayName(_ name: String) {
+ XCTContext.runActivity(named: "allure.name:\(name)") { _ in }
+ }
+
+ /// Adds a link to an issue tracker entry.
+ static func addIssue(_ title: String, url: String) {
+ XCTContext.runActivity(named: "allure.issue.\(title):\(url)") { _ in }
+ }
+
+ /// Adds a generic hyperlink label.
+ static func addLink(_ title: String, url: String) {
+ XCTContext.runActivity(named: "allure.link.\(title):\(url)") { _ in }
+ }
+
+ /// Adds a plain description to the test.
+ static func addDescription(_ description: String) {
+ XCTContext.runActivity(named: "allure.description:\(description)") { _ in }
+ }
+
+ /// Wraps a code block in an activity that appears as a step in Allure.
+ @discardableResult
+ static func step(_ name: String, block: () throws -> T) rethrows -> T {
+ try XCTContext.runActivity(named: name) { _ in try block() }
+ }
+}
+
+extension XCTestCase {
+ /// Attaches a full-screen capture to the current test. Lifetime is `.keepAlways`
+ /// so it remains available in the `.xcresult` bundle for Allure to render.
+ func allureAttachScreenshot(name: String = "Screenshot") {
+ let screenshot = XCUIScreen.main.screenshot()
+ let attachment = XCTAttachment(screenshot: screenshot)
+ attachment.name = name
+ attachment.lifetime = .keepAlways
+ add(attachment)
+ }
+
+ /// Attaches a screenshot of the supplied application.
+ func allureAttachAppScreenshot(app: XCUIApplication, name: String = "App Screenshot") {
+ let attachment = XCTAttachment(screenshot: app.screenshot())
+ attachment.name = name
+ attachment.lifetime = .keepAlways
+ add(attachment)
+ }
+
+ /// Captures failure-time artifacts if the current test did not succeed.
+ func allureCaptureFailureArtifacts(app: XCUIApplication?, name: String = "Failure Screenshot") {
+ guard let app else { return }
+ let attachment = XCTAttachment(screenshot: app.screenshot())
+ attachment.name = name
+ attachment.lifetime = .keepAlways
+ add(attachment)
+ }
+}
+
diff --git a/ios/BitPayAppUITests/TestConfig.swift b/ios/BitPayAppUITests/TestConfig.swift
new file mode 100644
index 0000000000..c8aced0863
--- /dev/null
+++ b/ios/BitPayAppUITests/TestConfig.swift
@@ -0,0 +1,5 @@
+import Foundation
+
+enum TestConfig {
+ static let walletPhrase = "WALLET_PHRASE_REPLACE_ME"
+}
\ No newline at end of file
diff --git a/ios/BitPayAppUITests/flows/AppFlows.swift b/ios/BitPayAppUITests/flows/AppFlows.swift
new file mode 100644
index 0000000000..31fe2645c5
--- /dev/null
+++ b/ios/BitPayAppUITests/flows/AppFlows.swift
@@ -0,0 +1,89 @@
+//
+// AppFlows.swift
+// BitPayApp
+//
+// Created by vinoth vasu on 15/05/26.
+//
+
+import XCTest
+
+final class AppFlows {
+
+ static func completeOnboardingIfRequired(app: XCUIApplication) {
+
+ let onboardingPage = OnboardingPage(app: app)
+
+ if onboardingPage.isContinueWithoutAccountButtonDisplayed() {
+
+ onboardingPage.tapContinuewithoutAnAccount()
+
+ onboardingPage.skipOnboarding()
+ Thread.sleep(forTimeInterval: 2.0)
+ onboardingPage.skipOnboarding()
+
+ onboardingPage.createWallet()
+
+ guard onboardingPage.isBackupKeyLabelDisplayed() else {
+ XCTFail("Backup key page did not appear")
+ return
+ }
+
+ onboardingPage.skipBackup()
+ Thread.sleep(forTimeInterval: 2.0)
+ onboardingPage.tapLater()
+ onboardingPage.acceptTerms()
+ onboardingPage.tapAgreeAndContinueButton()
+
+ XCTAssertTrue(
+ onboardingPage.isYourPortfolioBalanceTextDisplayed(),
+ "Home screen did not load"
+ )
+ }
+ }
+
+ /// Recovers the app to the Home screen.
+
+ static func goToHomeScreen(app: XCUIApplication, attempts: Int = 15) {
+ let onboardingPage = OnboardingPage(app: app)
+
+ let portfolio = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'portfolio-balance-info-button'")
+ ).firstMatch
+ let homeTab = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label BEGINSWITH 'Home, tab'")
+ ).firstMatch
+ let backControl = app.descendants(matching: .any).element(
+ matching: NSPredicate(
+ format: "identifier IN {'back-button','header-back-button','back-arrow','nav-back-button','close-button'} "
+ + "OR label IN {'Back','Close','Cancel','Go back'}"
+ )
+ ).firstMatch
+
+ for _ in 0.. Bool {
+ return selectAnOptionText.waitForExistence(timeout: timeout)
+ }
+
+ func tapImportKey() {
+ importKey.tap()
+ }
+
+}
diff --git a/ios/BitPayAppUITests/pages/BuyPage.swift b/ios/BitPayAppUITests/pages/BuyPage.swift
new file mode 100644
index 0000000000..7b472dc00f
--- /dev/null
+++ b/ios/BitPayAppUITests/pages/BuyPage.swift
@@ -0,0 +1,36 @@
+import XCTest
+
+class BuyPage {
+
+ let app: XCUIApplication
+
+ init(app: XCUIApplication) {
+ self.app = app
+ }
+
+ // MARK: - Elements
+
+ // Detect the Buy screen by its amount keypad; "Buy" static text also exists on Home.
+ var buyTitle: XCUIElement {
+ app.staticTexts["5"].firstMatch
+ }
+
+ var bitcoin: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "label == 'Bitcoin'")
+ ).firstMatch
+ }
+
+
+ // MARK: - Validations
+
+ func isBuyPageTitleDisplayed(timeout: TimeInterval = 30) -> Bool {
+ return buyTitle.waitForExistence(timeout: timeout)
+ }
+
+ func tapBitcoin() {
+ bitcoin.tap()
+ }
+
+
+}
diff --git a/ios/BitPayAppUITests/pages/ConfirmPaymentPage.swift b/ios/BitPayAppUITests/pages/ConfirmPaymentPage.swift
new file mode 100644
index 0000000000..fd82fc0aac
--- /dev/null
+++ b/ios/BitPayAppUITests/pages/ConfirmPaymentPage.swift
@@ -0,0 +1,45 @@
+
+//
+// OnboardingPage.swift
+// BitPayApp
+//
+// Created by vinoth vasu on 16/03/26.
+//
+
+import XCTest
+
+class ConfirmPaymentPage {
+
+ let app: XCUIApplication
+
+ init(app: XCUIApplication) {
+ self.app = app
+ }
+
+ // MARK: - Elements
+
+ var confirmPaymentText: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'Confirm Payment'")
+ ).firstMatch
+ }
+
+ var summaryText: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'SUMMARY'")
+ ).firstMatch
+ }
+
+
+ // MARK: - Actions
+
+ func isConfirmPaymentTitleDisplayed(timeout: TimeInterval = 180) -> Bool {
+ return confirmPaymentText.waitForExistence(timeout: timeout)
+ }
+
+ func isSummaryTextDisplayed(timeout: TimeInterval = 5) -> Bool {
+ return summaryText.waitForExistence(timeout: timeout)
+ }
+
+
+}
diff --git a/ios/BitPayAppUITests/pages/EnterAmountPage.swift b/ios/BitPayAppUITests/pages/EnterAmountPage.swift
new file mode 100644
index 0000000000..ad10ee8b1b
--- /dev/null
+++ b/ios/BitPayAppUITests/pages/EnterAmountPage.swift
@@ -0,0 +1,89 @@
+
+//
+// OnboardingPage.swift
+// BitPayApp
+//
+// Created by vinoth vasu on 16/03/26.
+//
+
+import XCTest
+
+class EnterAmountPage {
+
+ let app: XCUIApplication
+
+ init(app: XCUIApplication) {
+ self.app = app
+ }
+
+ // MARK: - Elements
+ var amountField: XCUIElement {
+ app.staticTexts.matching(identifier: "0").firstMatch
+ }
+
+ var continueButton: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label CONTAINS 'Continue'")
+ ).firstMatch
+ }
+
+ // MARK: - Actions
+ func enterAmount(amount: String = "0") {
+ for char in amount {
+ let key = app.staticTexts[String(char)].firstMatch
+ XCTAssertTrue(
+ key.waitForExistence(timeout: 15),
+ "'\(char)' key not found on the amount keypad"
+ )
+ key.tap()
+ }
+ }
+
+ func tapContinue() {
+ continueButton.tap()
+ }
+
+ /**
+ Backspace key does not expose a unique accessibility identifier.
+ Calculate its position dynamically using the relative spacing
+ between the keypad keys "6" and "9".
+ */
+ func tapBackspace(count: Int = 1) {
+
+ let sixKey = app.staticTexts["6"].firstMatch
+ let nineKey = app.staticTexts["9"].firstMatch
+
+ XCTAssertTrue(
+ sixKey.waitForExistence(timeout: 15),
+ "6 key not found - amount keypad is not on screen"
+ )
+
+ XCTAssertTrue(
+ nineKey.waitForExistence(timeout: 15),
+ "9 key not found - amount keypad is not on screen"
+ )
+
+ let sixFrame = sixKey.frame
+ let nineFrame = nineKey.frame
+
+ let rowHeight = abs(nineFrame.midY - sixFrame.midY)
+
+ let backspaceX = nineFrame.midX
+ let backspaceY = nineFrame.midY + rowHeight
+
+ let backspaceCoordinate = app.coordinate(
+ withNormalizedOffset: CGVector(dx: 0, dy: 0)
+ ).withOffset(
+ CGVector(
+ dx: backspaceX,
+ dy: backspaceY
+ )
+ )
+
+ for _ in 0.. Bool {
+ return backupKeyLabel.waitForExistence(timeout: timeout)
+ }
+
+ func tapLater() {
+ laterButton.tap()
+ }
+
+ func acceptTerms() {
+ checkbox1.tap()
+ checkbox2.tap()
+ checkbox3.tap()
+ }
+
+ func tapAgreeAndContinueButton() {
+ agreeAndContinueButton.tap()
+ }
+
+ func isYourPortfolioBalanceTextDisplayed(timeout: TimeInterval = 25) -> Bool {
+ return yourPortfolioBalanceText.waitForExistence(timeout: timeout)
+ }
+
+ func swipeOnboarding() {
+ onboardingScrollView.swipeRight()
+ }
+
+ func tapGotIt() {
+ gotItButton.tap()
+ }
+
+ func tapBuyOption() {
+ buyButton.tap()
+ }
+
+ func tapBackUpKey() {
+ backUpKey.tap()
+ }
+
+ func tapAddCryptoButton() {
+ addCryptoButton.tap()
+ }
+
+}
diff --git a/ios/BitPayAppUITests/pages/ImportRecoveryPhrasePage.swift b/ios/BitPayAppUITests/pages/ImportRecoveryPhrasePage.swift
new file mode 100644
index 0000000000..ebed0b9eac
--- /dev/null
+++ b/ios/BitPayAppUITests/pages/ImportRecoveryPhrasePage.swift
@@ -0,0 +1,39 @@
+import XCTest
+
+class ImportRecoveryPhrasePage {
+
+ let app: XCUIApplication
+
+ init(app: XCUIApplication) {
+ self.app = app
+ }
+
+ // MARK: - Elements
+
+ var inputRecoveryPhrase: XCUIElement {
+ app.descendants(matching: .any)
+ .matching(NSPredicate(format: "label == 'Enter recovery phrase'"))
+ .firstMatch
+ }
+
+ var importWalletButton: XCUIElement {
+ app.descendants(matching: .any)
+ .matching(NSPredicate(format: "label == 'Import Wallet'"))
+ .firstMatch
+ }
+
+ // MARK: - Actions
+
+ func isImportScreenDisplayed(timeout: TimeInterval = 5) -> Bool {
+ return inputRecoveryPhrase.waitForExistence(timeout: timeout)
+ }
+
+ func enterRecoveryPhrase(_ phrase: String) {
+ inputRecoveryPhrase.tap()
+ inputRecoveryPhrase.typeText(phrase)
+ }
+
+ func tapImportWallet() {
+ importWalletButton.tap()
+ }
+}
diff --git a/ios/BitPayAppUITests/pages/MykeyPage.swift b/ios/BitPayAppUITests/pages/MykeyPage.swift
new file mode 100644
index 0000000000..1fb5546839
--- /dev/null
+++ b/ios/BitPayAppUITests/pages/MykeyPage.swift
@@ -0,0 +1,45 @@
+import XCTest
+
+class MyKeyPage {
+
+ let app: XCUIApplication
+
+ init(app: XCUIApplication) {
+ self.app = app
+ }
+
+ // MARK: - Elements
+
+ var myKeyTab: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "label == 'My Key'")
+ ).firstMatch
+ }
+
+ var myWalletsText: XCUIElement {
+ app.staticTexts.matching(
+ NSPredicate(format: "label == 'My Wallets'")
+ ).firstMatch
+ }
+
+ var bitcoinWallet: XCUIElement {
+ app.descendants(matching: .any).matching(
+ NSPredicate(format: "label CONTAINS[c] %@", "Bitcoin")
+ ).firstMatch
+ }
+
+ // MARK: - Validations
+
+ func isMyKeyDisplayed(timeout: TimeInterval = 900) -> Bool {
+ return myKeyTab.waitForExistence(timeout: timeout)
+ }
+
+ func isMyWalletsDisplayed(timeout: TimeInterval = 5) -> Bool {
+ return myWalletsText.waitForExistence(timeout: timeout)
+ }
+
+ func isBitcoinBTCWalletDisplayed(timeout: TimeInterval = 300) -> Bool {
+ return bitcoinWallet.waitForExistence(timeout: timeout)
+ }
+
+}
diff --git a/ios/BitPayAppUITests/pages/OnboardingPage.swift b/ios/BitPayAppUITests/pages/OnboardingPage.swift
new file mode 100644
index 0000000000..0996ac8ebf
--- /dev/null
+++ b/ios/BitPayAppUITests/pages/OnboardingPage.swift
@@ -0,0 +1,388 @@
+//
+// OnboardingPage.swift
+// BitPayApp
+//
+// Created by vinoth vasu on 10/03/26.
+//
+
+import XCTest
+
+class OnboardingPage {
+
+ let app: XCUIApplication
+
+ init(app: XCUIApplication) {
+ self.app = app
+ }
+
+ // MARK: - Elements
+
+ var continueWithoutAnAccountButtonById: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'continue-without-an-account-button'")
+ ).firstMatch
+ }
+
+ var continueWithoutAnAccountButtonByLabel: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'Continue without an account'")
+ ).firstMatch
+ }
+
+ var continueWithoutAnAccountButtonByA11yIdAsLabel: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'continue-without-an-account-button'")
+ ).firstMatch
+ }
+
+ var skipButton: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'skip-button'")
+ ).firstMatch
+ }
+
+ var skipBackupButton: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'skip-button'")
+ ).firstMatch
+ }
+
+ var createKeyButton: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'create-a-key-button'")
+ ).firstMatch
+ }
+
+ var alreadyHaveWalletKeyButton: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'i-already-have-a-key-button'")
+ ).firstMatch
+ }
+
+ var backupKeyLabel: XCUIElement {
+ app.staticTexts["Would you like to backup your key?"]
+ }
+
+ var laterButton: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'bottom-notification-secondary-action-button'")
+ ).firstMatch
+ }
+
+ var gotItButton: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'bottom-notification-primary-action-button'")
+ ).firstMatch
+ }
+
+ var checkbox1: XCUIElement {
+ app.descendants(matching: .any).matching(identifier: "first-term-checkbox").firstMatch
+ }
+
+ var checkbox2: XCUIElement {
+ app.descendants(matching: .any).matching(identifier: "second-term-checkbox").firstMatch
+ }
+
+ var checkbox3: XCUIElement {
+ app.descendants(matching: .any).matching(identifier: "third-term-checkbox").firstMatch
+ }
+
+ var agreeAndContinueButton: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'agree-and-continue-button'")
+ ).firstMatch
+ }
+
+ var yourPortfolioBalanceText: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'portfolio-balance-info-button'")
+ ).firstMatch
+ }
+
+ var onboardingScrollView: XCUIElement {
+ app.scrollViews.firstMatch
+ }
+
+ // MARK: - Actions
+
+ func handleTrackingPermissionIfDisplayed(timeout: TimeInterval = 30) {
+ let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
+ let askAppNotToTrackButton = springboard.buttons["Ask App Not to Track"]
+
+ if askAppNotToTrackButton.waitForExistence(timeout: timeout) {
+ askAppNotToTrackButton.tap()
+ }
+ }
+
+
+ /// No-op in Release builds or when no overlay is present. Mirrors the Android
+ /// BaseTest `dismissLogboxIfPresent` / `dismissDebuggerNotificationIfPresent`.
+ func dismissReactNativeDevOverlays(attempts: Int = 5) {
+ let dismissLabel = NSPredicate(
+ format: "label CONTAINS[c] 'debugger' OR label CONTAINS[c] 'view warnings' OR label CONTAINS[c] 'view errors'"
+ )
+
+ for _ in 0.. Bool {
+ dismissReactNativeDevOverlays()
+ let deadline = Date().addingTimeInterval(timeout)
+ while Date() < deadline {
+ if continueWithoutAnAccountButtonById.exists ||
+ continueWithoutAnAccountButtonByLabel.exists ||
+ continueWithoutAnAccountButtonByA11yIdAsLabel.exists {
+ return true
+ }
+ RunLoop.current.run(until: Date().addingTimeInterval(0.25))
+ }
+ return false
+ }
+
+ func tapContinuewithoutAnAccount() {
+ dismissReactNativeDevOverlays()
+
+ if isPostContinueScreenVisible(timeout: 2) {
+ return
+ }
+
+ XCTAssertTrue(
+ tapFirstHittableElement(
+ candidates: [
+ continueWithoutAnAccountButtonById,
+ continueWithoutAnAccountButtonByLabel,
+ continueWithoutAnAccountButtonByA11yIdAsLabel,
+ ],
+ timeout: 25
+ ),
+ "Continue without an account button not tappable"
+ )
+ }
+
+ func skipOnboarding() {
+ if skipButton.waitForExistence(timeout: 30) {
+ skipButton.tap()
+ return
+ }
+
+ // Fallback: Pin screen body "Maybe Later" button (skip-security-button)
+ let skipSecurityButton = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'skip-security-button'")
+ ).firstMatch
+ if skipSecurityButton.waitForExistence(timeout: 10) {
+ skipSecurityButton.tap()
+ return
+ }
+
+ let skipLabelFallback = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'Skip'")
+ ).firstMatch
+ XCTAssertTrue(skipLabelFallback.waitForExistence(timeout: 5), "Skip button not found")
+ skipLabelFallback.tap()
+ }
+
+ func skipBackup() {
+ if skipBackupButton.waitForExistence(timeout: 20) {
+ skipBackupButton.tap()
+ return
+ }
+
+ let skipBackupLabelFallback = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'Skip backup'")
+ ).firstMatch
+ XCTAssertTrue(skipBackupLabelFallback.waitForExistence(timeout: 5), "Skip backup button not found")
+ skipBackupLabelFallback.tap()
+ }
+
+ func createWallet() {
+ createKeyButton.tap()
+ }
+
+ func alreadyHaveWalletKey() {
+ alreadyHaveWalletKeyButton.tap()
+ }
+
+ func isBackupKeyLabelDisplayed(timeout: TimeInterval = 120) -> Bool {
+ return backupKeyLabel.waitForExistence(timeout: timeout)
+ }
+
+ func tapLater() {
+ if laterButton.waitForExistence(timeout: 20) {
+ laterButton.tap()
+ return
+ }
+
+ let laterLabelFallback = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'LATER'")
+ ).firstMatch
+ XCTAssertTrue(laterLabelFallback.waitForExistence(timeout: 5), "Later button not found")
+ laterLabelFallback.tap()
+ }
+
+ func acceptTerms() {
+ XCTAssertTrue(waitForCheckbox("first-term-checkbox", timeout: 30), "First term checkbox not found")
+
+ tapCheckbox("first-term-checkbox")
+ tapCheckbox("second-term-checkbox")
+ tapCheckbox("third-term-checkbox")
+
+ XCTAssertTrue(
+ waitForAgreeAndContinueEnabled(timeout: 10),
+ "Agree and Continue did not enable after accepting all terms"
+ )
+ }
+
+ func tapAgreeAndContinueButton() {
+ XCTAssertTrue(agreeAndContinueButton.waitForExistence(timeout: 20), "Agree and Continue button not found")
+ for _ in 1...8 {
+ if agreeAndContinueButton.isEnabled {
+ break
+ }
+ RunLoop.current.run(until: Date().addingTimeInterval(0.3))
+ }
+
+ if agreeAndContinueButton.isHittable {
+ agreeAndContinueButton.tap()
+ } else {
+ agreeAndContinueButton.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
+ }
+ }
+
+ func isYourPortfolioBalanceTextDisplayed(timeout: TimeInterval = 120) -> Bool {
+ if yourPortfolioBalanceText.waitForExistence(timeout: timeout) {
+ return true
+ }
+
+ let labelFallback = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'Portfolio balance info'")
+ ).firstMatch
+ return labelFallback.waitForExistence(timeout: 5)
+ }
+
+ func swipeOnboarding() {
+ onboardingScrollView.swipeRight()
+ }
+
+ func tapGotIt() {
+ if gotItButton.waitForExistence(timeout: 20) {
+ gotItButton.tap()
+ return
+ }
+
+ let gotItLabelFallback = app.staticTexts["GOT IT"]
+ XCTAssertTrue(gotItLabelFallback.waitForExistence(timeout: 5), "Got it button not found")
+ gotItLabelFallback.tap()
+ }
+
+ // MARK: - Private Helpers
+
+ private func waitForCheckbox(_ identifier: String, timeout: TimeInterval) -> Bool {
+ let endTime = Date().addingTimeInterval(timeout)
+ while Date() < endTime {
+ let query = app.descendants(matching: .any).matching(identifier: identifier)
+ if query.firstMatch.exists {
+ return true
+ }
+ RunLoop.current.run(until: Date().addingTimeInterval(0.25))
+ }
+ return false
+ }
+
+ private func tapCheckbox(_ identifier: String) {
+ let query = app.descendants(matching: .any).matching(identifier: identifier)
+ XCTAssertTrue(query.firstMatch.waitForExistence(timeout: 15), "Checkbox not found: \(identifier)")
+
+
+ let box = app.descendants(matching: .any)
+ .matching(NSPredicate(format: "identifier == %@ AND label == %@", identifier, "Checkbox"))
+ .firstMatch
+ let target = box.waitForExistence(timeout: 5) ? box : query.firstMatch
+
+ if target.isHittable {
+ target.tap()
+ } else {
+ // The box only covers the toggle, so its own centre is a safe fallback.
+ target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
+ }
+ }
+
+ private func waitForAgreeAndContinueEnabled(timeout: TimeInterval) -> Bool {
+ guard agreeAndContinueButton.waitForExistence(timeout: timeout) else {
+ return false
+ }
+
+ let endTime = Date().addingTimeInterval(timeout)
+ while Date() < endTime {
+ if agreeAndContinueButton.isEnabled {
+ return true
+ }
+ RunLoop.current.run(until: Date().addingTimeInterval(0.25))
+ }
+ return false
+ }
+
+ private func tapFirstHittableElement(
+ candidates: [XCUIElement],
+ timeout: TimeInterval
+ ) -> Bool {
+ let deadline = Date().addingTimeInterval(timeout)
+ while Date() < deadline {
+ for element in candidates where element.exists {
+ if element.isHittable {
+ element.tap()
+ return true
+ }
+ }
+ RunLoop.current.run(until: Date().addingTimeInterval(0.25))
+ }
+
+ for element in candidates where element.exists {
+ element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
+ return true
+ }
+ return false
+ }
+
+ private func isPostContinueScreenVisible(timeout: TimeInterval) -> Bool {
+ let skipById = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'skip-button'")
+ ).firstMatch
+ let createKey = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'create-a-key-button'")
+ ).firstMatch
+ let alreadyHaveKey = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'i-already-have-a-key-button'")
+ ).firstMatch
+ let portfolio = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'portfolio-balance-info-button'")
+ ).firstMatch
+
+ let deadline = Date().addingTimeInterval(timeout)
+ while Date() < deadline {
+ if skipById.exists || createKey.exists || alreadyHaveKey.exists || portfolio.exists {
+ return true
+ }
+ RunLoop.current.run(until: Date().addingTimeInterval(0.2))
+ }
+ return false
+ }
+}
diff --git a/ios/BitPayAppUITests/pages/PortfolioBalancePage.swift b/ios/BitPayAppUITests/pages/PortfolioBalancePage.swift
new file mode 100644
index 0000000000..1759cd99a2
--- /dev/null
+++ b/ios/BitPayAppUITests/pages/PortfolioBalancePage.swift
@@ -0,0 +1,183 @@
+
+//
+// OnboardingPage.swift
+// BitPayApp
+//
+// Created by vinoth vasu on 16/03/26.
+//
+
+import XCTest
+
+class PortfolioBalancePage {
+
+ let app: XCUIApplication
+
+ init(app: XCUIApplication) {
+ self.app = app
+ }
+
+ // MARK: - Elements
+
+ var yourPortfolioBalanceText: XCUIElement {
+ app.otherElements["portfolio-balance-info-button"].firstMatch
+ }
+
+ var addCryptoButton: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'Add crypto wallet'")
+ ).firstMatch
+ }
+
+ var buyButton: XCUIElement {
+ app.descendants(matching: .any)
+ .matching(identifier: "buy-button")
+ .firstMatch
+ }
+
+ var sellButton: XCUIElement {
+ app.descendants(matching: .any)
+ .matching(identifier: "sell-button")
+ .firstMatch
+ }
+
+ var sendButton: XCUIElement {
+ app.descendants(matching: .any)
+ .matching(identifier: "send-button")
+ .firstMatch
+ }
+
+ var swapButton: XCUIElement {
+ app.descendants(matching: .any)
+ .matching(identifier: "swap-button")
+ .firstMatch
+ }
+
+ // MARK: - Actions
+
+ func isYourPortfolioBalanceTextDisplayed(timeout: TimeInterval = 25) -> Bool {
+ return yourPortfolioBalanceText.waitForExistence(timeout: timeout)
+ }
+
+// func tapBuyButton() {
+// buyButton.tap()
+// }
+
+ func tapBuyButton(timeout: TimeInterval = 900) {
+ XCTAssertTrue(
+ buyButton.waitForExistence(timeout: timeout),
+ "Buy button did not appear within \(timeout) seconds"
+ )
+
+ // Buy can briefly route to a key/backup-required sheet while WALLET.keys
+ // rehydrates after relaunch; retry (dismissing the sheet) until the keypad shows.
+ let keypadKey = app.staticTexts["5"].firstMatch
+ let maybeLater = app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "identifier == 'bottom-notification-secondary-action-button'")
+ ).firstMatch
+
+ for _ in 1...4 {
+ if keypadKey.exists {
+ return
+ }
+
+ if buyButton.exists {
+ buyButton.tap()
+ }
+ if keypadKey.waitForExistence(timeout: 15) {
+ return
+ }
+
+ if maybeLater.exists && maybeLater.isHittable {
+ maybeLater.tap()
+ }
+ RunLoop.current.run(until: Date().addingTimeInterval(1.5))
+ }
+
+ XCTAssertTrue(keypadKey.exists, "Buy screen did not open after tapping Buy")
+ }
+
+// func tapSellButton() {
+// sellButton.tap()
+// }
+
+ func tapSellButton(timeout: TimeInterval = 900) {
+ XCTAssertTrue(
+ sellButton.waitForExistence(timeout: timeout),
+ "Sell button did not appear within \(timeout) seconds"
+ )
+ sellButton.tap()
+ }
+
+// func tapAddCryptoButton() {
+// addCryptoButton.tap()
+// }
+
+ func tapAddCryptoButton() {
+
+// let selectAnOptionPage = AddCryptoOptionPage(app: app)
+//
+// for attempt in 1...5 {
+//
+ addCryptoButton.tap()
+
+// if selectAnOptionPage.isSelectAnOptionTitleDisplayed() {
+// return
+// }
+//
+// if attempt < 5 {
+// sleep(60)
+// }
+// }
+//
+// XCTFail("Failed to open 'Select an Option' screen after 5 attempts")
+ }
+
+ func tapSendButton(timeout: TimeInterval = 900) {
+ XCTAssertTrue(
+ sendButton.waitForExistence(timeout: timeout),
+ "Send button did not appear within \(timeout) seconds"
+ )
+ sendButton.tap()
+ }
+
+// func tapSendButton(timeout: TimeInterval = 900) {
+//
+// let isDisplayed = sendButton.waitForExistence(timeout: timeout)
+//
+// if !isDisplayed {
+//
+// // Attach full app hierarchy/page source into xcresult
+// let pageSourceAttachment = XCTAttachment(
+// string: app.debugDescription
+// )
+//
+// pageSourceAttachment.name = "App Debug Description XML"
+// pageSourceAttachment.lifetime = .keepAlways
+//
+// XCTContext.runActivity(
+// named: "Attach App Debug Description"
+// ) { activity in
+// activity.add(pageSourceAttachment)
+// }
+//
+// XCTFail(
+// "Send button did not appear within \(timeout) seconds"
+// )
+// }
+//
+// sendButton.tap()
+// }
+
+// func tapSwapButton() {
+// swapButton.tap()
+// }
+
+ func tapSwapButton(timeout: TimeInterval = 900) {
+ XCTAssertTrue(
+ swapButton.waitForExistence(timeout: timeout),
+ "Swap button did not appear within \(timeout) seconds"
+ )
+ swapButton.tap()
+ }
+
+}
diff --git a/ios/BitPayAppUITests/pages/SelectCurrencyPage.swift b/ios/BitPayAppUITests/pages/SelectCurrencyPage.swift
new file mode 100644
index 0000000000..9ddbdf8d80
--- /dev/null
+++ b/ios/BitPayAppUITests/pages/SelectCurrencyPage.swift
@@ -0,0 +1,68 @@
+
+//
+// OnboardingPage.swift
+// BitPayApp
+//
+// Created by vinoth vasu on 16/03/26.
+//
+
+import XCTest
+
+class SelectCurrencyPage {
+
+ let app: XCUIApplication
+
+ init(app: XCUIApplication) {
+ self.app = app
+ }
+
+ // MARK: - Elements
+
+ var selectCurrencyTitle: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'Select a Currency'")
+ ).firstMatch
+ }
+
+ var bitcoinCurrency: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label CONTAINS 'Bitcoin, BTC'")
+ ).firstMatch
+ }
+
+ var sendToTitle: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'Send To'")
+ ).firstMatch
+ }
+
+ var recipientAddress: XCUIElement {
+ app.descendants(matching: .any).element(
+ matching: NSPredicate(format: "label == 'Recipient address'")
+ ).firstMatch
+ }
+
+
+ // MARK: - Actions
+
+ func isSelectCurrencyTitleDisplayed(timeout: TimeInterval = 180) -> Bool {
+ return selectCurrencyTitle.waitForExistence(timeout: timeout)
+ }
+
+ func tapBitcoinCurrency() {
+ bitcoinCurrency.tap()
+ }
+
+ func isSendToTitleDisplayed(timeout: TimeInterval = 180) -> Bool {
+ return sendToTitle.waitForExistence(timeout: timeout)
+ }
+
+ func enterRecipientAddress(address: String) {
+ recipientAddress.tap()
+ for character in address {
+ recipientAddress.typeText(String(character))
+ // usleep(100000)
+ }
+ }
+
+}
diff --git a/ios/BitPayAppUITests/pages/SelectKeyToDepositPage.swift b/ios/BitPayAppUITests/pages/SelectKeyToDepositPage.swift
new file mode 100644
index 0000000000..89ed5846f4
--- /dev/null
+++ b/ios/BitPayAppUITests/pages/SelectKeyToDepositPage.swift
@@ -0,0 +1,57 @@
+import XCTest
+
+class SelectKeyToDepositPage {
+
+ let app: XCUIApplication
+
+ init(app: XCUIApplication) {
+ self.app = app
+ }
+
+ // MARK: - Elements
+
+ var selectKeyToDeposit: XCUIElement {
+ app.staticTexts.matching(
+ NSPredicate(format: "label CONTAINS 'Select Key to Deposit to'")
+ ).firstMatch
+ }
+
+ var myKeyWallet: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "label == 'My Key wallet'")
+ ).firstMatch
+ }
+
+ var secondMyKeyWallet: XCUIElement {
+ app.descendants(matching: .any)
+ .matching(NSPredicate(format: "label CONTAINS 'My Key'"))
+ .element(boundBy: 1)
+ }
+
+ var evmAccount: XCUIElement {
+ app.descendants(matching: .any).matching(
+ NSPredicate(format: "label == 'EVM Account'")
+ ).firstMatch
+ }
+
+
+ // MARK: - Validations
+
+ func isSelectKeyToDepositToDisplayed(timeout: TimeInterval = 5) -> Bool {
+ return selectKeyToDeposit.waitForExistence(timeout: timeout)
+ }
+
+ func tapMyKeyWallet() {
+ myKeyWallet.tap()
+ }
+
+ func tapSecondMyKeyWallet() {
+ secondMyKeyWallet.tap()
+ }
+
+ func tapEVMAccount() {
+ evmAccount.tap()
+ }
+
+
+}
diff --git a/ios/BitPayAppUITests/pages/SellPage.swift b/ios/BitPayAppUITests/pages/SellPage.swift
new file mode 100644
index 0000000000..1fb3b1e0d6
--- /dev/null
+++ b/ios/BitPayAppUITests/pages/SellPage.swift
@@ -0,0 +1,35 @@
+import XCTest
+
+class SellPage {
+
+ let app: XCUIApplication
+
+ init(app: XCUIApplication) {
+ self.app = app
+ }
+
+ // MARK: - Elements
+
+ var sellTitle: XCUIElement {
+ app.staticTexts["Sell"].firstMatch
+ }
+
+ var chooseCrypto: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "label == 'Choose Crypto'")
+ ).firstMatch
+ }
+
+
+ // MARK: - Validations
+
+ func isSellPageTitleDisplayed(timeout: TimeInterval = 180) -> Bool {
+ return sellTitle.waitForExistence(timeout: timeout)
+ }
+
+ func tapChooseCrypto() {
+ chooseCrypto.tap()
+ }
+
+
+}
diff --git a/ios/BitPayAppUITests/pages/SwapPage.swift b/ios/BitPayAppUITests/pages/SwapPage.swift
new file mode 100644
index 0000000000..51bab27c4c
--- /dev/null
+++ b/ios/BitPayAppUITests/pages/SwapPage.swift
@@ -0,0 +1,267 @@
+import XCTest
+
+class SwapPage {
+
+ let app: XCUIApplication
+
+ init(app: XCUIApplication) {
+ self.app = app
+ }
+
+ // MARK: - Elements
+
+ var swapTitle: XCUIElement {
+ app.staticTexts["Swap"].firstMatch
+ }
+
+ var selectWalletFrom: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "label == 'Select wallet to swap from'")
+ ).firstMatch
+ }
+
+ var cryptoToSwapPage: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "label == 'Crypto to Swap'")
+ ).firstMatch
+ }
+
+ var bitcoin: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "label CONTAINS 'Bitcoin, BTC'")
+ ).firstMatch
+ }
+
+ var selectWalletTo: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "identifier == 'swap-crypto-to-wallet-selector'")
+ ).firstMatch
+ }
+
+ var swapTo: XCUIElement {
+ app.descendants(matching: .any).matching(
+ NSPredicate(format: "label == 'Swap To'")
+ ).firstMatch
+ }
+
+ var selectCrypto: XCUIElement {
+ app.descendants(matching: .any).matching(
+ NSPredicate(format: "label == 'Select Crypto'")
+ ).firstMatch
+ }
+
+ var searchField: XCUIElement {
+ app.searchFields["Search"].firstMatch
+ }
+
+ var allNetworksFilter: XCUIElement {
+ app.descendants(matching: .any).matching(
+ NSPredicate(format: "label == 'All Networks'")
+ ).firstMatch
+ }
+
+ var ethereum: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "label CONTAINS[c] 'Ethereum'")
+ ).firstMatch
+ }
+
+ var selectKeyToDeposit: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "label == 'Select Key to Deposit to'")
+ ).firstMatch
+ }
+
+ var myKeyWallet: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "label == 'My Key wallet'")
+ ).firstMatch
+ }
+
+ var evmAccount: XCUIElement {
+ app.otherElements.matching(
+ NSPredicate(format: "label == 'EVM Account'")
+ ).firstMatch
+ }
+
+ var swapCrypoButton: XCUIElement {
+ app.descendants(matching: .any).matching(
+ NSPredicate(
+ format: "label == 'Swap crypto toggle fiat display button'"
+ )
+ ).firstMatch
+ }
+
+ var enterAmount: XCUIElement {
+ app.descendants(matching: .any).matching(
+ NSPredicate(
+ format: "label == 'Enter amount'"
+ )
+ ).firstMatch
+ }
+
+ var minAmount: XCUIElement {
+ app.descendants(matching: .any).matching(
+ NSPredicate(
+ format: "label == 'MIN'"
+ )
+ ).firstMatch
+ }
+
+ var changellyTermsCheckbox: XCUIElement {
+ app.descendants(matching: .any).matching(
+ NSPredicate(
+ format: "identifier == 'swap-crypto-changelly-terms-checkbox'"
+ )
+ ).firstMatch
+ }
+
+ var slideToSwapButton: XCUIElement {
+ app.descendants(matching: .any).matching(
+ NSPredicate(
+ format: "label == 'Swap crypto slide to swap button' OR label CONTAINS[c] 'slide to swap' OR identifier == 'swap-crypto-slide-to-swap-button' OR identifier CONTAINS[c] 'slide-to-swap'"
+ )
+ ).firstMatch
+ }
+
+
+ // MARK: - Validations
+
+ func isSwapPageTitleDisplayed(timeout: TimeInterval = 5) -> Bool {
+ return swapTitle.waitForExistence(timeout: timeout)
+ }
+
+ func tapSelectWalletFrom() {
+ selectWalletFrom.tap()
+ }
+
+ func isCryptoToSwapPageDisplayed(timeout: TimeInterval = 5) -> Bool {
+ return cryptoToSwapPage.waitForExistence(timeout: timeout)
+ }
+
+ func isBitcoinOptionDisplayed(timeout: TimeInterval = 10) -> Bool {
+ return bitcoin.waitForExistence(timeout: timeout)
+ }
+
+ func tapBitcoin() {
+ bitcoin.tap()
+ }
+
+ func tapSelectWalletTo() {
+ if selectWalletTo.waitForExistence(timeout: 30) {
+ for _ in 1...8 {
+ if selectWalletTo.isHittable {
+ selectWalletTo.tap()
+ return
+ }
+ RunLoop.current.run(until: Date().addingTimeInterval(0.5))
+ }
+
+ selectWalletTo.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
+ return
+ }
+
+ let selectWalletToByLabel = app.otherElements.matching(
+ NSPredicate(format: "label == 'Select wallet to swap to'")
+ ).firstMatch
+ XCTAssertTrue(selectWalletToByLabel.waitForExistence(timeout: 5), "Swap To selector not found")
+ selectWalletToByLabel.tap()
+ }
+
+ func isSwapToPageDisplayed(timeout: TimeInterval = 5) -> Bool {
+ let endTime = Date().addingTimeInterval(timeout)
+
+ while Date() < endTime {
+ if swapTo.exists || selectCrypto.exists || searchField.exists || allNetworksFilter.exists {
+ return true
+ }
+ RunLoop.current.run(until: Date().addingTimeInterval(0.3))
+ }
+
+ return false
+ }
+
+ func tapEthereum() {
+ if ethereum.waitForExistence(timeout: 20) {
+ if ethereum.isHittable {
+ ethereum.tap()
+ } else {
+ ethereum.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
+ }
+ return
+ }
+
+ if searchField.waitForExistence(timeout: 5), searchField.isHittable {
+ searchField.tap()
+ searchField.typeText("Ethereum")
+ if ethereum.waitForExistence(timeout: 12) {
+ if ethereum.isHittable {
+ ethereum.tap()
+ } else {
+ ethereum.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
+ }
+ return
+ }
+ }
+
+ XCTAssertTrue(false, "Ethereum option not found on Swap To list")
+ }
+
+ func isSelectKeyToDepositToDisplayed(timeout: TimeInterval = 5) -> Bool {
+ return selectKeyToDeposit.waitForExistence(timeout: timeout)
+ }
+
+ func tapMyKeyWallet() {
+ myKeyWallet.tap()
+ }
+
+ func tapSwapCrypoButton() {
+ swapCrypoButton.tap()
+ }
+
+ func tapEnterAmount() {
+ enterAmount.tap()
+ }
+
+ func isChangellyTermsCheckboxDisplayed(timeout: TimeInterval = 5) -> Bool {
+ return changellyTermsCheckbox.waitForExistence(timeout: timeout)
+ }
+
+ func tapChangellyTermsCheckbox() {
+ XCTAssertTrue(
+ changellyTermsCheckbox.waitForExistence(timeout: 20),
+ "Changelly Terms checkbox not found"
+ )
+
+ for _ in 1...4 {
+ if changellyTermsCheckbox.isHittable {
+ changellyTermsCheckbox.tap()
+ return
+ }
+ app.swipeUp()
+ RunLoop.current.run(until: Date().addingTimeInterval(0.4))
+ }
+
+ changellyTermsCheckbox.coordinate(withNormalizedOffset: CGVector(dx: 0.06, dy: 0.5)).tap()
+ }
+
+ func tapMinAmount() {
+ minAmount.tap()
+ }
+
+ func tapCenterOfScreen() {
+ let center = app.coordinate(
+ withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)
+ )
+ center.tap()
+ }
+
+ func isSlideToSwapButtonDisplayed(timeout: TimeInterval = 30) -> Bool {
+ if slideToSwapButton.waitForExistence(timeout: timeout) {
+ return true
+ }
+ app.swipeUp()
+ return slideToSwapButton.waitForExistence(timeout: 10)
+ }
+
+}
diff --git a/ios/BitPayAppUITests/tests/Test591_OnboardingCreateWallet.swift b/ios/BitPayAppUITests/tests/Test591_OnboardingCreateWallet.swift
new file mode 100644
index 0000000000..ccb1f61567
--- /dev/null
+++ b/ios/BitPayAppUITests/tests/Test591_OnboardingCreateWallet.swift
@@ -0,0 +1,96 @@
+import XCTest
+
+final class Test591_OnboardingCreateWallet: XCTestCase {
+
+ var app: XCUIApplication!
+
+ override func setUp() {
+ continueAfterFailure = false
+
+ app = XCUIApplication()
+ app.launch()
+
+ let onboardingPage = OnboardingPage(app: app)
+ onboardingPage.handleTrackingPermissionIfDisplayed()
+ }
+
+ @MainActor
+ func test591_OnboardingCreateWallet() throws {
+
+ AllureXCTestSupport.setDisplayName(
+ "Onboarding Create Wallet (Continue Without Account)"
+ )
+ AllureXCTestSupport.addLabel(
+ "testlioManualTestID",
+ value: "003e6686-1128-4e66-b020-aa03f3b6ec67"
+ )
+ AllureXCTestSupport.addDescription(
+ "Onboarding Create Wallet without creating a new"
+ )
+
+ let onboardingPage = OnboardingPage(app: app)
+
+ AllureXCTestSupport.step(
+ "(4) On the onboarding screen “Seamlessly buy & swap”, tap Continue without an account."
+ ) {
+ onboardingPage.tapContinuewithoutAnAccount()
+ }
+
+ AllureXCTestSupport.step(
+ "(5) On the “Turn on notifications” screen, tap Skip."
+ ) {
+ onboardingPage.skipOnboarding()
+ }
+
+ AllureXCTestSupport.step(
+ "(6) On the “Protect your wallet” screen, tap Skip"
+ ) {
+ onboardingPage.skipOnboarding() //To skip protect wallet
+ }
+
+ AllureXCTestSupport.step(
+ "(7) On the “Create a key or import an existing key” screen, tap Create a Key."
+ ) {
+ onboardingPage.createWallet()
+ }
+
+ AllureXCTestSupport.step(
+ "(8) Wait until wallet creation finishes and the “Would you like to backup your key?” screen is displayed."
+ ) {
+ XCTAssertTrue(
+ onboardingPage.isBackupKeyLabelDisplayed(),
+ "Backup key page did not appear after tapping 'Create a Key'"
+ )
+ }
+
+ AllureXCTestSupport.step("(9) Tap Skip on the backup prompt screen.") {
+ onboardingPage.skipBackup() //To skip backup key
+ }
+
+ AllureXCTestSupport.step(
+ "(10) On the “Are you sure?” confirmation modal, tap Later. "
+ ) {
+ onboardingPage.tapLater()
+ }
+
+ AllureXCTestSupport.step(
+ "(11, 12 & 13) On the “Important” screen, select all the three agreements checkbox. "
+ ) {
+ onboardingPage.acceptTerms()
+ }
+
+ AllureXCTestSupport.step("(14) Tap Agree and Continue. ") {
+ onboardingPage.tapAgreeAndContinueButton()
+ }
+
+ AllureXCTestSupport.step(
+ "(15) Wait until the Home screen finishes loading. "
+ ) {
+ XCTAssertTrue(
+ onboardingPage.isYourPortfolioBalanceTextDisplayed(),
+ "Home Page - Your Portfolio Balance text is not displayed"
+ )
+ }
+ }
+
+}
diff --git a/ios/BitPayAppUITests/tests/Test592_ImportWalletRecoveryPhrase.swift b/ios/BitPayAppUITests/tests/Test592_ImportWalletRecoveryPhrase.swift
new file mode 100644
index 0000000000..bd0d4718b4
--- /dev/null
+++ b/ios/BitPayAppUITests/tests/Test592_ImportWalletRecoveryPhrase.swift
@@ -0,0 +1,85 @@
+import XCTest
+
+final class Test592_ImportWalletRecoveryPhrase: XCTestCase {
+
+ var app: XCUIApplication!
+
+ override func setUp() {
+ continueAfterFailure = false
+
+ app = XCUIApplication()
+ app.launch()
+
+ let onboardingPage = OnboardingPage(app: app)
+ onboardingPage.handleTrackingPermissionIfDisplayed()
+
+ AppFlows.completeOnboardingIfRequired(app: app)
+ }
+
+ @MainActor
+ func test592_ImportWalletRecoveryPhrase() throws {
+
+ AllureXCTestSupport.setDisplayName(
+ "Import Wallet via Recovery Phrase (BTC)"
+ )
+ AllureXCTestSupport.addLabel(
+ "testlioManualTestID",
+ value: "1ca663da-1909-451f-9908-25ba9cdc0396"
+ )
+ AllureXCTestSupport.addDescription(
+ "Import Wallet via Recovery Phrase (BTC)"
+ )
+
+ let portfolioBalancePage = PortfolioBalancePage(app: app)
+ let selectAnOptionPage = AddCryptoOptionPage(app: app)
+ let importRecoveryPhasePage = ImportRecoveryPhrasePage(app: app)
+ let myKeyPage = MyKeyPage(app: app)
+ let walletPhrase = TestConfig.walletPhrase
+
+ AllureXCTestSupport.step(
+ "(1 to 3) On the Home screen, tap the Plus (+) control near the “Your Crypto” section to open wallet/key options."
+ ) {
+ portfolioBalancePage.tapAddCryptoButton()
+ }
+
+ AllureXCTestSupport.step(
+ "(4) Wait until the “Select an Option” screen is displayed."
+ ) {
+ XCTAssertTrue(
+ selectAnOptionPage.isSelectAnOptionTitleDisplayed(),
+ "Select an option page is not displayed"
+ )
+ }
+
+ AllureXCTestSupport.step("(5) Tap Import Key") {
+ selectAnOptionPage.tapImportKey()
+ }
+
+ AllureXCTestSupport.step(
+ "(6 to 7) On the “Import” screen, tap the Recovery Phrase input field & Enter a valid Recovery Phrase. "
+ ) {
+ importRecoveryPhasePage.enterRecoveryPhrase(walletPhrase)
+ }
+
+ AllureXCTestSupport.step("(8) Tap Import Wallet") {
+ importRecoveryPhasePage.tapImportWallet()
+ }
+
+ AllureXCTestSupport.step(
+ "(9) Wait until the import finishes and the My Key screen is displayed (use an extended wait to account for slower imports). "
+ ) {
+ XCTAssertTrue(myKeyPage.isMyKeyDisplayed(), "Is My Key not displayed")
+ XCTAssertTrue(
+ myKeyPage.isMyWalletsDisplayed(),
+ "My Wallets not displayed"
+ )
+
+ XCTAssertTrue(
+ myKeyPage.isBitcoinBTCWalletDisplayed(),
+ "Bitcoin BTC Wallet not displayed"
+ )
+ }
+
+ }
+
+}
diff --git a/ios/BitPayAppUITests/tests/Test593_BTCConfirmPaymentScreen.swift b/ios/BitPayAppUITests/tests/Test593_BTCConfirmPaymentScreen.swift
new file mode 100644
index 0000000000..5ff3bad540
--- /dev/null
+++ b/ios/BitPayAppUITests/tests/Test593_BTCConfirmPaymentScreen.swift
@@ -0,0 +1,85 @@
+import XCTest
+
+final class Test593_BTCConfirmPaymentScreen: XCTestCase {
+
+ var app: XCUIApplication!
+
+ override func setUp() {
+ continueAfterFailure = false
+
+ app = XCUIApplication()
+ app.launch()
+
+ let onboardingPage = OnboardingPage(app: app)
+ onboardingPage.handleTrackingPermissionIfDisplayed()
+
+ AppFlows.completeOnboardingIfRequired(app: app)
+
+ AppFlows.goToHomeScreen(app: app)
+ }
+
+ @MainActor
+ func test593_BTCConfirmPaymentScreen() throws {
+
+ AllureXCTestSupport.setDisplayName(
+ "Basic Send (BTC) - Confirm Payment Screen"
+ )
+ AllureXCTestSupport.addLabel(
+ "testlioManualTestID",
+ value: "be4a044a-2c3a-4cad-aa5c-9398f415b483"
+ )
+ AllureXCTestSupport.addDescription(
+ "Basic Send (BTC) - Confirm Payment Screen"
+ )
+
+ let portfolioBalancePage = PortfolioBalancePage(app: app)
+ let selectCurrencyPage = SelectCurrencyPage(app: app)
+ let enterAmountPage = EnterAmountPage(app: app)
+ let confirmPaymentPage = ConfirmPaymentPage(app: app)
+
+ AllureXCTestSupport.step("(1 to 4) On the Home screen, tap Send in the top menu.") {
+ portfolioBalancePage.tapSendButton()
+ }
+
+ AllureXCTestSupport.step("(5) Wait until the Select a Currency screen is displayed. ") {
+ XCTAssertTrue(
+ selectCurrencyPage.isSelectCurrencyTitleDisplayed(),
+ "Select Currency page not displayed"
+ )
+ }
+
+ AllureXCTestSupport.step("(6) Tap Bitcoin.") {
+ selectCurrencyPage.tapBitcoinCurrency()
+ }
+
+ AllureXCTestSupport.step("(7) Wait until the Send To screen is displayed.") {
+ XCTAssertTrue(
+ selectCurrencyPage.isSendToTitleDisplayed(),
+ "Send To page not displayed"
+ )
+ }
+
+ AllureXCTestSupport.step("(8 & 9) Tap the Search contact or enter address field & Paste the receiver address.") {
+ selectCurrencyPage.enterRecipientAddress(address: "bc1q0effzahtsn685tyjppgukpvfhv37hrlm4g67ws")
+ }
+
+ AllureXCTestSupport.step("(10 & 11) Wait until the Amount screen is displayed & Enter 0.00005 BTC.") {
+ enterAmountPage.enterAmount(amount: "0.00005")
+ }
+
+ AllureXCTestSupport.step("(12) Tap Continue and Verify") {
+ enterAmountPage.tapContinue()
+
+ XCTAssertTrue(
+ confirmPaymentPage.isConfirmPaymentTitleDisplayed(),
+ "Confirm payment page not displayed"
+ )
+
+ XCTAssertTrue(
+ confirmPaymentPage.isSummaryTextDisplayed(),
+ "Confirm payment - Summary text not displayed"
+ )
+
+ }
+ }
+}
diff --git a/ios/BitPayAppUITests/tests/Test594_BasicSwapBTC.swift b/ios/BitPayAppUITests/tests/Test594_BasicSwapBTC.swift
new file mode 100644
index 0000000000..2dd04e4630
--- /dev/null
+++ b/ios/BitPayAppUITests/tests/Test594_BasicSwapBTC.swift
@@ -0,0 +1,101 @@
+import XCTest
+
+final class Test594_BasicSwapBTC: XCTestCase {
+
+ var app: XCUIApplication!
+
+ override func setUp() {
+ continueAfterFailure = false
+
+ app = XCUIApplication()
+ app.launch()
+
+ let onboardingPage = OnboardingPage(app: app)
+ onboardingPage.handleTrackingPermissionIfDisplayed()
+
+ AppFlows.completeOnboardingIfRequired(app: app)
+
+ AppFlows.goToHomeScreen(app: app)
+ }
+
+ @MainActor
+ func test594_BasicSwapBTC() throws {
+
+ AllureXCTestSupport.setDisplayName(
+ "Basic Swap BTC to ETH"
+ )
+ AllureXCTestSupport.addLabel(
+ "testlioManualTestID",
+ value: "947f685f-e106-4708-922f-43419f9f8f50"
+ )
+ AllureXCTestSupport.addDescription(
+ "Basic Swap BTC to ETH (View Offers)"
+ )
+
+ let portfolioBalancePage = PortfolioBalancePage(app: app)
+ let swapPage = SwapPage(app: app)
+ let selectKeyToDepositPage = SelectKeyToDepositPage(app: app)
+ let enterAmountPage = EnterAmountPage(app: app)
+ let selectCurrencyPage = SelectCurrencyPage(app: app)
+
+ AllureXCTestSupport.step("(1 to 4) From the Home screen, tap Swap in the top menu. ") {
+ portfolioBalancePage.tapSwapButton()
+ }
+
+ AllureXCTestSupport.step("(5) Wait until the Swap Crypto screen is displayed.") {
+ XCTAssertTrue(swapPage.isSwapPageTitleDisplayed(), "Swap page not displayed")
+ }
+
+ AllureXCTestSupport.step("(6) In the Swap From section, tap Select Wallet") {
+ swapPage.tapSelectWalletFrom()
+ }
+
+ AllureXCTestSupport.step("(7) On the Crypto to Swap screen, tap Bitcoin (BTC)") {
+ XCTAssertTrue(swapPage.isBitcoinOptionDisplayed(), "Bitcoin option not displayed")
+ swapPage.tapBitcoin()
+ }
+
+ AllureXCTestSupport.step("(8) Back on the Swap Crypto screen, in the Swap To section, tap Select Crypto.") {
+ swapPage.tapSelectWalletTo()
+ }
+
+ AllureXCTestSupport.step("(9) On the Swap To list screen, tap Ethereum (ETH).") {
+ XCTAssertTrue(
+ swapPage.isSwapToPageDisplayed(timeout: 15),
+ "Swap To list not displayed"
+ )
+ swapPage.tapEthereum()
+ }
+
+ AllureXCTestSupport.step("(10) On the Select Key to Deposit to screen, tap My Key (choose the entry that has funds available if multiple are shown).") {
+ XCTAssertTrue(selectKeyToDepositPage.isSelectKeyToDepositToDisplayed(), "Select Key to Deposit to dialog not displayed")
+ selectKeyToDepositPage.tapSecondMyKeyWallet()
+ }
+
+ AllureXCTestSupport.step("(11) On the Swap Crypto screen, tap Enter Amount.") {
+ selectKeyToDepositPage.tapEVMAccount()
+ // swapPage.tapSwapCrypoButton() this tap is not needed
+ }
+
+ AllureXCTestSupport.step("(12) On the Swap Amount bottom sheet, enter 0.0007 BTC.") {
+ swapPage.tapEnterAmount()
+ swapPage.tapMinAmount()
+ // enterAmountPage.enterAmount(amount: "0.0007")
+ }
+
+ AllureXCTestSupport.step("(13) Tap Changelly checkbox.") {
+ // swapPage.tapCenterOfScreen()
+ XCTAssertTrue(swapPage.isChangellyTermsCheckboxDisplayed(), "Changelly Terms and Conditions checkbox not displayed")
+ swapPage.tapChangellyTermsCheckbox()
+ }
+
+ AllureXCTestSupport.step(
+ "(14) Verify Slide to Swap button is displayed."
+ ) {
+ XCTAssertTrue(
+ swapPage.isSlideToSwapButtonDisplayed(),
+ "Slide to Swap button not displayed"
+ )
+ }
+ }
+}
diff --git a/ios/BitPayAppUITests/tests/Test595_BasicSellBTC_.swift b/ios/BitPayAppUITests/tests/Test595_BasicSellBTC_.swift
new file mode 100644
index 0000000000..0a1028a553
--- /dev/null
+++ b/ios/BitPayAppUITests/tests/Test595_BasicSellBTC_.swift
@@ -0,0 +1,66 @@
+import XCTest
+
+final class Test595_BasicSellBTC: XCTestCase {
+
+ var app: XCUIApplication!
+
+ override func setUp() {
+ continueAfterFailure = false
+
+ app = XCUIApplication()
+ app.launch()
+
+ let onboardingPage = OnboardingPage(app: app)
+ onboardingPage.handleTrackingPermissionIfDisplayed()
+
+ AppFlows.completeOnboardingIfRequired(app: app)
+
+ AppFlows.goToHomeScreen(app: app)
+ }
+
+ @MainActor
+ func test595_BasicSellBTC() throws {
+
+ AllureXCTestSupport.setDisplayName(
+ "Basic Sell BTC - Select Bitcoin"
+ )
+ AllureXCTestSupport.addLabel(
+ "testlioManualTestID",
+ value: "dfc4301a-fad9-4d7f-a13a-91ca1f1d54a7"
+ )
+ AllureXCTestSupport.addDescription(
+ "Basic Sell BTC (Select Bitcoin and reach the sell amount flow)"
+ )
+
+ let portfolioBalancePage = PortfolioBalancePage(app: app)
+ let sellPage = SellPage(app: app)
+ let enterAmountPage = EnterAmountPage(app: app)
+ let selectCurrencyPage = SelectCurrencyPage(app: app)
+
+ AllureXCTestSupport.step("(1 to 4) From the Home screen, tap Sell.") {
+ portfolioBalancePage.tapSellButton()
+ }
+
+ AllureXCTestSupport.step("(5) Wait until the Sell screen is displayed.") {
+ XCTAssertTrue(sellPage.isSellPageTitleDisplayed(), "Sell page not displayed")
+ }
+
+ AllureXCTestSupport.step("(6) Enter $30 as the sell amount.") {
+ enterAmountPage.enterAmount(amount: "30")
+ }
+
+ AllureXCTestSupport.step("(7) Tap Choose Crypto.") {
+ sellPage.tapChooseCrypto()
+ }
+
+ AllureXCTestSupport.step("(8) On the Select Crypto screen, tap Bitcoin (BTC). ") {
+ selectCurrencyPage.tapBitcoinCurrency()
+ //tap swap currency
+ }
+
+ AllureXCTestSupport.step("(9) Tap Continue with.") {
+ enterAmountPage.tapContinue()
+ }
+
+ }
+}
diff --git a/ios/BitPayAppUITests/tests/Test596_BasicBuyBTC_.swift b/ios/BitPayAppUITests/tests/Test596_BasicBuyBTC_.swift
new file mode 100644
index 0000000000..abe226d880
--- /dev/null
+++ b/ios/BitPayAppUITests/tests/Test596_BasicBuyBTC_.swift
@@ -0,0 +1,68 @@
+import XCTest
+
+final class Test596_BasicBuyBTC: XCTestCase {
+
+ var app: XCUIApplication!
+
+ override func setUp() {
+ continueAfterFailure = false
+
+ app = XCUIApplication()
+ app.launch()
+
+ let onboardingPage = OnboardingPage(app: app)
+ onboardingPage.handleTrackingPermissionIfDisplayed()
+
+ AppFlows.completeOnboardingIfRequired(app: app)
+
+ AppFlows.goToHomeScreen(app: app)
+ }
+
+ @MainActor
+ func test596_BasicBuyBTC() throws {
+
+ AllureXCTestSupport.setDisplayName(
+ "Basic Buy BTC - Select tap My Key with funds"
+ )
+ AllureXCTestSupport.addLabel(
+ "testlioManualTestID",
+ value: "3c9c0271-76ba-4da3-88ad-8a5d6ea60211"
+ )
+ AllureXCTestSupport.addDescription(
+ "Basic Buy BTC (Select tap My Key with funds)"
+ )
+
+ let portfolioBalancePage = PortfolioBalancePage(app: app)
+ let buyPage = BuyPage(app: app)
+ let selectKeyToDepositPage = SelectKeyToDepositPage(app: app)
+ let enterAmountPage = EnterAmountPage(app: app)
+ let selectCurrencyPage = SelectCurrencyPage(app: app)
+
+ AllureXCTestSupport.step("(1 to 4) From the Home screen, tap Buy.") {
+ portfolioBalancePage.tapBuyButton()
+ }
+
+ AllureXCTestSupport.step("(5) Wait until the Buy screen is displayed. ") {
+ XCTAssertTrue(buyPage.isBuyPageTitleDisplayed(), "Buy page not displayed")
+ }
+
+ AllureXCTestSupport.step("(6) Enter $30 as the buy amount.") {
+ enterAmountPage.tapBackspace(count: 3)
+ enterAmountPage.enterAmount(amount: "30")
+ }
+
+ AllureXCTestSupport.step("(7) Tap Choose Crypto.") {
+ buyPage.tapBitcoin()
+ }
+
+ AllureXCTestSupport.step("(8) On the Select Crypto screen, tap Bitcoin (BTC). ") {
+ selectCurrencyPage.tapBitcoinCurrency()
+ }
+
+ AllureXCTestSupport.step("(9) On the Select Key to Deposit to screen, tap My Key with funds available.") {
+ XCTAssertTrue(selectKeyToDepositPage.isSelectKeyToDepositToDisplayed(), "Select Key to Deposit to dialog not displayed")
+ selectKeyToDepositPage.tapMyKeyWallet()
+ }
+
+ }
+}
diff --git a/package.json b/package.json
index 9fd36f50d8..2a83e983f8 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,10 @@
"reset-allowed-url-prefixes": "node ./scripts/allowed-url-prefixes.js reset",
"reset-braze": "node ./scripts/braze-config-reset.js",
"reset-google-maps": "node ./scripts/google-maps-reset.js",
- "translation:extract": "npx i18next-cli extract",
+ "translation:extract": "yarn run i18next 'src/**/*.{js,jsx,ts,tsx}'",
+ "translation:upload": "crowdin upload sources --identity crowdin-credentials.yml",
+ "translation:download": "crowdin download --identity crowdin-credentials.yml",
+ "set:WalletPhrase": "export NODE_ENV=development && node ./scripts/wallet-phrase.js",
"translation:check": "node --test scripts/check-translations.test.js && node scripts/check-translations.js"
},
"dependencies": {
diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh
new file mode 100644
index 0000000000..b5abc2040e
--- /dev/null
+++ b/scripts/run-tests.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+OVERALL_EXIT=0
+ALLURE_RESULTS_DIR="${ALLURE_RESULTS_DIR:-$PWD/.tmp/allure-results}"
+mkdir -p "$ALLURE_RESULTS_DIR"
+
+for TEST_CLASS in \
+ "Test591OnboardingCreateWallet" \
+ "Test592ImportWalletRecoveryPhrase" \
+ "Test593SendBTC" \
+ "Test594SwapBTC" \
+ "Test595SellBTC" \
+ "Test596BuyBTC"
+do
+ echo "=== Clearing state before $TEST_CLASS ==="
+ adb shell pm clear com.bitpay.wallet || true
+ sleep 2
+
+ echo "=== Running $TEST_CLASS ==="
+ cd android && ./gradlew connectedDebugAndroidTest \
+ -PreactNativeArchitectures=x86_64 \
+ -Pandroid.testInstrumentationRunnerArguments.class="com.bitpay.wallet.tests.$TEST_CLASS" \
+ --no-daemon
+ EXIT=$?
+ cd ..
+
+ [ $EXIT -ne 0 ] && OVERALL_EXIT=$EXIT
+
+ # Pull allure results immediately after each test
+ # so a timeout or crash on a later test doesn't lose earlier results
+ echo "=== Pulling allure results after $TEST_CLASS ==="
+ adb pull /sdcard/googletest/test_outputfiles/allure-results/. "$ALLURE_RESULTS_DIR/" || true
+done
+
+exit $OVERALL_EXIT
\ No newline at end of file
diff --git a/scripts/wallet-phrase.js b/scripts/wallet-phrase.js
new file mode 100644
index 0000000000..84a6fb503b
--- /dev/null
+++ b/scripts/wallet-phrase.js
@@ -0,0 +1,37 @@
+const fs = require('fs');
+const dotenv = require('dotenv');
+
+(() => {
+ if (!['development', 'production'].includes(process.env.NODE_ENV)) {
+ console.log('Please set NODE_ENV ["development" | "production"]');
+ return;
+ }
+
+ const envPath = `${__dirname}/../.env.${process.env.NODE_ENV}`;
+ const result = dotenv.config({path: envPath});
+
+ if (result.error) {
+ throw result.error;
+ }
+
+ if (process.env.WALLET_PHRASE) {
+ const files = [
+ `${__dirname}/../android/app/src/androidTest/java/com/bitpay/wallet/TestConfig.kt`,
+ `${__dirname}/../ios/BitPayAppUITests/TestConfig.swift`,
+ ];
+
+ files.forEach(file => {
+ if (!fs.existsSync(file)) {
+ console.log(`Skipped (not found): ${file}`);
+ return;
+ }
+ const content = fs.readFileSync(file, 'utf-8');
+ fs.writeFileSync(
+ file,
+ content.replace('WALLET_PHRASE_REPLACE_ME', process.env.WALLET_PHRASE),
+ );
+ });
+ }
+
+ console.log(`${process.env.NODE_ENV.toUpperCase()} Wallet phrase successfully updated.`);
+})();
\ No newline at end of file
diff --git a/src/navigation/wallet/screens/send/SendTo.tsx b/src/navigation/wallet/screens/send/SendTo.tsx
index f5ba97ec09..c3abdb1c8b 100644
--- a/src/navigation/wallet/screens/send/SendTo.tsx
+++ b/src/navigation/wallet/screens/send/SendTo.tsx
@@ -155,8 +155,8 @@ const isEmailAddress = (text: string) => {
if (!text.includes('@')) {
return false;
}
-
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text);
+ const reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w\w+)+$/;
+ return reg.test(text);
};
export const BuildKeyAccountRow = (
diff --git a/testlio-cli/project-config-sandbox.json b/testlio-cli/project-config-sandbox.json
new file mode 100644
index 0000000000..1ea5da9269
--- /dev/null
+++ b/testlio-cli/project-config-sandbox.json
@@ -0,0 +1,10 @@
+{
+ "baseURI": "https://api.testlio.com/",
+ "platformURI": "https://app.testlio.com/tmt/project/",
+ "projectId": "3307",
+ "automatedRunCollectionGuid": "2df42fa4-225b-430e-882d-e5fc3ec31de7",
+ "testRunCollectionGuid": "a77e695c-5061-4451-98fe-4f93b8a26d9b",
+ "resultCollectionGuid": "3688212d-e132-40a3-a4bd-aae8b664a2d8",
+ "workspaceName": "bitpay---sandbox"
+}
+
diff --git a/testlio-cli/project-config.json b/testlio-cli/project-config.json
new file mode 100644
index 0000000000..9336b93825
--- /dev/null
+++ b/testlio-cli/project-config.json
@@ -0,0 +1,10 @@
+{
+ "baseURI": "https://api.testlio.com/",
+ "platformURI": "https://app.testlio.com/tmt/project/",
+ "projectId": "2975",
+ "automatedRunCollectionGuid": "8d2e31a1-017d-4e11-9674-e63b4bf223f4",
+ "testRunCollectionGuid": "8eace78b-2c39-4cdb-935d-7e51bd4d6065",
+ "resultCollectionGuid": "a04d061a-88ad-4749-853e-90e227d5eb2a",
+ "workspaceName": "bitpay"
+}
+
diff --git a/testlio-cli/test-config.json b/testlio-cli/test-config.json
new file mode 100644
index 0000000000..2986f99020
--- /dev/null
+++ b/testlio-cli/test-config.json
@@ -0,0 +1,4 @@
+{
+ "automatedTestNamePrefix": "Automation Run: Smoke Test"
+}
+