diff --git a/.github/workflows/ci_e2e_walletkit.yaml b/.github/workflows/ci_e2e_walletkit.yaml
index 4025e205..742e62e4 100644
--- a/.github/workflows/ci_e2e_walletkit.yaml
+++ b/.github/workflows/ci_e2e_walletkit.yaml
@@ -22,11 +22,6 @@ on:
required: false
default: 'pay'
type: string
- app-id:
- description: 'App bundle ID / package name for Maestro'
- required: false
- default: 'com.walletconnect.web3wallet.rnsample.internal'
- type: string
platform:
description: 'Platform to run tests on'
required: false
@@ -39,7 +34,7 @@ on:
env:
MAESTRO_TAGS: ${{ inputs.maestro-tags || 'pay' }}
- MAESTRO_APP_ID: ${{ inputs.app-id || 'com.walletconnect.web3wallet.rnsample.internal' }}
+ MAESTRO_APP_ID: com.walletconnect.web3wallet.rnsample.internal
ROOT_PATH: wallets/rn_cli_wallet
jobs:
@@ -63,6 +58,11 @@ jobs:
sudo xcode-select -s "$XCODE_PATH"
xcodebuild -version
+ - name: Setup SSH for Match
+ uses: webfactory/ssh-agent@v0.9.0
+ with:
+ ssh-private-key: ${{ secrets.MATCH_SSH_KEY }}
+
- name: Setup
uses: ./.github/actions/ci-setup
with:
@@ -76,6 +76,7 @@ jobs:
fi
echo "${{ secrets.WALLETKIT_ENV_FILE }}" > ${{ env.ROOT_PATH }}/.env
echo "ENV_TEST_PRIVATE_KEY=${{ secrets.TEST_WALLET_PRIVATE_KEY }}" >> ${{ env.ROOT_PATH }}/.env
+ echo "ENV_TEST_MODE=true" >> ${{ env.ROOT_PATH }}/.env
- name: Install Ruby
uses: ruby/setup-ruby@v1
@@ -114,6 +115,11 @@ jobs:
BUILD_FOR_SIMULATOR: true
PODS_CACHE_HIT: ${{ steps.pods-cache.outputs.cache-hit }}
SENTRY_DISABLE_AUTO_UPLOAD: true
+ APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
+ APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
+ APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
+ MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
+ MATCH_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
- name: Find .app path
run: |
@@ -136,22 +142,18 @@ jobs:
curl -fsSL "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
- - name: Create payment via API
- id: payment
- run: bash scripts/create-payment.sh
- env:
- WPAY_CUSTOMER_KEY: ${{ secrets.WPAY_CUSTOMER_KEY }}
- WPAY_MERCHANT_ID: ${{ secrets.WPAY_MERCHANT_ID }}
-
- - name: Set payment URL in simulator clipboard
- run: echo "${{ steps.payment.outputs.gateway_url }}" | xcrun simctl pbcopy "$DEVICE_ID"
-
- name: Run Maestro tests
id: maestro
run: |
set -o pipefail
maestro test \
--env APP_ID="${{ env.MAESTRO_APP_ID }}" \
+ --env WPAY_CUSTOMER_KEY_MULTI_KYC="${{ secrets.WPAY_CUSTOMER_KEY_MULTI_KYC }}" \
+ --env WPAY_MERCHANT_ID_MULTI_KYC="${{ secrets.WPAY_MERCHANT_ID_MULTI_KYC }}" \
+ --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_SINGLE_NOKYC="${{ secrets.WPAY_CUSTOMER_KEY_SINGLE_NOKYC }}" \
+ --env WPAY_MERCHANT_ID_SINGLE_NOKYC="${{ secrets.WPAY_MERCHANT_ID_SINGLE_NOKYC }}" \
--include-tags "${{ env.MAESTRO_TAGS }}" \
--test-output-dir maestro-artifacts \
--debug-output maestro-artifacts \
@@ -251,6 +253,12 @@ jobs:
- name: Accept Android Licenses
uses: SimonMarquis/android-accept-licenses@8d8deab5b7ab2aaea9ed69f0b802e53173f21fd1 # v1
+ - name: Add Secrets file
+ run: echo "${{ secrets.ANDROID_SECRETS_FILE }}" > ${{ env.ROOT_PATH }}/android/secrets.properties
+
+ - name: Add Keystore file
+ run: echo ${{ secrets.WC_INTERNAL_KEYSTORE }} | base64 --decode >> ${{ env.ROOT_PATH }}/android/app/${{ vars.WC_INTERNAL_KEYSTORE_NAME }}.keystore
+
- name: Optimize Gradle for CI
run: |
echo "org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m" >> ${{ env.ROOT_PATH }}/android/gradle.properties
@@ -276,6 +284,12 @@ jobs:
id: build
run: cd ${{ env.ROOT_PATH }} && 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
@@ -287,13 +301,6 @@ jobs:
curl -fsSL "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
- - name: Create payment via API
- id: payment
- run: bash scripts/create-payment.sh
- env:
- WPAY_CUSTOMER_KEY: ${{ secrets.WPAY_CUSTOMER_KEY }}
- WPAY_MERCHANT_ID: ${{ secrets.WPAY_MERCHANT_ID }}
-
- name: Run E2E tests on Android Emulator
id: maestro
uses: reactivecircus/android-emulator-runner@70f4dee990796918b78d040e3278474bdbd348a7 # v2
@@ -303,6 +310,7 @@ jobs:
target: default
avd-name: test_device
ram-size: 4096M
+ disk-size: 4096M
heap-size: 576M
emulator-boot-timeout: 900
force-avd-creation: false
@@ -310,7 +318,7 @@ jobs:
disable-animations: true
script: |
adb install ${{ env.ROOT_PATH }}/android/app/build/outputs/apk/internal/app-internal.apk
- $HOME/.maestro/bin/maestro test --env APP_ID="${{ env.MAESTRO_APP_ID }}" --env GATEWAY_URL="${{ steps.payment.outputs.gateway_url }}" --include-tags "${{ env.MAESTRO_TAGS }}" --test-output-dir maestro-artifacts --debug-output maestro-artifacts .maestro/ >maestro-output.log 2>&1; echo $? > maestro-exit-code
+ $HOME/.maestro/bin/maestro test --env APP_ID="${{ env.MAESTRO_APP_ID }}" --env WPAY_CUSTOMER_KEY_MULTI_KYC="${{ secrets.WPAY_CUSTOMER_KEY_MULTI_KYC }}" --env WPAY_MERCHANT_ID_MULTI_KYC="${{ secrets.WPAY_MERCHANT_ID_MULTI_KYC }}" --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_SINGLE_NOKYC="${{ secrets.WPAY_CUSTOMER_KEY_SINGLE_NOKYC }}" --env WPAY_MERCHANT_ID_SINGLE_NOKYC="${{ secrets.WPAY_MERCHANT_ID_SINGLE_NOKYC }}" --include-tags "${{ env.MAESTRO_TAGS }}" --test-output-dir maestro-artifacts --debug-output maestro-artifacts .maestro/ >maestro-output.log 2>&1; echo $? > maestro-exit-code
cat maestro-output.log
exit $(cat maestro-exit-code)
diff --git a/.github/workflows/e2e-balance-check.yml b/.github/workflows/e2e-balance-check.yml
index 6aeb162d..89b1b5cc 100644
--- a/.github/workflows/e2e-balance-check.yml
+++ b/.github/workflows/e2e-balance-check.yml
@@ -11,10 +11,12 @@ permissions:
env:
BASE_USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
BASE_RPC: 'https://mainnet.base.org'
+ OP_USDC: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85'
+ OP_RPC: 'https://mainnet.optimism.io'
jobs:
check-balance:
- name: Check E2E Wallet USDC Balance
+ name: Check E2E Wallet Balances
runs-on: ubuntu-latest
env:
SLACK_FAUCETBOT_WEBHOOK_URL: ${{ secrets.SLACK_FAUCETBOT_WEBHOOK_URL }}
@@ -27,13 +29,13 @@ jobs:
run: |
BALANCE=$(cast call --rpc-url "$BASE_RPC" "$BASE_USDC" \
"balanceOf(address)(uint256)" \
- "${{ vars.TEST_WALLET_ADDRESS }}" | awk '{print $1}')
+ "${{ vars.TEST_WALLET_ADDRESS }}" | tr -d '[:space:]')
echo "balance=$BALANCE" >> $GITHUB_OUTPUT
BALANCE_HUMAN=$(echo "scale=2; $BALANCE / 1000000" | bc)
echo "USDC on Base: $BALANCE_HUMAN ($BALANCE raw)"
- - name: Prepare alert
- id: alert
+ - name: Prepare USDC alert
+ id: usdc_alert
run: |
BALANCE="${{ steps.usdc_balance.outputs.balance }}"
THRESHOLD="${{ vars.USDC_THRESHOLD_UNITS || '500000' }}"
@@ -47,9 +49,9 @@ jobs:
echo "::warning::$ALERT_TEXT"
fi
- - name: Send Slack alert
+ - name: Send Slack alert (USDC)
if: |
- steps.alert.outputs.should_alert == 'true' &&
+ steps.usdc_alert.outputs.should_alert == 'true' &&
env.SLACK_FAUCETBOT_WEBHOOK_URL != ''
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0
with:
@@ -57,12 +59,57 @@ jobs:
webhook-type: incoming-webhook
payload: |
{
- "text": "${{ steps.alert.outputs.text }}"
+ "text": "${{ steps.usdc_alert.outputs.text }}"
}
- - name: Skip Slack alert (no webhook)
+ - name: Skip Slack alert (USDC - no webhook)
if: |
- steps.alert.outputs.should_alert == 'true' &&
+ steps.usdc_alert.outputs.should_alert == 'true' &&
env.SLACK_FAUCETBOT_WEBHOOK_URL == ''
run: |
- echo "::warning::SLACK_FAUCETBOT_WEBHOOK_URL not configured, skipping alert"
+ echo "::warning::SLACK_FAUCETBOT_WEBHOOK_URL not configured, skipping USDC alert"
+
+ - name: Check USDC balance on Optimism
+ id: op_usdc_balance
+ run: |
+ BALANCE=$(cast call --rpc-url "$OP_RPC" "$OP_USDC" \
+ "balanceOf(address)(uint256)" \
+ "${{ vars.TEST_WALLET_ADDRESS }}" | tr -d '[:space:]')
+ echo "balance=$BALANCE" >> $GITHUB_OUTPUT
+ BALANCE_HUMAN=$(echo "scale=2; $BALANCE / 1000000" | bc)
+ echo "USDC on Optimism: $BALANCE_HUMAN ($BALANCE raw)"
+
+ - name: Prepare USDC on Optimism alert
+ id: op_usdc_alert
+ run: |
+ BALANCE="${{ steps.op_usdc_balance.outputs.balance }}"
+ THRESHOLD="${{ vars.USDC_THRESHOLD_UNITS || '500000' }}"
+ if [ "$BALANCE" -lt "$THRESHOLD" ]; then
+ BALANCE_HUMAN=$(echo "scale=2; $BALANCE / 1000000" | bc)
+ THRESHOLD_HUMAN=$(echo "scale=2; $THRESHOLD / 1000000" | bc)
+ ALERT_TEXT="[request] Can i have ${THRESHOLD_HUMAN} USDC on Optimism in wallet: \
+ ${{ vars.TEST_WALLET_ADDRESS }}"
+ echo "should_alert=true" >> "$GITHUB_OUTPUT"
+ echo "text=$ALERT_TEXT" >> "$GITHUB_OUTPUT"
+ echo "::warning::$ALERT_TEXT"
+ fi
+
+ - name: Send Slack alert (USDC on Optimism)
+ if: |
+ steps.op_usdc_alert.outputs.should_alert == 'true' &&
+ env.SLACK_FAUCETBOT_WEBHOOK_URL != ''
+ uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0
+ with:
+ webhook: ${{ env.SLACK_FAUCETBOT_WEBHOOK_URL }}
+ webhook-type: incoming-webhook
+ payload: |
+ {
+ "text": "${{ steps.op_usdc_alert.outputs.text }}"
+ }
+
+ - name: Skip Slack alert (USDC on Optimism - no webhook)
+ if: |
+ steps.op_usdc_alert.outputs.should_alert == 'true' &&
+ env.SLACK_FAUCETBOT_WEBHOOK_URL == ''
+ run: |
+ echo "::warning::SLACK_FAUCETBOT_WEBHOOK_URL not configured, skipping Optimism USDC alert"
diff --git a/.maestro/flows/pay_confirm_and_verify.yaml b/.maestro/flows/pay_confirm_and_verify.yaml
new file mode 100644
index 00000000..d033ee1b
--- /dev/null
+++ b/.maestro/flows/pay_confirm_and_verify.yaml
@@ -0,0 +1,21 @@
+appId: ${APP_ID}
+---
+# Shared flow: Tap Pay, verify loading, wait for success, tap "Got it!"
+
+# On review screen, tap Pay button
+- extendedWaitUntil:
+ visible:
+ id: "pay-button-pay"
+ timeout: 10000
+- tapOn:
+ id: "pay-button-pay"
+
+# Wait for success screen (generous timeout for on-chain confirmation)
+- extendedWaitUntil:
+ visible:
+ id: "pay-result-success-icon"
+ timeout: 30000
+
+# Tap "Got it!" button
+- tapOn:
+ id: "pay-button-result-action"
diff --git a/.maestro/flows/pay_open_and_paste_url.yaml b/.maestro/flows/pay_open_and_paste_url.yaml
new file mode 100644
index 00000000..b2e5b118
--- /dev/null
+++ b/.maestro/flows/pay_open_and_paste_url.yaml
@@ -0,0 +1,36 @@
+appId: ${APP_ID}
+---
+# Shared flow: Launch wallet, open scanner, paste payment URL, wait for merchant info
+# Requires: ${output.gateway_url} to be set by a prior runScript step
+
+# Launch wallet app
+- launchApp:
+ appId: ${APP_ID}
+ permissions:
+ all: allow
+
+# Tap scan button to open scanner options modal
+- tapOn:
+ id: "button-scan"
+
+# Type the payment URL into the test input field
+- tapOn:
+ id: "input-paste-url"
+
+# Dismiss iOS keyboard language prompt if it appears
+- runFlow:
+ when:
+ visible: "Continue"
+ commands:
+ - tapOn: "Continue"
+
+- inputText: ${output.gateway_url}
+- pressKey: Enter
+- tapOn:
+ id: "button-submit-url"
+
+# Wait for payment options to load with merchant info
+- extendedWaitUntil:
+ visible:
+ id: "pay-merchant-info"
+ timeout: 15000
diff --git a/.maestro/flows/pay_open_via_deeplink.yaml b/.maestro/flows/pay_open_via_deeplink.yaml
new file mode 100644
index 00000000..56d8a1e5
--- /dev/null
+++ b/.maestro/flows/pay_open_via_deeplink.yaml
@@ -0,0 +1,13 @@
+appId: ${APP_ID}
+---
+# Shared flow: Open payment URL via deep link and wait for merchant info
+# Requires: ${output.gateway_url} to be set by a prior runScript step
+
+# Open the payment URL as a deep link
+- openLink: ${output.gateway_url}
+
+# Wait for payment options to load with merchant info
+- extendedWaitUntil:
+ visible:
+ id: "pay-merchant-info"
+ timeout: 15000
diff --git a/.maestro/pay_confirm.yaml b/.maestro/pay_confirm.yaml
deleted file mode 100644
index 8ad7d611..00000000
--- a/.maestro/pay_confirm.yaml
+++ /dev/null
@@ -1,97 +0,0 @@
-appId: ${APP_ID}
-name: WalletConnect Pay - Wallet Payment Confirmed
-tags:
- - pay
----
-# Launch wallet app (GATEWAY_URL is already in the device clipboard, set by CI)
-- launchApp:
- appId: ${APP_ID}
- permissions:
- all: allow
-
-- startRecording: "WalletConnect Pay Confirmed"
-
-# Tap scan button to open scanner options modal
-- tapOn:
- id: "button-scan"
-
-# Android: type the payment URL into the test input field
-- runFlow:
- when:
- platform: Android
- commands:
- - tapOn:
- id: "input-paste-url"
- - inputText: ${GATEWAY_URL}
- - pressKey: Enter
- - tapOn:
- id: "button-submit-url"
-
-# iOS: paste from clipboard
-- runFlow:
- when:
- platform: iOS
- commands:
- - tapOn:
- id: "button-paste-url"
- - runFlow:
- when:
- visible: "Allow Paste"
- commands:
- - tapOn: "Allow Paste"
-
-# Wait for payment options to load with merchant info
-- extendedWaitUntil:
- visible:
- id: "pay-merchant-info"
- timeout: 15000
-
-# Handle single vs. multi option: if Continue button visible, tap it
-- runFlow:
- when:
- visible:
- id: "pay-button-continue"
- commands:
- - tapOn:
- id: "pay-button-continue"
-
-# Handle personal details webview (if user needs KYC)
-- runFlow:
- when:
- visible: "Add your personal details"
- commands:
- # Data is autocompleted, just tap Add
- - tapOn: "Add"
- # Confirm your details popup: tap the Terms checkbox then Confirm
- - extendedWaitUntil:
- visible: "Confirm your details"
- timeout: 10000
- # Tap the checkbox / label for terms agreement
- - tapOn:
- text: "I agree to the Terms and Conditions and Privacy Policy"
- retryTapIfNoChange: true
- - tapOn: "Confirm"
-
-# On review screen, tap Pay button
-- extendedWaitUntil:
- visible:
- id: "pay-button-pay"
- timeout: 10000
-- tapOn:
- id: "pay-button-pay"
-
-# Verify confirming screen
-- assertVisible:
- id: "pay-loading-message"
-
-# Wait for success screen (generous timeout for on-chain confirmation)
-- extendedWaitUntil:
- visible:
- id: "pay-result-success-icon"
- timeout: 30000
-
-# Tap "Got it!" button
-- tapOn:
- id: "pay-button-result-action"
-
-- stopRecording
diff --git a/.maestro/pay_multiple_options_kyc.yaml b/.maestro/pay_multiple_options_kyc.yaml
new file mode 100644
index 00000000..ba6ef6e1
--- /dev/null
+++ b/.maestro/pay_multiple_options_kyc.yaml
@@ -0,0 +1,88 @@
+appId: ${APP_ID}
+name: WalletConnect Pay - Multiple Options with KYC
+tags:
+ - pay
+---
+# Create payment via API (multi-option, KYC merchant)
+- runScript:
+ file: scripts/create-payment.js
+ env:
+ WPAY_CUSTOMER_KEY: ${WPAY_CUSTOMER_KEY_MULTI_KYC}
+ WPAY_MERCHANT_ID: ${WPAY_MERCHANT_ID_MULTI_KYC}
+
+- startRecording: "WalletConnect Pay Multiple Options KYC"
+
+# Open wallet, paste payment URL, wait for merchant info
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Verify first option is pre-selected (index 0)
+- assertVisible:
+ id: "pay-option-0-selected"
+
+# Verify at least a second option exists (multiple options)
+- assertVisible:
+ id: "pay-option-1"
+
+# Verify "Info required" badge is visible on KYC options
+- assertVisible:
+ id: "pay-info-required-badge"
+
+# Verify "?" info button is visible in the header
+- assertVisible:
+ id: "pay-button-info"
+
+# Select the second option (index 1)
+- tapOn:
+ id: "pay-option-1"
+
+# Verify second option is now selected
+- assertVisible:
+ id: "pay-option-1-selected"
+
+# Verify first option is now deselected
+- assertVisible:
+ id: "pay-option-0"
+
+# Copy the network name from the selected option's accessibilityLabel
+- copyTextFrom:
+ id: "pay-option-1-selected"
+
+# Tap Continue to proceed
+- tapOn:
+ id: "pay-button-continue"
+
+# Handle personal details webview (KYC)
+- extendedWaitUntil:
+ visible: "Add your personal details"
+ timeout: 10000
+
+# Data is autocompleted, just tap Add
+- tapOn: "Add"
+
+# Confirm your details popup: tap the Terms checkbox then Confirm
+- extendedWaitUntil:
+ visible: "Confirm your details"
+ timeout: 10000
+
+# Tap the checkbox / label for terms agreement
+- tapOn:
+ text: "I agree to the Terms and Conditions and Privacy Policy"
+ retryTapIfNoChange: true
+- tapOn: "Confirm"
+
+# Verify review screen shows the same token we selected
+- assertVisible:
+ id: "pay-review-token-${maestro.copiedText}"
+
+# Verify pay button shows the correct amount
+- copyTextFrom:
+ id: "pay-button-pay"
+- assertTrue:
+ condition: "${maestro.copiedText == 'Pay $0.01'}"
+
+# Tap Pay, verify success
+- runFlow:
+ file: flows/pay_confirm_and_verify.yaml
+
+- stopRecording
diff --git a/.maestro/pay_multiple_options_nokyc.yaml b/.maestro/pay_multiple_options_nokyc.yaml
new file mode 100644
index 00000000..f8de6c56
--- /dev/null
+++ b/.maestro/pay_multiple_options_nokyc.yaml
@@ -0,0 +1,70 @@
+appId: ${APP_ID}
+name: WalletConnect Pay - Multiple Options No KYC
+tags:
+ - pay
+---
+# Create payment via API (multi-option, no-KYC merchant)
+- runScript:
+ file: scripts/create-payment.js
+ env:
+ WPAY_CUSTOMER_KEY: ${WPAY_CUSTOMER_KEY_MULTI_NOKYC}
+ WPAY_MERCHANT_ID: ${WPAY_MERCHANT_ID_MULTI_NOKYC}
+
+- startRecording: "WalletConnect Pay Multiple Options No KYC"
+
+# Open wallet, paste payment URL, wait for merchant info
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Verify first option is pre-selected (index 0)
+- assertVisible:
+ id: "pay-option-0-selected"
+
+# Verify at least a second option exists (multiple options)
+- assertVisible:
+ id: "pay-option-1"
+
+# Verify "?" info button is NOT visible (no KYC merchant)
+- assertNotVisible:
+ id: "pay-button-info"
+
+# Verify "info required" badge is NOT visible (no KYC)
+- assertNotVisible:
+ id: "pay-info-required-badge"
+
+# Select the second option (index 1)
+- tapOn:
+ id: "pay-option-1"
+
+# Verify second option is now selected
+- assertVisible:
+ id: "pay-option-1-selected"
+
+# Verify first option is now deselected
+- assertVisible:
+ id: "pay-option-0"
+
+# Copy the network name from the selected option's accessibilityLabel
+- copyTextFrom:
+ id: "pay-option-1-selected"
+
+# Tap Continue to proceed
+- tapOn:
+ id: "pay-button-continue"
+
+# No KYC — goes straight to review screen
+# Verify review screen shows the same token we selected
+- assertVisible:
+ id: "pay-review-token-${maestro.copiedText}"
+
+# Verify pay button shows the correct amount
+- copyTextFrom:
+ id: "pay-button-pay"
+- assertTrue:
+ condition: "${maestro.copiedText == 'Pay $0.01'}"
+
+# Tap Pay, verify success
+- runFlow:
+ file: flows/pay_confirm_and_verify.yaml
+
+- stopRecording
diff --git a/.maestro/pay_single_option_nokyc.yaml b/.maestro/pay_single_option_nokyc.yaml
new file mode 100644
index 00000000..622e0028
--- /dev/null
+++ b/.maestro/pay_single_option_nokyc.yaml
@@ -0,0 +1,35 @@
+appId: ${APP_ID}
+name: WalletConnect Pay - Single Option No KYC
+tags:
+ - pay
+---
+# Create payment via API (single-option, no-KYC merchant)
+- runScript:
+ file: scripts/create-payment.js
+ env:
+ WPAY_CUSTOMER_KEY: ${WPAY_CUSTOMER_KEY_SINGLE_NOKYC}
+ WPAY_MERCHANT_ID: ${WPAY_MERCHANT_ID_SINGLE_NOKYC}
+
+- startRecording: "WalletConnect Pay Single Option No KYC"
+
+# Open wallet, paste payment URL, wait for merchant info
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Single option auto-selects — go straight to review screen
+
+# Verify pay button shows the correct amount
+- extendedWaitUntil:
+ visible:
+ id: "pay-button-pay"
+ timeout: 10000
+- copyTextFrom:
+ id: "pay-button-pay"
+- assertTrue:
+ condition: "${maestro.copiedText == 'Pay $0.01'}"
+
+# Tap Pay, verify success
+- runFlow:
+ file: flows/pay_confirm_and_verify.yaml
+
+- stopRecording
diff --git a/.maestro/pay_single_option_nokyc_deeplink.yaml b/.maestro/pay_single_option_nokyc_deeplink.yaml
new file mode 100644
index 00000000..5953f097
--- /dev/null
+++ b/.maestro/pay_single_option_nokyc_deeplink.yaml
@@ -0,0 +1,35 @@
+appId: ${APP_ID}
+name: WalletConnect Pay - Single Option No KYC (Deep Link)
+tags:
+ - pay
+---
+# Create payment via API (single-option, no-KYC merchant)
+- runScript:
+ file: scripts/create-payment.js
+ env:
+ WPAY_CUSTOMER_KEY: ${WPAY_CUSTOMER_KEY_SINGLE_NOKYC}
+ WPAY_MERCHANT_ID: ${WPAY_MERCHANT_ID_SINGLE_NOKYC}
+
+- startRecording: "WalletConnect Pay Single Option No KYC Deep Link"
+
+# Open wallet via deep link with payment URL
+- runFlow:
+ file: flows/pay_open_via_deeplink.yaml
+
+# Single option auto-selects — go straight to review screen
+
+# Verify pay button shows the correct amount
+- extendedWaitUntil:
+ visible:
+ id: "pay-button-pay"
+ timeout: 10000
+- copyTextFrom:
+ id: "pay-button-pay"
+- assertTrue:
+ condition: "${maestro.copiedText == 'Pay $0.01'}"
+
+# Tap Pay, verify success
+- runFlow:
+ file: flows/pay_confirm_and_verify.yaml
+
+- stopRecording
diff --git a/.maestro/scripts/create-payment.js b/.maestro/scripts/create-payment.js
new file mode 100644
index 00000000..4a033c17
--- /dev/null
+++ b/.maestro/scripts/create-payment.js
@@ -0,0 +1,28 @@
+// Creates a WalletConnect Pay payment via the API.
+// Expects WPAY_CUSTOMER_KEY and WPAY_MERCHANT_ID env vars from Maestro.
+// Sets output.gateway_url for use in subsequent flow steps.
+
+var response = http.post('https://api.pay.walletconnect.com/v1/payments', {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Api-Key': WPAY_CUSTOMER_KEY,
+ 'Merchant-Id': WPAY_MERCHANT_ID,
+ },
+ body: JSON.stringify({
+ referenceId: '' + Date.now() + Math.random().toString(36).substring(2, 10),
+ amount: { value: '1', unit: 'iso4217/USD' },
+ }),
+});
+
+if (response.status < 200 || response.status >= 300) {
+ throw new Error('API returned HTTP ' + response.status + ': ' + response.body);
+}
+
+var data = json(response.body);
+
+if (!data.gatewayUrl) {
+ throw new Error('No gatewayUrl in response: ' + response.body);
+}
+
+console.log('Payment created: ' + data.paymentId);
+output.gateway_url = data.gatewayUrl;
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index e195b966..666c18be 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -107,6 +107,36 @@ platform :ios do
end
lane :build_for_simulator do |options|
+ sign_build = ENV['MATCH_GIT_URL'] && !ENV['MATCH_GIT_URL'].empty?
+
+ if sign_build
+ UI.message("🔑 Signing enabled — setting up Match development profile")
+ setup_ci()
+
+ api_key = app_store_connect_api_key(
+ key_id: ENV["APPLE_KEY_ID"],
+ issuer_id: ENV["APPLE_ISSUER_ID"],
+ key_content: ENV["APPLE_KEY_CONTENT"],
+ duration: 1200,
+ in_house: false,
+ )
+
+ match(
+ readonly: true,
+ type: "development",
+ app_identifier: ENV['BUNDLE_ID'],
+ git_url: ENV['MATCH_GIT_URL'],
+ api_key: api_key,
+ )
+
+ update_code_signing_settings(
+ use_automatic_signing: false,
+ path: ENV['XCODE_PROJECT_PATH'],
+ bundle_identifier: ENV['BUNDLE_ID'],
+ code_sign_identity: "Apple Development",
+ profile_name: "match Development #{ENV['BUNDLE_ID']}"
+ )
+ end
# Install pods (skip repo_update if cache hit)
cocoapods(
@@ -120,16 +150,22 @@ platform :ios do
build_dir = File.expand_path("../build")
archive_path = "#{build_dir}/wallet-simulator.xcarchive"
- # Archive for simulator without code signing
- gym(
+ gym_params = {
workspace: ENV['XCWORKSPACE_PATH'],
scheme: ENV['SCHEME'],
configuration: "Release",
destination: "generic/platform=iOS Simulator",
archive_path: archive_path,
- skip_codesigning: true,
skip_package_ipa: true,
- )
+ }
+
+ if sign_build
+ gym_params[:xcargs] = "CODE_SIGNING_ALLOWED=YES"
+ else
+ gym_params[:skip_codesigning] = true
+ end
+
+ gym(gym_params)
# Find the .app directly inside the xcarchive — no IPA needed for simulator install
app_path = Dir.glob("#{archive_path}/Products/Applications/*.app").first
diff --git a/scripts/create-payment.sh b/scripts/create-payment.sh
deleted file mode 100755
index 1cf4c939..00000000
--- a/scripts/create-payment.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-# Creates a WalletConnect Pay payment via the API and outputs the gateway URL.
-# Required env vars: WPAY_CUSTOMER_KEY, WPAY_MERCHANT_ID
-
-API_URL="https://api.pay.walletconnect.com/v1/payments"
-
-: "${WPAY_CUSTOMER_KEY:?WPAY_CUSTOMER_KEY is required}"
-: "${WPAY_MERCHANT_ID:?WPAY_MERCHANT_ID is required}"
-
-REFERENCE_ID=$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid)
-REFERENCE_ID=${REFERENCE_ID//-/}
-
-HTTP_CODE=$(curl -s -o /tmp/payment_response.json -w "%{http_code}" -X POST "$API_URL" \
- -H "Content-Type: application/json" \
- -H "Api-Key: ${WPAY_CUSTOMER_KEY}" \
- -H "Merchant-Id: ${WPAY_MERCHANT_ID}" \
- -d "{\"referenceId\":\"${REFERENCE_ID}\",\"amount\":{\"value\":\"1\",\"unit\":\"iso4217/USD\"}}")
-
-RESPONSE=$(cat /tmp/payment_response.json)
-
-if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
- echo "Error: API returned HTTP $HTTP_CODE" >&2
- echo "Response: $RESPONSE" >&2
- exit 1
-fi
-
-GATEWAY_URL=$(echo "$RESPONSE" | jq -r '.gatewayUrl')
-PAYMENT_ID=$(echo "$RESPONSE" | jq -r '.paymentId')
-
-if [ -z "$GATEWAY_URL" ] || [ "$GATEWAY_URL" = "null" ]; then
- echo "Error: Failed to get gatewayUrl from API response" >&2
- echo "Response: $RESPONSE" >&2
- exit 1
-fi
-
-echo "gateway_url=$GATEWAY_URL"
-echo "payment_id=$PAYMENT_ID"
-
-# Output for GitHub Actions
-if [ -n "${GITHUB_OUTPUT:-}" ]; then
- echo "gateway_url=${GATEWAY_URL}" >> "$GITHUB_OUTPUT"
- echo "payment_id=${PAYMENT_ID}" >> "$GITHUB_OUTPUT"
-fi
diff --git a/wallets/rn_cli_wallet/AGENTS.md b/wallets/rn_cli_wallet/AGENTS.md
index 0becde90..1e56ad7f 100644
--- a/wallets/rn_cli_wallet/AGENTS.md
+++ b/wallets/rn_cli_wallet/AGENTS.md
@@ -183,9 +183,17 @@ The app uses standardized `testID` props for Maestro E2E testing. These IDs are
- `pay-*`: WalletConnect Pay flow elements (e.g., `pay-merchant-info`, `pay-button-pay`, `pay-result-success-icon`)
### Test Files
-- `.maestro/web/pay_confirm.yaml`: WalletConnect Pay end-to-end flow (POS → Wallet → POS)
-- `.maestro/native/`: Native dapp-to-wallet flows (connect, sign, reject)
-- `.maestro/web/`: Web-to-wallet flows (connect, sign, reject)
+- `.maestro/pay_single_option_nokyc.yaml`: Single payment option, no KYC — goes straight to review screen
+- `.maestro/pay_multiple_options_nokyc.yaml`: Multiple payment options, no KYC — option selection then review
+- `.maestro/pay_multiple_options_kyc.yaml`: Multiple payment options with KYC — option selection, webview KYC flow, then review
+- `.maestro/flows/pay_open_and_paste_url.yaml`: Shared sub-flow — opens wallet, pastes payment URL, waits for merchant info
+- `.maestro/flows/pay_confirm_and_verify.yaml`: Shared sub-flow — taps Pay, verifies success screen
+- `.maestro/scripts/create-payment.js`: Creates a payment via the WalletConnect Pay API (called via `runScript`)
+
+### Running Pay Tests Locally
+```bash
+maestro test --env APP_ID=com.walletconnect.web3wallet.rnsample.internal --env WPAY_CUSTOMER_KEY_SINGLE_NOKYC="" --env WPAY_MERCHANT_ID_SINGLE_NOKYC="" --env WPAY_CUSTOMER_KEY_MULTI_KYC="" --env WPAY_MERCHANT_ID_MULTI_KYC="" --env WPAY_CUSTOMER_KEY_MULTI_NOKYC="" --env WPAY_MERCHANT_ID_MULTI_NOKYC="" --include-tags pay .maestro/
+```
### Dynamic App ID
Maestro tests use `${APP_ID}` env variable instead of hardcoded bundle IDs, enabling reuse across wallet platforms. Pass via `--env APP_ID=` when running tests.
diff --git a/wallets/rn_cli_wallet/README.md b/wallets/rn_cli_wallet/README.md
index 7223efd6..9861ccd2 100644
--- a/wallets/rn_cli_wallet/README.md
+++ b/wallets/rn_cli_wallet/README.md
@@ -91,14 +91,14 @@ curl -fsSL "https://get.maestro.mobile.dev" | bash
### Running Tests Locally
-Run all tests with the `pay` tag:
+Run all pay tests:
```bash
-maestro test --env APP_ID=com.walletconnect.web3wallet.rnsample.internal --tags pay .maestro/
+maestro test --env APP_ID=com.walletconnect.web3wallet.rnsample.internal --env WPAY_CUSTOMER_KEY_SINGLE_NOKYC="" --env WPAY_MERCHANT_ID_SINGLE_NOKYC="" --env WPAY_CUSTOMER_KEY_MULTI_KYC="" --env WPAY_MERCHANT_ID_MULTI_KYC="" --env WPAY_CUSTOMER_KEY_MULTI_NOKYC="" --env WPAY_MERCHANT_ID_MULTI_NOKYC="" --include-tags pay .maestro/
```
Run a specific test:
```bash
-maestro test --env APP_ID=com.walletconnect.web3wallet.rnsample.internal .maestro/web/pay_confirm.yaml
+maestro test --env APP_ID=com.walletconnect.web3wallet.rnsample.internal --env WPAY_CUSTOMER_KEY_SINGLE_NOKYC="" --env WPAY_MERCHANT_ID_SINGLE_NOKYC="" .maestro/pay_single_option_nokyc.yaml
```
Run all tests:
diff --git a/wallets/rn_cli_wallet/ios/RNWeb3Wallet.xcodeproj/xcshareddata/xcschemes/RNWallet-Internal.xcscheme b/wallets/rn_cli_wallet/ios/RNWeb3Wallet.xcodeproj/xcshareddata/xcschemes/RNWallet-Internal.xcscheme
index f367b0e4..a5f18e2a 100644
--- a/wallets/rn_cli_wallet/ios/RNWeb3Wallet.xcodeproj/xcshareddata/xcschemes/RNWallet-Internal.xcscheme
+++ b/wallets/rn_cli_wallet/ios/RNWeb3Wallet.xcodeproj/xcshareddata/xcschemes/RNWallet-Internal.xcscheme
@@ -30,7 +30,7 @@
shouldAutocreateTestPlan = "YES">
;
textStyle?: StyleProp;
testID?: string;
+ accessibilityLabel?: string;
}
export function ActionButton({
@@ -38,6 +39,7 @@ export function ActionButton({
style,
textStyle,
testID,
+ accessibilityLabel,
}: ActionButtonProps) {
const Theme = useTheme();
@@ -71,6 +73,7 @@ export function ActionButton({
onPress={onPress}
disabled={disabled || loading || silentDisabled}
testID={testID}
+ accessibilityLabel={accessibilityLabel}
style={[
styles.container,
{ backgroundColor, borderColor },
diff --git a/wallets/rn_cli_wallet/src/components/Button.tsx b/wallets/rn_cli_wallet/src/components/Button.tsx
index a8a60a8d..6c977f07 100644
--- a/wallets/rn_cli_wallet/src/components/Button.tsx
+++ b/wallets/rn_cli_wallet/src/components/Button.tsx
@@ -9,6 +9,7 @@ interface ButtonProps {
disabled?: boolean;
hitSlop?: number | Insets;
testID?: string;
+ accessibilityLabel?: string;
}
export const Button: React.FC = ({
@@ -18,6 +19,7 @@ export const Button: React.FC = ({
disabled = false,
hitSlop,
testID,
+ accessibilityLabel,
}) => {
return (
= ({
enabled={!disabled}
hitSlop={hitSlop}
testID={testID}
+ accessibilityLabel={accessibilityLabel}
>
{children}
diff --git a/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/ReviewPaymentView.tsx b/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/ReviewPaymentView.tsx
index 5819ade7..45be2a9a 100644
--- a/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/ReviewPaymentView.tsx
+++ b/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/ReviewPaymentView.tsx
@@ -53,6 +53,8 @@ export function ReviewPaymentView({
styles.item,
{ backgroundColor: Theme['foreground-primary'] },
]}
+ testID={`pay-review-token-${selectedOption.amount.display?.networkName?.toLowerCase() || ''}`}
+ accessibilityLabel={selectedOption.amount.display?.networkName?.toLowerCase() || ''}
>
Pay with
@@ -87,6 +89,7 @@ export function ReviewPaymentView({
disabled={isSigningPayment || isLoadingActions}
fullWidth
testID="pay-button-pay"
+ accessibilityLabel={`Pay ${currencySymbol}${payAmount}`}
>
{`Pay ${currencySymbol}${payAmount}`}
diff --git a/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/SelectOptionView.tsx b/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/SelectOptionView.tsx
index a838ad97..aebcd8d6 100644
--- a/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/SelectOptionView.tsx
+++ b/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/SelectOptionView.tsx
@@ -27,6 +27,7 @@ const OPTION_HEIGHT = 64;
interface OptionItemProps {
option: PaymentOption;
+ index: number;
isSelected: boolean;
hasCollectData: boolean;
onSelect: (option: PaymentOption) => void;
@@ -36,6 +37,7 @@ const ANIMATION_DURATION = 250;
function OptionItem({
option,
+ index,
isSelected,
hasCollectData,
onSelect,
@@ -95,7 +97,12 @@ function OptionItem({
);
return (
-