Skip to content

E2E Pay Tests (both) #221

E2E Pay Tests (both)

E2E Pay Tests (both) #221

name: E2E Pay Tests
# "(skip-metrics)" tells the E2E KPI job (WalletConnect/actions) to drop this run.
run-name: "E2E Pay Tests (${{ github.event.inputs.platform || 'both' }})${{ ((github.event_name == 'pull_request' && github.event.pull_request.draft) || (github.event_name == 'workflow_dispatch' && github.ref_name != 'develop')) && ' (skip-metrics)' || '' }}"
permissions:
contents: read
on:
workflow_dispatch:
inputs:
platform:
description: 'Platform(s) to run pay E2E tests'
required: false
type: choice
options:
- both
- android
- ios
default: both
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
paths:
- 'packages/reown_walletkit/example/**'
- 'packages/walletconnect_pay/**'
- '.github/workflows/ci_e2e_pay_tests.yml'
schedule:
# Run daily at 07:00 UTC
- cron: '0 7 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref_name }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
check-balance:
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
uses: ./.github/workflows/e2e_balance_check.yml
secrets: inherit
pay-tests-android:
needs: check-balance
if: ${{ always() && ((github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.platform != 'ios')) }}
runs-on: ubuntu-16core
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v4
- 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
- name: Install Java 17
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
architecture: x86_64
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.38.3"
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('packages/reown_walletkit/example/android/**/*.gradle*', 'packages/reown_walletkit/example/android/**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Cache Pub
uses: actions/cache@v4
with:
path: ~/.pub-cache
key: ${{ runner.os }}-pub-${{ hashFiles('packages/reown_walletkit/example/pubspec.lock') }}
restore-keys: |
${{ runner.os }}-pub-
- name: Get wallet dependencies
working-directory: packages/reown_walletkit/example
run: flutter pub get
- name: Add Secrets file
working-directory: packages/reown_walletkit/example/android
run: touch secrets.properties && echo "${{ secrets.SECRETS_FILE }}" >> secrets.properties
- name: Add Keystore file
working-directory: packages/reown_walletkit/example/android
run: echo "${{ secrets.KEYSTORE }}" | base64 --decode >> app/debug.keystore
- name: Build APK with test mode
id: build
working-directory: packages/reown_walletkit/example
run: |
flutter build apk \
--flavor internal \
--dart-define="PROJECT_ID=${{ secrets.WALLET_PROJECT_ID }}" \
--dart-define="ENABLE_TEST_MODE=true" \
--dart-define="TEST_WALLET_PRIVATE_KEY=${{ secrets.TEST_WALLET_PRIVATE_KEY }}" \
--release \
--target-platform android-x64 \
--split-per-abi
- name: Copy Maestro Pay test flows
uses: WalletConnect/actions/maestro/pay-tests@4be7c0112f9651fc63b18c137650b75d2fc9b200
- name: Install Maestro CLI
uses: WalletConnect/actions/maestro/setup@4be7c0112f9651fc63b18c137650b75d2fc9b200
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Run emulator and tests
id: maestro
uses: reactivecircus/android-emulator-runner@v2
env:
APP_ID: com.walletconnect.flutterwallet.internal
WPAY_CUSTOMER_KEY_SINGLE_NOKYC: ${{ secrets.WPAY_CUSTOMER_KEY_SINGLE_NOKYC }}
WPAY_MERCHANT_ID_SINGLE_NOKYC: ${{ secrets.WPAY_MERCHANT_ID_SINGLE_NOKYC }}
WPAY_CUSTOMER_KEY_MULTI_NOKYC: ${{ secrets.WPAY_CUSTOMER_KEY_MULTI_NOKYC }}
WPAY_MERCHANT_ID_MULTI_NOKYC: ${{ secrets.WPAY_MERCHANT_ID_MULTI_NOKYC }}
WPAY_CUSTOMER_KEY_MULTI_KYC: ${{ secrets.WPAY_CUSTOMER_KEY_MULTI_KYC }}
WPAY_MERCHANT_ID_MULTI_KYC: ${{ secrets.WPAY_MERCHANT_ID_MULTI_KYC }}
with:
api-level: 34
arch: x86_64
target: default
profile: pixel_6
ram-size: 4096M
disk-size: 4096M
heap-size: 576M
avd-name: test_device
force-avd-creation: false
disable-animations: true
emulator-boot-timeout: 900
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -no-snapshot-load
script: |
mkdir -p /tmp/maestro-output
adb logcat -c
adb logcat > /tmp/logcat.txt 2>&1 &
adb install -r packages/reown_walletkit/example/build/app/outputs/flutter-apk/app-x86_64-internal-release.apk
maestro test --include-tags pay --env "APP_ID=$APP_ID" --env "WPAY_CUSTOMER_KEY_SINGLE_NOKYC=$WPAY_CUSTOMER_KEY_SINGLE_NOKYC" --env "WPAY_MERCHANT_ID_SINGLE_NOKYC=$WPAY_MERCHANT_ID_SINGLE_NOKYC" --env "WPAY_CUSTOMER_KEY_MULTI_NOKYC=$WPAY_CUSTOMER_KEY_MULTI_NOKYC" --env "WPAY_MERCHANT_ID_MULTI_NOKYC=$WPAY_MERCHANT_ID_MULTI_NOKYC" --env "WPAY_CUSTOMER_KEY_MULTI_KYC=$WPAY_CUSTOMER_KEY_MULTI_KYC" --env "WPAY_MERCHANT_ID_MULTI_KYC=$WPAY_MERCHANT_ID_MULTI_KYC" --test-output-dir /tmp/maestro-output .maestro/
# Reset the USDT Permit2 allowance back to 0 so the next pay_usdt_polygon
# run re-exercises the on-chain approve step. Runs even when the suite
# failed (if: always()) so a mid-flow failure doesn't leave the allowance
# set; continue-on-error so a reset hiccup can't fail an otherwise-green
# run. Node step (signs a tx) — the private key goes via env, never the CLI.
- name: Reset USDT Permit2 allowance (Polygon)
if: always()
continue-on-error: true
uses: WalletConnect/actions/maestro/permit2-reset@4be7c0112f9651fc63b18c137650b75d2fc9b200
with:
chain-id: eip155:137
rpc-url: https://polygon-bor-rpc.publicnode.com
token-address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'
private-key: ${{ secrets.TEST_WALLET_PRIVATE_KEY }}
# Clean up APK before uploading artifacts — never upload APK/IPA
- name: Clean up build artifacts
if: always()
run: rm -rf packages/reown_walletkit/example/build/
- name: Upload debug artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: pay-tests-debug-android
path: |
/tmp/logcat.txt
/tmp/maestro-output/**/*.png
/tmp/maestro-output/**/*.jpg
/tmp/maestro-output/**/*.jpeg
/tmp/maestro-output/**/*.mp4
/tmp/maestro-output/**/*.mov
/tmp/maestro-output/**/*.webm
/tmp/maestro-output/**/*.gif
retention-days: 14
if-no-files-found: ignore
- name: Send Slack notification
if: failure()
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"text": "E2E Test Report - Android Pay - WalletKit Flutter",
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "🧪 E2E Test Report - Android (pay)" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Project:*\n`WalletKit Flutter`" },
{ "type": "mrkdwn", "text": "*Platform:*\n`Android`" },
{ "type": "mrkdwn", "text": "*Branch:*\n`${{ github.ref_name }}`" },
{ "type": "mrkdwn", "text": "*Trigger:*\n`${{ github.event_name }}`" }
]
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Build:*\n`${{ steps.build.outcome == 'success' && '✅ Success' || '❌ Failed' }}`" },
{ "type": "mrkdwn", "text": "*Maestro Tests:*\n`${{ steps.maestro.outcome == 'success' && '✅ Passed' || '❌ Failed' }}`" },
{ "type": "mrkdwn", "text": "*Overall Status:*\n`${{ job.status == 'success' && '✅ Success' || '❌ Failed' }}`" }
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "View Workflow Run" },
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
},
{
"type": "button",
"text": { "type": "plain_text", "text": "Download Artifacts" },
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts"
}
]
}
]
}
pay-tests-ios:
needs: check-balance
if: ${{ always() && ((github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.platform != 'android')) }}
runs-on: macos-latest-xlarge
timeout-minutes: 60
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Select Xcode 26 (iOS 26 SDK)
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26'
- name: Boot iOS simulator
run: |
# Pick whatever iPhone simulator the runner actually has, so this
# doesn't break each time the Xcode/runner image bumps the model.
SIMULATOR_UDID=$(xcrun simctl list devices available \
| grep -Eo 'iPhone[^(]*\([A-Fa-f0-9-]{36}\)' \
| grep -Eo '[A-Fa-f0-9-]{36}' | head -n 1)
if [ -z "$SIMULATOR_UDID" ]; then
echo "ERROR: no available iPhone simulator found"
xcrun simctl list devices available
exit 1
fi
echo "Using simulator: $SIMULATOR_UDID"
xcrun simctl boot "$SIMULATOR_UDID" 2>/dev/null || true
# Wait for full boot before configuring the simulator
xcrun simctl bootstatus "$SIMULATOR_UDID" -b
# Disable UIKit animations to reduce Maestro timing flakiness.
# Target the UDID we just booted, not `booted` — if the runner has
# another simulator up, `booted` is ambiguous (simctl errors on
# multiple) and could configure the wrong device.
xcrun simctl spawn "$SIMULATOR_UDID" defaults write com.apple.UIKit UIAnimationDragCoefficient -float 0 || true
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.38.3"
- name: Get wallet dependencies
working-directory: packages/reown_walletkit/example
run: flutter pub get
- name: Refresh CocoaPods specs
run: pod repo update --silent
# Two-step iOS simulator build: let Flutter emit Dart kernel +
# Generated.xcconfig, then drive xcodebuild ourselves. The custom
# xcodebuild step exists only to pin ARCHS=x86_64 for the
# mobile_scanner/MLKit simulator slice (see the build step below) —
# `flutter build ios --simulator` can't override ARCHS. The build is
# unsigned: the pay suite no longer exercises Universal Links, so no
# associated-domains entitlement needs to survive.
- name: Prepare Flutter iOS artifacts
working-directory: packages/reown_walletkit/example
run: |
flutter build ios \
--config-only \
--flavor internal \
--simulator \
--debug \
--dart-define="PROJECT_ID=${{ secrets.WALLET_PROJECT_ID }}" \
--dart-define="ENABLE_TEST_MODE=true" \
--dart-define="TEST_WALLET_PRIVATE_KEY=${{ secrets.TEST_WALLET_PRIVATE_KEY }}"
- name: Install Pods
working-directory: packages/reown_walletkit/example/ios
run: pod install
- name: Build Runner.app via xcodebuild (unsigned)
id: build_ios
working-directory: packages/reown_walletkit/example/ios
# Build the simulator app for x86_64. mobile_scanner pulls in Google
# MLKit, whose .frameworks are fat binaries with an arm64-*device*
# slice and an x86_64-*simulator* slice, but NO arm64-simulator slice.
# On the Apple Silicon runner the default arm64 simulator build fails
# to link MLKit ("building for 'iOS-simulator', but linking in object
# file ... built for 'iOS'"). x86_64 is the only working simulator
# slice; it runs on the simulator via Rosetta 2. EXCLUDED_ARCHS alone
# isn't enough (arm64 is the standard arch here), so pin ARCHS=x86_64
# with ONLY_ACTIVE_ARCH=NO.
run: |
set -o pipefail && env NSUnbufferedIO=YES \
xcodebuild \
-workspace Runner.xcworkspace \
-scheme internal \
-configuration Debug-internal \
-sdk iphonesimulator \
-destination "generic/platform=iOS Simulator" \
-derivedDataPath ../build/ios-ddcache \
ARCHS=x86_64 \
ONLY_ACTIVE_ARCH=NO \
CODE_SIGNING_ALLOWED=NO \
build
- name: Install app on simulator
run: |
APP_PATH=$(find packages/reown_walletkit/example/build/ios-ddcache \
-name "Runner.app" -path "*/Debug-internal-iphonesimulator/*" | head -1)
if [ -z "$APP_PATH" ]; then
echo "ERROR: Runner.app not found in DerivedData"
find packages/reown_walletkit/example/build -name "Runner.app" || true
exit 1
fi
echo "Installing: $APP_PATH"
xcrun simctl install booted "$APP_PATH"
- name: Copy Maestro Pay test flows
uses: WalletConnect/actions/maestro/pay-tests@4be7c0112f9651fc63b18c137650b75d2fc9b200
- name: Install Maestro CLI
uses: WalletConnect/actions/maestro/setup@4be7c0112f9651fc63b18c137650b75d2fc9b200
- name: Run Maestro iOS tests
id: maestro_ios
run: |
maestro test \
--env APP_ID=com.walletconnect.flutterwallet.internal \
--env WPAY_CUSTOMER_KEY_SINGLE_NOKYC=${{ secrets.WPAY_CUSTOMER_KEY_SINGLE_NOKYC }} \
--env WPAY_MERCHANT_ID_SINGLE_NOKYC=${{ secrets.WPAY_MERCHANT_ID_SINGLE_NOKYC }} \
--env WPAY_CUSTOMER_KEY_MULTI_NOKYC=${{ secrets.WPAY_CUSTOMER_KEY_MULTI_NOKYC }} \
--env WPAY_MERCHANT_ID_MULTI_NOKYC=${{ secrets.WPAY_MERCHANT_ID_MULTI_NOKYC }} \
--env WPAY_CUSTOMER_KEY_MULTI_KYC=${{ secrets.WPAY_CUSTOMER_KEY_MULTI_KYC }} \
--env WPAY_MERCHANT_ID_MULTI_KYC=${{ secrets.WPAY_MERCHANT_ID_MULTI_KYC }} \
--include-tags pay \
--test-output-dir /tmp/maestro-output \
.maestro/
# Reset the USDT Permit2 allowance back to 0 so the next pay_usdt_polygon
# run re-exercises the on-chain approve step. Runs even when the suite
# failed (if: always()) so a mid-flow failure doesn't leave the allowance
# set; continue-on-error so a reset hiccup can't fail an otherwise-green
# run. Node step (signs a tx) — the private key goes via env, never the CLI.
- name: Reset USDT Permit2 allowance (Polygon)
if: always()
continue-on-error: true
uses: WalletConnect/actions/maestro/permit2-reset@4be7c0112f9651fc63b18c137650b75d2fc9b200
with:
chain-id: eip155:137
rpc-url: https://polygon-bor-rpc.publicnode.com
token-address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'
private-key: ${{ secrets.TEST_WALLET_PRIVATE_KEY }}
# Clean up app build before uploading artifacts — never upload APK/IPA
- name: Clean up build artifacts
if: always()
run: rm -rf packages/reown_walletkit/example/build/
- name: Upload debug artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: pay-tests-debug-ios
path: |
/tmp/maestro-output/**/*.png
/tmp/maestro-output/**/*.jpg
/tmp/maestro-output/**/*.jpeg
/tmp/maestro-output/**/*.mp4
/tmp/maestro-output/**/*.mov
/tmp/maestro-output/**/*.webm
/tmp/maestro-output/**/*.gif
retention-days: 14
if-no-files-found: ignore
- name: Send Slack notification
if: failure()
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"text": "E2E Test Report - iOS Pay - WalletKit Flutter",
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "🧪 E2E Test Report - iOS (pay)" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Project:*\n`WalletKit Flutter`" },
{ "type": "mrkdwn", "text": "*Platform:*\n`iOS`" },
{ "type": "mrkdwn", "text": "*Branch:*\n`${{ github.ref_name }}`" },
{ "type": "mrkdwn", "text": "*Trigger:*\n`${{ github.event_name }}`" }
]
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Build:*\n`${{ steps.build_ios.outcome == 'success' && '✅ Success' || '❌ Failed' }}`" },
{ "type": "mrkdwn", "text": "*Maestro Tests:*\n`${{ steps.maestro_ios.outcome == 'success' && '✅ Passed' || '❌ Failed' }}`" },
{ "type": "mrkdwn", "text": "*Overall Status:*\n`${{ job.status == 'success' && '✅ Success' || '❌ Failed' }}`" }
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "View Workflow Run" },
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
},
{
"type": "button",
"text": { "type": "plain_text", "text": "Download Artifacts" },
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts"
}
]
}
]
}