diff --git a/.github/actions/ci-setup-android-files/action.yml b/.github/actions/ci-setup-android-files/action.yml new file mode 100644 index 000000000..1ddcdf980 --- /dev/null +++ b/.github/actions/ci-setup-android-files/action.yml @@ -0,0 +1,58 @@ +name: 'Setup Android Files' +description: 'Setup required files for building an Android project' + +inputs: + root-path: + description: 'Path of the react-native project' + required: true + google-services-file: + description: 'Google Services Json file content, base64 encoded' + required: false + secrets-file: + description: 'Secrets properties file content' + required: true + keystore: + description: 'Keystore file content, base64 encoded' + required: true + keystore-name: + description: 'Keystore file name' + required: true + sentry-file: + description: 'Sentry file content' + required: true + project-id: + description: 'Reown Cloud ID' + required: true + relay-url: + description: 'Relay URL' + required: true + sentry-dsn: + description: 'Sentry DSN' + required: true + release-type: + description: 'Release type of the project (debug/internal/production)' + default: 'debug' + +runs: + using: "composite" + steps: + - name: Create Google Services Json File + if: ${{ inputs.google-services-file != '' }} + shell: bash + run: echo "${{ inputs.google-services-file }}" | base64 --decode > ${{ inputs.root-path }}/android/app/google-services.json + + - name: Create secrets.properties File + shell: bash + run: echo "${{ inputs.secrets-file }}" > ${{ inputs.root-path }}/android/secrets.properties + + - name: Decode Keystore + shell: bash + run: echo "${{ inputs.keystore }}" | base64 --decode > ${{ inputs.root-path }}/android/app/${{ inputs.keystore-name }}.keystore + + - name: Create Sentry File + shell: bash + run: echo "${{ inputs.sentry-file }}" > ${{ inputs.root-path }}/android/sentry.properties + + - name: Create Env File + shell: bash + run: echo -e "ENV_PROJECT_ID=${{ inputs.project-id }}\nENV_RELAY_URL=${{ inputs.relay-url }}\nENV_SENTRY_DSN=${{ inputs.sentry-dsn }}\nENV_SENTRY_TAG=${{ inputs.release-type }}" >> ${{ inputs.root-path }}/.env.${{ inputs.release-type }} \ No newline at end of file diff --git a/.github/actions/ci-setup-ios-files/action.yml b/.github/actions/ci-setup-ios-files/action.yml new file mode 100644 index 000000000..c3007b92e --- /dev/null +++ b/.github/actions/ci-setup-ios-files/action.yml @@ -0,0 +1,41 @@ +name: 'Setup iOS Files' +description: 'Setup required files for building an iOS project' + +inputs: + root-path: + description: 'Path of the react-native project' + required: true + google-services-file: + description: 'Google Services Plist file content, base64 encoded' + required: false + sentry-file: + description: 'Sentry file content' + required: true + project-id: + description: 'WalletConnect Cloud ID' + required: true + relay-url: + description: 'WalletConnect Relay URL' + required: false + sentry-dsn: + description: 'Sentry DSN' + required: true + release-type: + description: 'Release type of the project (debug/internal/production)' + default: 'debug' + +runs: + using: "composite" + steps: + - name: Create .env file + shell: bash + run: echo -e "ENV_PROJECT_ID=${{ inputs.project-id }}\nENV_RELAY_URL=${{ inputs.relay-url }}\nENV_SENTRY_DSN=${{ inputs.sentry-dsn }}\nENV_SENTRY_TAG=${{ inputs.release-type }}" >> ${{ inputs.root-path }}/.env.${{ inputs.release-type }} + + - name: Create sentry.properties file + shell: bash + run: echo "${{ inputs.sentry-file }}" > ${{ inputs.root-path }}/ios/sentry.properties + + - name: Create GoogleService-Info.plist file + if: ${{ inputs.google-services-file != '' }} + shell: bash + run: echo "${{ inputs.google-services-file }}" | base64 --decode > ${{ inputs.root-path }}/ios/GoogleService-Info.plist \ No newline at end of file diff --git a/.github/workflows/ci_e2e_relay.yaml b/.github/workflows/ci_e2e_relay.yaml new file mode 100644 index 000000000..e17ddf590 --- /dev/null +++ b/.github/workflows/ci_e2e_relay.yaml @@ -0,0 +1,382 @@ +name: CI E2E Relay +# Cross-app WalletConnect *relay* E2E (connect / sign / switch-chain / disconnect / +# restore): the native rn_cli_wallet (app under test) drives the appkit-wagmi WEB +# dapp (served from source in the device browser) via Maestro, one device, one +# flow. Separate from the WC Pay E2E (ci_e2e_walletkit.yaml). See +# .context/RELAY_E2E_PLAN.md. +# +# Both apps are built/served from the PR's source: the wallet natively (simulator/ +# emulator) and the dapp as a static web export served on the runner. Flows live +# in .maestro/relay/ (tag: relay-walletkit). Wallet build steps mirror the proven +# walletkit-build-and-maestro composite (kept self-contained, no pay baggage); +# the dapp web export mirrors that composite's web leg. + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + schedule: + - cron: '0 9 * * *' # Daily canary. + pull_request: + branches: [main] + types: [opened, synchronize, reopened, ready_for_review] + paths: + - 'wallets/rn_cli_wallet/**' + - 'dapps/appkit-wagmi/**' + - '.maestro/relay/**' + - '.github/workflows/ci_e2e_relay.yaml' + workflow_dispatch: + inputs: + platform: + description: 'Platform to run' + required: false + default: ios + type: choice + options: [ios, android] + +env: + APP_ID: com.walletconnect.web3wallet.rnsample.internal + WALLET_ROOT: wallets/rn_cli_wallet + DAPP_ROOT: dapps/appkit-wagmi + +jobs: + e2e-relay-walletkit-ios: + # macOS is expensive but it's the validated platform (the Home->browser resume + # is proven on the iOS simulator). Runs on PR / daily cron / dispatch. + if: >- + (github.event_name == 'schedule') + || (github.event_name == 'workflow_dispatch' && inputs.platform == 'ios') + || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) + name: E2E Relay WalletKit - iOS + runs-on: macos-latest-xlarge + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + # --- Wallet (native, app under test) --- + - name: Install wallet dependencies + working-directory: ${{ env.WALLET_ROOT }} + run: yarn install --immutable + + - name: Write wallet .env + working-directory: ${{ env.WALLET_ROOT }} + env: + WALLET_ENV_FILE: ${{ secrets.WALLETKIT_ENV_FILE }} + WALLET_PRIVATE_KEY: ${{ secrets.TEST_WALLET_PRIVATE_KEY }} + run: | + set -euo pipefail + printf '%s\n' "$WALLET_ENV_FILE" > .env + { + echo "EXPO_PUBLIC_TEST_PRIVATE_KEY=$WALLET_PRIVATE_KEY" + echo "EXPO_PUBLIC_TEST_MODE=true" + echo "SENTRY_DISABLE_AUTO_UPLOAD=true" + } >> .env + + - name: Select Xcode 26.2 + run: | + sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer + xcodebuild -version + + - name: Install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3.0 + bundler-cache: true + working-directory: ${{ env.WALLET_ROOT }} + + - name: Expo Prebuild (iOS) + working-directory: ${{ env.WALLET_ROOT }} + run: APP_VARIANT=internal npx expo prebuild --platform ios --no-install + + - name: Set Xcode paths + run: | + XCODEPROJ=$(find "$WALLET_ROOT/ios" -maxdepth 1 -name "*.xcodeproj" -print -quit) + echo "XCODE_PROJECT_PATH=$XCODEPROJ" >> "$GITHUB_ENV" + echo "XCWORKSPACE_PATH=$WALLET_ROOT/ios/ReactNWallet.xcworkspace" >> "$GITHUB_ENV" + + - name: Cache Pods + id: pods-cache + uses: actions/cache@v4 + with: + path: ${{ env.WALLET_ROOT }}/ios/Pods + key: ${{ runner.os }}-pods-relay-${{ hashFiles(format('{0}/ios/Podfile.lock', env.WALLET_ROOT)) }} + + - name: Build wallet for Simulator + uses: maierj/fastlane-action@v3.0.0 + with: + lane: build_for_simulator + env: + SCHEME: ReactNWallet + BUNDLE_ID: com.walletconnect.web3wallet.rnsample.internal + PODFILE_PATH: ${{ env.WALLET_ROOT }}/ios/Podfile + XCWORKSPACE_PATH: ${{ env.XCWORKSPACE_PATH }} + XCODE_PROJECT_PATH: ${{ env.XCODE_PROJECT_PATH }} + PROJECT_TYPE: wallet + RELEASE_TYPE: internal + BUILD_FOR_SIMULATOR: true + PODS_CACHE_HIT: ${{ steps.pods-cache.outputs.cache-hit }} + SENTRY_DISABLE_AUTO_UPLOAD: true + + - name: Find .app path + run: echo "APP_PATH=$(cat build/app_path.txt)" >> "$GITHUB_ENV" + + - name: Boot iOS Simulator + run: | + DEVICE_TYPE=$(xcrun simctl list devicetypes -j | \ + jq -r '.devicetypes[] | select(.productFamily == "iPhone") | .identifier' | tail -1) + DEVICE_ID=$(xcrun simctl create "E2E-iPhone" "$DEVICE_TYPE" 2>/dev/null || \ + xcrun simctl list devices available -j | \ + jq -r '.devices | to_entries[] | select(.key | contains("iOS")) | .value[] | select(.name | startswith("iPhone")) | .udid' | head -1) + xcrun simctl boot "$DEVICE_ID" || true + xcrun simctl bootstatus "$DEVICE_ID" -b + xcrun simctl spawn "$DEVICE_ID" defaults write com.apple.UIKit UIAnimationDragCoefficient 0 || true + echo "DEVICE_ID=$DEVICE_ID" >> "$GITHUB_ENV" + + - name: Install app on Simulator + run: xcrun simctl install "$DEVICE_ID" "$APP_PATH" + + # --- Dapp (web, counterparty) — static export served on the runner --- + - name: Install dapp dependencies + working-directory: ${{ env.DAPP_ROOT }} + run: yarn install --immutable + + - name: Write dapp .env + working-directory: ${{ env.DAPP_ROOT }} + env: + APPKIT_ENV_FILE: ${{ secrets.APPKIT_ENV_FILE }} + run: | + set -euo pipefail + printf '%s\n' "$APPKIT_ENV_FILE" > .env + { + echo "EXPO_PUBLIC_TEST_MODE=true" + echo "SENTRY_DISABLE_AUTO_UPLOAD=true" + } >> .env + + - name: Export + serve dapp (web) + working-directory: ${{ env.DAPP_ROOT }} + run: | + set -euo pipefail + npx expo export --platform web --output-dir dist + # `serve -s` does SPA fallback; background and wait until ready. The iOS + # simulator reaches the runner's localhost directly. + npx --yes serve -s dist -l 8081 > "${RUNNER_TEMP}/dapp-serve.log" 2>&1 & + for i in $(seq 1 60); do + curl -fsS -o /dev/null http://localhost:8081 && { echo "dapp up"; break; } + sleep 1 + done + curl -fsS -o /dev/null http://localhost:8081 || { echo "dapp serve failed:"; cat "${RUNNER_TEMP}/dapp-serve.log"; exit 1; } + + - name: Install Maestro + uses: WalletConnect/actions/maestro/setup@4be7c0112f9651fc63b18c137650b75d2fc9b200 + + - name: Run Maestro relay tests (iOS) + id: maestro + run: | + xcrun simctl launch "$DEVICE_ID" "$APP_ID" || true + mkdir -p maestro-artifacts + "$HOME/.maestro/bin/maestro" test \ + --env APP_ID="$APP_ID" \ + --env DAPP_URL="http://localhost:8081" \ + --include-tags relay-walletkit \ + --test-output-dir maestro-artifacts \ + --debug-output "${RUNNER_TEMP}/maestro-debug" \ + .maestro/relay + + - name: Upload Maestro artifacts (iOS) + if: ${{ !cancelled() }} + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: maestro-relay-walletkit-ios-artifacts + path: | + maestro-artifacts/** + ${{ runner.temp }}/dapp-serve.log + if-no-files-found: warn + retention-days: 14 + + e2e-relay-walletkit-android: + # UNVERIFIED on Android: the browser is Chrome (not Safari) and the emulator + # reaches the runner via 10.0.2.2 — the _focus_browser return step and DAPP_URL + # need validating on the emulator. Dispatch-only so it can't red-flag PRs until + # proven; promote to PR/schedule once green. + if: github.event_name == 'workflow_dispatch' && inputs.platform == 'android' + name: E2E Relay WalletKit - Android (unverified) + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install wallet dependencies + working-directory: ${{ env.WALLET_ROOT }} + run: yarn install --immutable + + - name: Write wallet .env + working-directory: ${{ env.WALLET_ROOT }} + env: + WALLET_ENV_FILE: ${{ secrets.WALLETKIT_ENV_FILE }} + WALLET_PRIVATE_KEY: ${{ secrets.TEST_WALLET_PRIVATE_KEY }} + run: | + set -euo pipefail + printf '%s\n' "$WALLET_ENV_FILE" > .env + { + echo "EXPO_PUBLIC_TEST_PRIVATE_KEY=$WALLET_PRIVATE_KEY" + echo "EXPO_PUBLIC_TEST_MODE=true" + echo "SENTRY_DISABLE_AUTO_UPLOAD=true" + } >> .env + + - name: Install Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + architecture: x86_64 + + - name: Accept Android Licenses + uses: SimonMarquis/android-accept-licenses@8d8deab5b7ab2aaea9ed69f0b802e53173f21fd1 # v1 + + - name: Install Android NDK 27.0.12077973 + run: | + set +o pipefail + yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install "ndk;27.0.12077973" + + - name: Expo Prebuild (Android) + working-directory: ${{ env.WALLET_ROOT }} + run: npx expo prebuild --platform android --no-install + + - name: Add Secrets file + working-directory: ${{ env.WALLET_ROOT }} + env: + ANDROID_SECRETS_FILE: ${{ secrets.ANDROID_SECRETS_FILE }} + run: printf '%s\n' "$ANDROID_SECRETS_FILE" > android/secrets.properties + + - name: Add Keystore file + working-directory: ${{ env.WALLET_ROOT }} + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.WC_INTERNAL_KEYSTORE }} + ANDROID_KEYSTORE_NAME: ${{ vars.WC_INTERNAL_KEYSTORE_NAME }} + run: printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode >> "android/app/${ANDROID_KEYSTORE_NAME}.keystore" + + - name: Optimize Gradle for CI + working-directory: ${{ env.WALLET_ROOT }} + run: | + { + echo "org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m" + echo "org.gradle.parallel=true" + echo "org.gradle.caching=true" + echo "reactNativeArchitectures=x86_64" + } >> android/gradle.properties + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ${{ env.WALLET_ROOT }}/android/.gradle + ${{ env.WALLET_ROOT }}/android/app/.cxx + ${{ env.WALLET_ROOT }}/android/app/build/generated + key: ${{ runner.os }}-gradle-relay-${{ hashFiles(format('{0}/android/**/*.gradle*', env.WALLET_ROOT), format('{0}/android/**/gradle-wrapper.properties', env.WALLET_ROOT)) }} + restore-keys: | + ${{ runner.os }}-gradle-relay- + + - name: Build wallet APK + working-directory: ${{ env.WALLET_ROOT }} + run: | + export SENTRY_DISABLE_AUTO_UPLOAD=true + yarn run android:build:internal + + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android/sdk/ndk /opt/ghc /usr/local/share/boost + sudo apt-get clean + df -h + + - name: Enable KVM + 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 + + # --- Dapp (web, counterparty) — served on the runner; emulator uses 10.0.2.2 --- + - name: Install dapp dependencies + working-directory: ${{ env.DAPP_ROOT }} + run: yarn install --immutable + + - name: Write dapp .env + working-directory: ${{ env.DAPP_ROOT }} + env: + APPKIT_ENV_FILE: ${{ secrets.APPKIT_ENV_FILE }} + run: | + set -euo pipefail + printf '%s\n' "$APPKIT_ENV_FILE" > .env + { + echo "EXPO_PUBLIC_TEST_MODE=true" + echo "SENTRY_DISABLE_AUTO_UPLOAD=true" + } >> .env + + - name: Export + serve dapp (web) + working-directory: ${{ env.DAPP_ROOT }} + run: | + set -euo pipefail + npx expo export --platform web --output-dir dist + npx --yes serve -s dist -l 8081 > "${RUNNER_TEMP}/dapp-serve.log" 2>&1 & + for i in $(seq 1 60); do + curl -fsS -o /dev/null http://localhost:8081 && { echo "dapp up"; break; } + sleep 1 + done + curl -fsS -o /dev/null http://localhost:8081 || { echo "dapp serve failed:"; cat "${RUNNER_TEMP}/dapp-serve.log"; exit 1; } + + - name: Install Maestro + uses: WalletConnect/actions/maestro/setup@4be7c0112f9651fc63b18c137650b75d2fc9b200 + + - name: Run Maestro relay tests (Android) + id: maestro + uses: reactivecircus/android-emulator-runner@70f4dee990796918b78d040e3278474bdbd348a7 # v2 + env: + APK_PATH: ${{ env.WALLET_ROOT }}/android/app/build/outputs/apk/internal/app-internal.apk + with: + api-level: 34 + arch: x86_64 + target: default + avd-name: test_device + ram-size: 4096M + disk-size: 4096M + heap-size: 576M + emulator-boot-timeout: 900 + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -no-snapshot-load + disable-animations: true + # `adb reverse` maps the emulator's localhost:8081 -> the runner's dapp + # server. DAPP_URL must be http://localhost:8081 (NOT 10.0.2.2): Chrome + # only allows navigator.clipboard.writeText (the dapp's Copy link) from a + # secure context, and localhost qualifies while 10.0.2.2 does not. + script: | + adb install "$APK_PATH"; adb reverse tcp:8081 tcp:8081; mkdir -p maestro-artifacts; "$HOME/.maestro/bin/maestro" test --env APP_ID="$APP_ID" --env DAPP_URL="http://localhost:8081" --include-tags relay-walletkit --test-output-dir maestro-artifacts --debug-output "${RUNNER_TEMP}/maestro-debug" .maestro/relay + + - name: Upload Maestro artifacts (Android) + if: ${{ !cancelled() }} + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: maestro-relay-walletkit-android-artifacts + path: | + maestro-artifacts/** + ${{ runner.temp }}/dapp-serve.log + if-no-files-found: warn + retention-days: 14 diff --git a/.github/workflows/ci_e2e_tests_android.yaml b/.github/workflows/ci_e2e_tests_android.yaml index 80473c7ef..48f03911f 100644 --- a/.github/workflows/ci_e2e_tests_android.yaml +++ b/.github/workflows/ci_e2e_tests_android.yaml @@ -12,4 +12,4 @@ jobs: runs-on: ubuntu-latest steps: - name: Do nothing - run: echo "This is a placeholder workflow to enable manual triggers from feature branches." \ No newline at end of file + run: echo "This is a placeholder workflow to enable manual triggers from feature branches." diff --git a/.github/workflows/ci_e2e_tests_ios.yaml b/.github/workflows/ci_e2e_tests_ios.yaml index 92d105670..cd75b8ed1 100644 --- a/.github/workflows/ci_e2e_tests_ios.yaml +++ b/.github/workflows/ci_e2e_tests_ios.yaml @@ -12,4 +12,4 @@ jobs: runs-on: ubuntu-latest steps: - name: Do nothing - run: echo "This is a placeholder workflow to enable manual triggers from feature branches." \ No newline at end of file + run: echo "This is a placeholder workflow to enable manual triggers from feature branches." diff --git a/.maestro/native/connect_confirm.yaml b/.maestro/native/connect_confirm.yaml index 5bf45da40..a3d3b0286 100644 --- a/.maestro/native/connect_confirm.yaml +++ b/.maestro/native/connect_confirm.yaml @@ -6,8 +6,24 @@ name: React Native Dapp to React Native Wallet Connection Confirmed permissions: all: allow - startRecording: "Native Connection Confirmed" -- tapOn: "Connect" -- tapOn: "React Native Wallet" -- tapOn: "Approve" +- tapOn: + id: "connect-button" +- tapOn: + id: "all-wallets" +- waitForAnimationToEnd +- tapOn: "React" +- runFlow: + when: + visible: "Open" + platform: iOS + commands: + - tapOn: "Open" +- extendedWaitUntil: + visible: + id: "approve-button" + timeout: 10000 +- tapOn: + id: "approve-button" +- waitForAnimationToEnd - assertVisible: "Sign message" - stopRecording \ No newline at end of file diff --git a/.maestro/native/connect_reject.yaml b/.maestro/native/connect_reject.yaml index 2015895ff..2e3e921ff 100644 --- a/.maestro/native/connect_reject.yaml +++ b/.maestro/native/connect_reject.yaml @@ -1,11 +1,29 @@ appId: com.walletconnect.web3modal.rnsample.internal -name: React Native Dapp to React Native Wallet Connection Confirmed +name: React Native Dapp to React Native Wallet Connection Rejected --- - clearState - launchApp: permissions: all: allow -- tapOn: "Connect" -- tapOn: "React Native Wallet" -- tapOn: "Reject" -- assertVisible: "Connection declined" \ No newline at end of file +- startRecording: "Native to Native Connection Rejected" +- tapOn: + id: "connect-button" +- tapOn: + id: "all-wallets" +- waitForAnimationToEnd +- tapOn: "React" +- runFlow: + when: + visible: "Open" + platform: iOS + commands: + - tapOn: "Open" +- extendedWaitUntil: + visible: + id: "reject-button" + timeout: 10000 +- tapOn: + id: "reject-button" +- waitForAnimationToEnd +- assertVisible: "Connection declined" +- stopRecording \ No newline at end of file diff --git a/.maestro/native/personal_sign_confirm.yaml b/.maestro/native/personal_sign_confirm.yaml index 31ec415eb..48ca8dd36 100644 --- a/.maestro/native/personal_sign_confirm.yaml +++ b/.maestro/native/personal_sign_confirm.yaml @@ -5,7 +5,21 @@ name: React Native Dapp to React Native Wallet Personal Sign Confirmed permissions: all: allow - startRecording: "Native personal_sign Confirmed" +- extendedWaitUntil: + visible: "Sign message" + timeout: 10000 - tapOn: "Sign message" -- tapOn: "Approve" +- runFlow: + when: + visible: "Open" + platform: iOS + commands: + - tapOn: "Open" +- extendedWaitUntil: + visible: + id: "approve-button" + timeout: 10000 +- tapOn: + id: "approve-button" - assertVisible: "Request response" - stopRecording \ No newline at end of file diff --git a/.maestro/native/personal_sign_reject.yaml b/.maestro/native/personal_sign_reject.yaml index c102a41f5..5ccd5d517 100644 --- a/.maestro/native/personal_sign_reject.yaml +++ b/.maestro/native/personal_sign_reject.yaml @@ -5,7 +5,21 @@ name: React Native Dapp to React Native Wallet Personal Sign Rejected permissions: all: allow - startRecording: "Native personal_sign Rejected" +- extendedWaitUntil: + visible: "Sign message" + timeout: 10000 - tapOn: "Sign message" -- tapOn: "Reject" +- runFlow: + when: + visible: "Open" + platform: iOS + commands: + - tapOn: "Open" +- extendedWaitUntil: + visible: + id: "reject-button" + timeout: 10000 +- tapOn: + id: "reject-button" - assertVisible: "Request failure" - stopRecording \ No newline at end of file diff --git a/.maestro/relay/_connect_walletkit.yaml b/.maestro/relay/_connect_walletkit.yaml new file mode 100644 index 000000000..72986bf25 --- /dev/null +++ b/.maestro/relay/_connect_walletkit.yaml @@ -0,0 +1,56 @@ +# Reusable connect handshake — NOT a standalone test. +# Untagged on purpose: CI runs `--include-tags relay-walletkit`, so tag filtering +# never picks this up directly; the tagged flows pull it in via `runFlow`. +# +# Native wallet (app under test) <-> WEB appkit dapp (counterparty, in the device +# browser). The dapp opens the QR view and copies the wc: URI (Web Clipboard API); +# the native wallet pastes it and approves. To return to the dapp we RESUME Safari +# (Home + app icon) rather than relaunching it (`launchApp`/`openLink` reload the +# tab and drop the pending connection) — a resumed tab keeps its live WC client, +# which fetches the queued session settlement and completes the connection. +# +# Env: +# APP_ID native wallet bundle id (the app under test) +# DAPP_URL appkit-wagmi WEB dapp base URL (served locally or on Vercel) +appId: ${APP_ID} +--- +# Clean wallet so sessions don't accumulate across runs (the disconnect flows +# need to reach an empty list). The OS clipboard survives clearState. +- launchApp: + appId: ${APP_ID} + clearState: true + permissions: + all: allow +# --- WEB dapp: open the QR view and copy the wc: URI --- +- openLink: + link: ${DAPP_URL} + browser: true +- extendedWaitUntil: + visible: 'AppKit + Multichain' + timeout: 60000 +- tapOn: 'Open QR (E2E)' +- extendedWaitUntil: + visible: 'Copy link' + timeout: 30000 +- tapOn: 'Copy link' +# --- Native wallet: pair by pasting the copied wc: URI --- +- launchApp: + appId: ${APP_ID} +- tapOn: + id: 'button-scan' +- tapOn: + id: 'button-paste-url' +# iOS shows a one-time "… would like to paste" system dialog. +- runFlow: + when: + visible: 'Allow Paste' + commands: + - tapOn: 'Allow Paste' +- extendedWaitUntil: + visible: + id: 'approve-button' + timeout: 60000 +- tapOn: + id: 'approve-button' +# --- Return to the dapp by RESUMING the browser (Home + icon), not relaunching --- +- runFlow: _focus_browser.yaml diff --git a/.maestro/relay/_focus_browser.yaml b/.maestro/relay/_focus_browser.yaml new file mode 100644 index 000000000..265f88940 --- /dev/null +++ b/.maestro/relay/_focus_browser.yaml @@ -0,0 +1,28 @@ +# Resume the device browser tab (NOT relaunch) so the dapp's live WalletConnect +# client receives the queued session settlement / response. Relaunching +# (openLink/launchApp) reloads the tab and drops the pending in-memory state. +# +# iOS: Home, then tap the Safari icon (resumes the app + tab). +# Android: Home, then tap the Chrome icon (resumes Chrome + tab). Back isn't +# reliable here — the wallet's own navigation consumes it instead of +# returning to the browser. +appId: ${APP_ID} +--- +- runFlow: + when: + platform: iOS + commands: + - pressKey: Home + - extendedWaitUntil: + visible: 'Safari' + timeout: 10000 + - tapOn: 'Safari' +- runFlow: + when: + platform: Android + commands: + - pressKey: Home + - extendedWaitUntil: + visible: 'Chrome' + timeout: 10000 + - tapOn: 'Chrome' diff --git a/.maestro/relay/config.yaml b/.maestro/relay/config.yaml new file mode 100644 index 000000000..140d6f6b1 --- /dev/null +++ b/.maestro/relay/config.yaml @@ -0,0 +1,14 @@ +# Maestro config for the relay (dapp <-> wallet) E2E suite. +# +# maestro test --include-tags relay-walletkit .maestro/relay +# native rn_cli_wallet (app under test) <-> WEB appkit-wagmi dapp (device browser). +# +# Tag filtering keeps the untagged `_*.yaml` subflows (_connect_walletkit, +# _focus_browser) from running as standalone tests; they're pulled in via runFlow. +# +# Suite is intentionally separate from the WC Pay suite (shared flows copied in +# by ci_e2e_walletkit.yaml) and from the legacy .maestro/native + .maestro/web +# flows. +baselineBranch: main +flows: + - '*.yaml' diff --git a/.maestro/relay/connect_walletkit.yaml b/.maestro/relay/connect_walletkit.yaml new file mode 100644 index 000000000..7f7365d61 --- /dev/null +++ b/.maestro/relay/connect_walletkit.yaml @@ -0,0 +1,12 @@ +# Flow 1 — connect: dapp initiates, wallet approves, session established. +appId: ${APP_ID} +tags: + - relay-walletkit + - connect +--- +- runFlow: _connect_walletkit.yaml +# Web dapp reflects the connected account (matched by text — Safari webview +# doesn't expose testIDs). +- extendedWaitUntil: + visible: 'Connected to' + timeout: 60000 diff --git a/.maestro/relay/disconnect_dapp_walletkit.yaml b/.maestro/relay/disconnect_dapp_walletkit.yaml new file mode 100644 index 000000000..ff91a55ad --- /dev/null +++ b/.maestro/relay/disconnect_dapp_walletkit.yaml @@ -0,0 +1,24 @@ +# Flow 5 — disconnect from the dapp side: dapp tears down the session; the +# wallet receives session_delete and drops it. Uses the dapp's TEST_MODE +# "Disconnect (E2E)" button (the AppKit account button isn't text-matchable on web). +appId: ${APP_ID} +tags: + - relay-walletkit + - disconnect-dapp +--- +- runFlow: _connect_walletkit.yaml +- extendedWaitUntil: + visible: 'Connected to' + timeout: 60000 +# Disconnect from the dapp. +- tapOn: 'Disconnect (E2E)' +- extendedWaitUntil: + notVisible: 'Connected to' + timeout: 30000 +# Wallet side dropped the session too (session_delete over the relay). +- launchApp: + appId: ${APP_ID} +- tapOn: 'Connected apps' +- extendedWaitUntil: + visible: 'No connected apps yet' + timeout: 30000 diff --git a/.maestro/relay/disconnect_wallet_walletkit.yaml b/.maestro/relay/disconnect_wallet_walletkit.yaml new file mode 100644 index 000000000..18fa9dd0f --- /dev/null +++ b/.maestro/relay/disconnect_wallet_walletkit.yaml @@ -0,0 +1,24 @@ +# Flow 6 — disconnect from the wallet side: wallet tears down the session via +# the session detail modal. +appId: ${APP_ID} +tags: + - relay-walletkit + - disconnect-wallet +--- +- runFlow: _connect_walletkit.yaml +# Foreground the wallet and open the connected app's detail. +- launchApp: + appId: ${APP_ID} +- tapOn: 'Connected apps' +- tapOn: + id: 'session-item' +- extendedWaitUntil: + visible: + id: 'session-disconnect-button' + timeout: 30000 +- tapOn: + id: 'session-disconnect-button' +# Wallet's session list is now empty. +- extendedWaitUntil: + visible: 'No connected apps yet' + timeout: 30000 diff --git a/.maestro/relay/restore_walletkit.yaml b/.maestro/relay/restore_walletkit.yaml new file mode 100644 index 000000000..ebdcad33f --- /dev/null +++ b/.maestro/relay/restore_walletkit.yaml @@ -0,0 +1,21 @@ +# Flow 7 — restore sessions: after a full restart (no clearState) the wallet +# rehydrates its active sessions from storage. +appId: ${APP_ID} +tags: + - relay-walletkit + - restore +--- +- runFlow: _connect_walletkit.yaml +# Foreground the wallet, then full stop + relaunch WITHOUT clearing state. +- launchApp: + appId: ${APP_ID} +- stopApp: + appId: ${APP_ID} +- launchApp: + appId: ${APP_ID} + clearState: false +- tapOn: 'Connected apps' +- extendedWaitUntil: + visible: + id: 'session-item' + timeout: 30000 diff --git a/.maestro/relay/sign_other_chain_walletkit.yaml b/.maestro/relay/sign_other_chain_walletkit.yaml new file mode 100644 index 000000000..69ffa22d1 --- /dev/null +++ b/.maestro/relay/sign_other_chain_walletkit.yaml @@ -0,0 +1,29 @@ +# Flow 4 — sign a message on another chain: switch chain, then personal_sign. +appId: ${APP_ID} +tags: + - relay-walletkit + - sign-other-chain +--- +- runFlow: _connect_walletkit.yaml +- extendedWaitUntil: + visible: 'Connected to' + timeout: 60000 +# Switch to Polygon via the dapp's TEST_MODE button. +- tapOn: 'Switch to Polygon (E2E)' +- extendedWaitUntil: + visible: 'Chain: Polygon' + timeout: 30000 +# Sign on the new chain. +- tapOn: '^Sign$' +- launchApp: + appId: ${APP_ID} +- extendedWaitUntil: + visible: + id: 'approve-button' + timeout: 60000 +- tapOn: + id: 'approve-button' +- runFlow: _focus_browser.yaml +- extendedWaitUntil: + visible: 'Sign success' + timeout: 60000 diff --git a/.maestro/relay/sign_walletkit.yaml b/.maestro/relay/sign_walletkit.yaml new file mode 100644 index 000000000..bc03f7bce --- /dev/null +++ b/.maestro/relay/sign_walletkit.yaml @@ -0,0 +1,29 @@ +# Flow 2 — sign a message: dapp requests personal_sign, wallet approves. +# Web dapp (Safari) + native wallet. Dapp actions/asserts are matched by TEXT +# (Safari webview doesn't expose testIDs); wallet by id. Return to the dapp by +# resuming Safari (Home + icon), not relaunching. +appId: ${APP_ID} +tags: + - relay-walletkit + - sign +--- +- runFlow: _connect_walletkit.yaml +- extendedWaitUntil: + visible: 'Connected to' + timeout: 60000 +# Trigger personal_sign on the dapp ("^Sign$" to avoid "Sign Typed Data"). +- tapOn: '^Sign$' +# Foreground the wallet and approve the request. +- launchApp: + appId: ${APP_ID} +- extendedWaitUntil: + visible: + id: 'approve-button' + timeout: 60000 +- tapOn: + id: 'approve-button' +# Resume the dapp; it shows the persistent result ("Sign success"). +- runFlow: _focus_browser.yaml +- extendedWaitUntil: + visible: 'Sign success' + timeout: 60000 diff --git a/.maestro/relay/switch_chain_walletkit.yaml b/.maestro/relay/switch_chain_walletkit.yaml new file mode 100644 index 000000000..f0b22190f --- /dev/null +++ b/.maestro/relay/switch_chain_walletkit.yaml @@ -0,0 +1,19 @@ +# Flow 3 — switch chain: dapp switches to a second EVM network (client-side; all +# EVM chains are in the approved namespaces, so no wallet re-approval). +# Web dapp: matched by text. The NetworkButton shows the current chain name; +# "^Ethereum$" targets it (vs the "Chain: Ethereum" info text). +appId: ${APP_ID} +tags: + - relay-walletkit + - switch-chain +--- +- runFlow: _connect_walletkit.yaml +- extendedWaitUntil: + visible: 'Connected to' + timeout: 60000 +# Switch to Polygon via the dapp's TEST_MODE button (the AppKit NetworkButton +# modal isn't reliably driveable by text on web). +- tapOn: 'Switch to Polygon (E2E)' +- extendedWaitUntil: + visible: 'Chain: Polygon' + timeout: 30000 diff --git a/.maestro/web/connect_confirm.yaml b/.maestro/web/connect_confirm.yaml index 3f5a0c428..5bfbe37ea 100644 --- a/.maestro/web/connect_confirm.yaml +++ b/.maestro/web/connect_confirm.yaml @@ -12,12 +12,26 @@ name: AppKit Web to React Native Wallet Connection Confirmed - tapOn: "Connect Wallet" - scroll - scroll -- tapOn: "React Native Sample Wallet React Native Sample Wallet" +- runFlow: + when: + platform: iOS + commands: + - tapOn: "React Native Sample Wallet" +- runFlow: + when: + platform: Android + commands: + - tapOn: "React Native Sample Wallet React Native Sample Wallet" - runFlow: when: platform: iOS commands: - back -- tapOn: "Approve" +- extendedWaitUntil: + visible: + id: "approve-button" + timeout: 10000 +- tapOn: + id: "approve-button" - assertVisible: "AppKit Interactions" - stopRecording \ No newline at end of file diff --git a/.maestro/web/connect_reject.yaml b/.maestro/web/connect_reject.yaml index 83f900ab9..032830b1d 100644 --- a/.maestro/web/connect_reject.yaml +++ b/.maestro/web/connect_reject.yaml @@ -12,8 +12,22 @@ name: AppKit Web to React Native Wallet Connection Rejected - tapOn: "Connect Wallet" - scroll - scroll -- tapOn: "React Native Sample Wallet React Native Sample Wallet" -- tapOn: "Reject" +- runFlow: + when: + platform: iOS + commands: + - tapOn: "React Native Sample Wallet" +- runFlow: + when: + platform: Android + commands: + - tapOn: "React Native Sample Wallet React Native Sample Wallet" +- extendedWaitUntil: + visible: + id: "reject-button" + timeout: 10000 +- tapOn: + id: "reject-button" - runFlow: when: platform: iOS diff --git a/.maestro/web/personal_sign_confirm.yaml b/.maestro/web/personal_sign_confirm.yaml index 61e754783..e2395b15f 100644 --- a/.maestro/web/personal_sign_confirm.yaml +++ b/.maestro/web/personal_sign_confirm.yaml @@ -11,11 +11,17 @@ name: AppKit Web to React Native Wallet Sign Request Confirmed - scroll - scroll - tapOn: "Sign Message" -- tapOn: "Approve" +- extendedWaitUntil: + visible: + id: "approve-button" + timeout: 10000 +- tapOn: + id: "approve-button" - runFlow: when: platform: iOS commands: - back +- waitForAnimationToEnd - assertVisible: "Signing Succeeded" - stopRecording \ No newline at end of file diff --git a/.maestro/web/personal_sign_reject.yaml b/.maestro/web/personal_sign_reject.yaml index c69275f9e..ddb6cd487 100644 --- a/.maestro/web/personal_sign_reject.yaml +++ b/.maestro/web/personal_sign_reject.yaml @@ -11,11 +11,17 @@ name: AppKit Web to React Native Wallet Sign Request Rejected - scroll - scroll - tapOn: "Sign Message" -- tapOn: "Reject" +- extendedWaitUntil: + visible: + id: "reject-button" + timeout: 10000 +- tapOn: + id: "reject-button" - runFlow: when: platform: iOS commands: - back +- waitForAnimationToEnd - assertVisible: "Signing Failed" - stopRecording \ No newline at end of file diff --git a/dapps/appkit-wagmi/src/App.tsx b/dapps/appkit-wagmi/src/App.tsx index 789a18519..3cd5437be 100644 --- a/dapps/appkit-wagmi/src/App.tsx +++ b/dapps/appkit-wagmi/src/App.tsx @@ -56,11 +56,29 @@ Sentry.init({ // 1. Get projectId const projectId = process.env.EXPO_PUBLIC_PROJECT_ID ?? ''; +// Feature the "React Native Sample Wallet" (WalletConnect Explorer id) so it's a +// one-tap entry on the connect modal's first screen — used by the web relay E2E, +// which connects by tapping the wallet by name (no QR). +const RN_SAMPLE_WALLET_ID = + '6880782cf8ff712bf8772b585960346290fd2499c1f1c51df9fe713d9d2e9384'; + // 2. Create config const metadata = getMetadata(); const clipboardClient = { setString: async (value: string) => { + // @react-native-clipboard/clipboard is native-only (no-ops on web), so + // AppKit's "Copy link" wouldn't actually copy the wc: URI on the web build. + // Use the Web Clipboard API on web (secure context: https / localhost) so + // the relay E2E can copy the URI and paste it into the native wallet. + if ( + Platform.OS === 'web' && + typeof navigator !== 'undefined' && + navigator.clipboard + ) { + await navigator.clipboard.writeText(value); + return; + } Clipboard.setString(value); }, }; @@ -84,6 +102,7 @@ const appKit = createAppKit({ debug: __DEV__, storage, extraConnectors, + featuredWalletIds: [RN_SAMPLE_WALLET_ID], }); const queryClient = new QueryClient(); diff --git a/dapps/appkit-wagmi/src/screens/Connections/components/OpenQrButton.tsx b/dapps/appkit-wagmi/src/screens/Connections/components/OpenQrButton.tsx new file mode 100644 index 000000000..573ef3243 --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Connections/components/OpenQrButton.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Pressable, StyleSheet, Text, View } from 'react-native'; +import { useAppKit } from '@reown/appkit-react-native'; +import { useAccount, useDisconnect, useSwitchChain } from 'wagmi'; + +// Polygon mainnet — the second chain the relay switch-chain tests target. +const POLYGON_CHAIN_ID = 137; + +// TEST-ONLY (EXPO_PUBLIC_TEST_MODE) dapp buttons for the relay E2E. On web the +// AppKit modal's controls aren't matchable by testID (Safari webview) and some +// are icon-only, so we expose plain, text-matchable buttons: +// - "Open QR (E2E)" -> open({ view: 'WalletConnect' }) jumps to the QR view. +// - "Disconnect (E2E)" -> wagmi disconnect (the AppKit account button shows the +// address/balance, which isn't reliably text-matchable on web). +// No-op in normal builds. +const TEST_MODE = process.env.EXPO_PUBLIC_TEST_MODE === 'true'; + +export function OpenQrButton() { + const { open } = useAppKit(); + const { isConnected } = useAccount(); + const { disconnect } = useDisconnect(); + const { switchChain } = useSwitchChain(); + + if (!TEST_MODE) { + return null; + } + + return ( + + open({ view: 'WalletConnect' })}> + Open QR (E2E) + + {isConnected ? ( + <> + switchChain({ chainId: POLYGON_CHAIN_ID })}> + Switch to Polygon (E2E) + + disconnect()}> + Disconnect (E2E) + + + ) : null} + + ); +} + +const styles = StyleSheet.create({ + container: { marginTop: 8, gap: 8, alignItems: 'center' }, + button: { + backgroundColor: '#8c8c8c', + paddingVertical: 10, + paddingHorizontal: 16, + borderRadius: 12, + alignItems: 'center', + }, + text: { color: 'white', fontWeight: '600' }, +}); diff --git a/dapps/appkit-wagmi/src/screens/Connections/components/WagmiActionsView.tsx b/dapps/appkit-wagmi/src/screens/Connections/components/WagmiActionsView.tsx index db2d2ef39..f3918c412 100644 --- a/dapps/appkit-wagmi/src/screens/Connections/components/WagmiActionsView.tsx +++ b/dapps/appkit-wagmi/src/screens/Connections/components/WagmiActionsView.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Button, Text, FlexView } from '@reown/appkit-ui-react-native'; -import { StyleSheet } from 'react-native'; +import { StyleSheet, Text as RNText } from 'react-native'; import { useSignMessage, useAccount, useSendTransaction, useEstimateGas, useSignTypedData } from 'wagmi'; import { Hex, parseEther } from 'viem'; import { SendTransactionData, SignMessageData, SignTypedDataData } from 'wagmi/query'; @@ -10,11 +10,17 @@ import { eip712 } from '@/utils/eip712'; export function WagmiActionsView() { const { isConnected } = useAccount(); + // Persistent, testID'd sign result for Maestro (the toasts below are + // ephemeral and racy to assert on). Cleared on each new sign attempt. + const [signResult, setSignResult] = useState(null); + const onSignSuccess = (data: SignMessageData) => { + setSignResult('Sign success'); ToastUtils.showSuccessToast('Signature successful', data); }; const onSignError = (error: Error) => { + setSignResult('Sign failed'); ToastUtils.showErrorToast('Signature failed', error.message); }; @@ -73,10 +79,14 @@ export function WagmiActionsView() { disabled={isPending} loading={isPending} testID="sign-message-button" - onPress={() => signMessage({ message: 'Hello AppKit!' })} + onPress={() => { + setSignResult(null); + signMessage({ message: 'Hello AppKit!' }); + }} > Sign + {signResult && {signResult}} {isGasError && Error estimating gas}