diff --git a/maestro/pay-tests/.maestro/flows/pay_confirm_and_verify.yaml b/maestro/pay-tests/.maestro/flows/pay_confirm_and_verify.yaml
new file mode 100644
index 0000000..92e9adf
--- /dev/null
+++ b/maestro/pay-tests/.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-success"
diff --git a/maestro/pay-tests/.maestro/flows/pay_open_and_paste_url.yaml b/maestro/pay-tests/.maestro/flows/pay_open_and_paste_url.yaml
new file mode 100644
index 0000000..8d1871e
--- /dev/null
+++ b/maestro/pay-tests/.maestro/flows/pay_open_and_paste_url.yaml
@@ -0,0 +1,43 @@
+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
+
+# Wait for the app to fully load before interacting
+- extendedWaitUntil:
+ visible:
+ id: "button-scan"
+ timeout: 15000
+
+# 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
+
+- extendedWaitUntil:
+ visible:
+ id: "button-submit-url"
+ timeout: 10000
+
+- tapOn:
+ id: "button-submit-url"
diff --git a/maestro/pay-tests/.maestro/flows/pay_open_via_deeplink.yaml b/maestro/pay-tests/.maestro/flows/pay_open_via_deeplink.yaml
new file mode 100644
index 0000000..93ded1b
--- /dev/null
+++ b/maestro/pay-tests/.maestro/flows/pay_open_via_deeplink.yaml
@@ -0,0 +1,9 @@
+appId: ${APP_ID}
+---
+# Shared flow: Open payment URL via deep link
+# Requires: ${output.gateway_url} to be set by a prior runScript step
+
+# Stop app then open via deeplink so openLink is the launch intent.
+- stopApp:
+ appId: ${APP_ID}
+- openLink: ${output.gateway_url}
diff --git a/maestro/pay-tests/.maestro/pay_cancel_from_kyc.yaml b/maestro/pay-tests/.maestro/pay_cancel_from_kyc.yaml
new file mode 100644
index 0000000..5e57391
--- /dev/null
+++ b/maestro/pay-tests/.maestro/pay_cancel_from_kyc.yaml
@@ -0,0 +1,85 @@
+appId: ${APP_ID}
+name: WalletConnect Pay - Cancel from KYC Webview
+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 Cancel from KYC"
+
+# Open wallet, paste payment URL
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Wait for payment options to load
+- extendedWaitUntil:
+ visible:
+ id: "pay-merchant-info"
+ timeout: 15000
+
+# Verify first option is pre-selected
+- assertVisible:
+ id: "pay-option-0-selected"
+
+# Tap Continue to go to collectData webview
+- tapOn:
+ id: "pay-button-continue"
+
+# Wait for KYC webview to load
+- extendedWaitUntil:
+ visible: "Add your personal details"
+ timeout: 30000
+
+# Cancel the payment server-side while in KYC webview
+- runScript:
+ file: scripts/cancel-payment.js
+ env:
+ WPAY_CUSTOMER_KEY: ${WPAY_CUSTOMER_KEY_MULTI_KYC}
+ WPAY_MERCHANT_ID: ${WPAY_MERCHANT_ID_MULTI_KYC}
+ PAYMENT_ID: ${output.payment_id}
+
+# Complete KYC form (data is autocompleted, just tap Add)
+- tapOn: "Add"
+
+# Retry tap on "Add" if "Confirm your details" doesn't appear (webview can be slow)
+- runFlow:
+ when:
+ notVisible: "Confirm your details"
+ commands:
+ - tapOn: "Add"
+
+# Confirm your details popup
+- 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"
+
+# Wait for result screen
+- extendedWaitUntil:
+ visible:
+ id: "pay-result-container"
+ timeout: 30000
+
+# Verify cancelled icon
+- assertVisible:
+ id: "pay-result-cancelled-icon"
+
+# Verify action button
+- assertVisible:
+ id: "pay-button-result-action-cancelled"
+
+# Dismiss
+- tapOn:
+ id: "pay-button-result-action-cancelled"
+
+- stopRecording
diff --git a/maestro/pay-tests/.maestro/pay_cancel_from_review.yaml b/maestro/pay-tests/.maestro/pay_cancel_from_review.yaml
new file mode 100644
index 0000000..970a852
--- /dev/null
+++ b/maestro/pay-tests/.maestro/pay_cancel_from_review.yaml
@@ -0,0 +1,61 @@
+appId: ${APP_ID}
+name: WalletConnect Pay - Cancel from Review Screen
+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 Cancel from Review"
+
+# Open wallet, paste payment URL
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Wait for payment options to load
+- extendedWaitUntil:
+ visible:
+ id: "pay-merchant-info"
+ timeout: 15000
+
+# Single option auto-selects — verify review screen
+- extendedWaitUntil:
+ visible:
+ id: "pay-button-pay"
+ timeout: 10000
+
+# Cancel the payment server-side while on review screen
+- runScript:
+ file: scripts/cancel-payment.js
+ env:
+ WPAY_CUSTOMER_KEY: ${WPAY_CUSTOMER_KEY_SINGLE_NOKYC}
+ WPAY_MERCHANT_ID: ${WPAY_MERCHANT_ID_SINGLE_NOKYC}
+ PAYMENT_ID: ${output.payment_id}
+
+# Tap Pay — payment was cancelled, should show cancelled result
+- tapOn:
+ id: "pay-button-pay"
+
+# Wait for result screen
+- extendedWaitUntil:
+ visible:
+ id: "pay-result-container"
+ timeout: 30000
+
+# Verify cancelled icon
+- assertVisible:
+ id: "pay-result-cancelled-icon"
+
+# Verify action button
+- assertVisible:
+ id: "pay-button-result-action-cancelled"
+
+# Dismiss
+- tapOn:
+ id: "pay-button-result-action-cancelled"
+
+- stopRecording
diff --git a/maestro/pay-tests/.maestro/pay_cancelled.yaml b/maestro/pay-tests/.maestro/pay_cancelled.yaml
new file mode 100644
index 0000000..b2144e3
--- /dev/null
+++ b/maestro/pay-tests/.maestro/pay_cancelled.yaml
@@ -0,0 +1,44 @@
+appId: ${APP_ID}
+name: WalletConnect Pay - Cancelled Payment
+tags:
+ - pay
+---
+# Create a fresh payment, then cancel it immediately via API
+- runScript:
+ file: scripts/create-payment.js
+ env:
+ WPAY_CUSTOMER_KEY: ${WPAY_CUSTOMER_KEY_SINGLE_NOKYC}
+ WPAY_MERCHANT_ID: ${WPAY_MERCHANT_ID_SINGLE_NOKYC}
+
+- runScript:
+ file: scripts/cancel-payment.js
+ env:
+ WPAY_CUSTOMER_KEY: ${WPAY_CUSTOMER_KEY_SINGLE_NOKYC}
+ WPAY_MERCHANT_ID: ${WPAY_MERCHANT_ID_SINGLE_NOKYC}
+ PAYMENT_ID: ${output.payment_id}
+
+- startRecording: "WalletConnect Pay Cancelled Payment"
+
+# Open wallet, paste payment URL
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Payment is cancelled — app shows error result screen directly
+- extendedWaitUntil:
+ visible:
+ id: "pay-result-container"
+ timeout: 15000
+
+# Verify cancelled icon is shown
+- assertVisible:
+ id: "pay-result-cancelled-icon"
+
+# Verify result action button is visible
+- assertVisible:
+ id: "pay-button-result-action-cancelled"
+
+# Dismiss the error dialog
+- tapOn:
+ id: "pay-button-result-action-cancelled"
+
+- stopRecording
diff --git a/maestro/pay-tests/.maestro/pay_double_scan.yaml b/maestro/pay-tests/.maestro/pay_double_scan.yaml
new file mode 100644
index 0000000..c25efb6
--- /dev/null
+++ b/maestro/pay-tests/.maestro/pay_double_scan.yaml
@@ -0,0 +1,81 @@
+appId: ${APP_ID}
+name: WalletConnect Pay - Double Scan Same Payment
+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 Double Scan"
+
+# === First scan: complete the payment normally ===
+
+# Open wallet, paste payment URL
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Wait for payment options to load
+- extendedWaitUntil:
+ visible:
+ id: "pay-merchant-info"
+ timeout: 15000
+
+# Single option auto-selects — verify pay button
+- extendedWaitUntil:
+ visible:
+ id: "pay-button-pay"
+ timeout: 10000
+- copyTextFrom:
+ id: "pay-button-pay"
+- assertTrue:
+ condition: "${maestro.copiedText == 'Pay $0.01'}"
+
+# Tap Pay, verify success, tap "Got it!"
+- runFlow:
+ file: flows/pay_confirm_and_verify.yaml
+
+# === Second scan: re-open the same gateway URL ===
+
+# Tap scan button to open scanner
+- tapOn:
+ id: "button-scan"
+
+# Type the same payment URL
+- 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 error result screen (payment already completed)
+- extendedWaitUntil:
+ visible:
+ id: "pay-result-container"
+ timeout: 20000
+
+# Verify generic error icon is shown (not success)
+- assertVisible:
+ id: "pay-result-error-icon"
+
+# Verify result action button is visible
+- assertVisible:
+ id: "pay-button-result-action-generic"
+
+# Dismiss the error dialog
+- tapOn:
+ id: "pay-button-result-action-generic"
+
+- stopRecording
diff --git a/maestro/pay-tests/.maestro/pay_expired_link.yaml b/maestro/pay-tests/.maestro/pay_expired_link.yaml
new file mode 100644
index 0000000..719475e
--- /dev/null
+++ b/maestro/pay-tests/.maestro/pay_expired_link.yaml
@@ -0,0 +1,33 @@
+appId: ${APP_ID}
+name: WalletConnect Pay - Expired Link
+tags:
+ - pay
+---
+# Use a hardcoded expired payment URL (no API call needed)
+- evalScript: ${output.gateway_url = 'https://pay.walletconnect.com/?pid=pay_b8a2ecc101KNHRNWXD2VF8SGZDS7WK19ZA'}
+
+- startRecording: "WalletConnect Pay Expired Link"
+
+# Open wallet, paste payment URL
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Payment is expired — app shows error result screen directly
+- extendedWaitUntil:
+ visible:
+ id: "pay-result-container"
+ timeout: 15000
+
+# Verify expired icon is shown
+- assertVisible:
+ id: "pay-result-expired-icon"
+
+# Verify result action button is visible
+- assertVisible:
+ id: "pay-button-result-action-expired"
+
+# Dismiss the error dialog
+- tapOn:
+ id: "pay-button-result-action-expired"
+
+- stopRecording
diff --git a/maestro/pay-tests/.maestro/pay_insufficient_funds.yaml b/maestro/pay-tests/.maestro/pay_insufficient_funds.yaml
new file mode 100644
index 0000000..f71a25c
--- /dev/null
+++ b/maestro/pay-tests/.maestro/pay_insufficient_funds.yaml
@@ -0,0 +1,38 @@
+appId: ${APP_ID}
+name: WalletConnect Pay - Insufficient Funds
+tags:
+ - pay
+---
+# Create payment with amount exceeding wallet balance ($9.99)
+- runScript:
+ file: scripts/create-payment.js
+ env:
+ WPAY_CUSTOMER_KEY: ${WPAY_CUSTOMER_KEY_SINGLE_NOKYC}
+ WPAY_MERCHANT_ID: ${WPAY_MERCHANT_ID_SINGLE_NOKYC}
+ WPAY_AMOUNT: "999"
+
+- startRecording: "WalletConnect Pay Insufficient Funds"
+
+# Open wallet, paste payment URL
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# No options available — app jumps directly to error result screen
+- extendedWaitUntil:
+ visible:
+ id: "pay-result-container"
+ timeout: 15000
+
+# Verify insufficient funds icon is shown
+- assertVisible:
+ id: "pay-result-insufficient-funds-icon"
+
+# Verify result action button is visible
+- assertVisible:
+ id: "pay-button-result-action-insufficient_funds"
+
+# Dismiss the error dialog
+- tapOn:
+ id: "pay-button-result-action-insufficient_funds"
+
+- stopRecording
diff --git a/maestro/pay-tests/.maestro/pay_kyc_back_navigation.yaml b/maestro/pay-tests/.maestro/pay_kyc_back_navigation.yaml
new file mode 100644
index 0000000..7f0b063
--- /dev/null
+++ b/maestro/pay-tests/.maestro/pay_kyc_back_navigation.yaml
@@ -0,0 +1,66 @@
+appId: ${APP_ID}
+name: WalletConnect Pay - KYC Header Buttons
+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 KYC Header Buttons"
+
+# Open wallet, paste payment URL
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Wait for payment options to load
+- extendedWaitUntil:
+ visible:
+ id: "pay-merchant-info"
+ timeout: 15000
+
+# Verify first option is pre-selected
+- assertVisible:
+ id: "pay-option-0-selected"
+
+# Tap Continue to go to collectData webview
+- tapOn:
+ id: "pay-button-continue"
+
+# Wait for KYC webview to load
+- extendedWaitUntil:
+ visible: "Add your personal details"
+ timeout: 30000
+
+# Verify the close button (X) is visible in the header
+- assertVisible:
+ id: "pay-button-close"
+
+# Verify the back button is visible
+- assertVisible:
+ id: "pay-button-back"
+
+# Tap back to return to option selection
+- tapOn:
+ id: "pay-button-back"
+
+# Verify we're back at the selectOption view
+- extendedWaitUntil:
+ visible:
+ id: "pay-option-0-selected"
+ timeout: 10000
+
+# Dismiss the modal
+- tapOn:
+ id: "pay-button-close"
+
+# Verify modal is dismissed and we're back at the main wallet screen
+- extendedWaitUntil:
+ visible:
+ id: "button-scan"
+ timeout: 10000
+
+- stopRecording
diff --git a/maestro/pay-tests/.maestro/pay_multiple_options_kyc.yaml b/maestro/pay-tests/.maestro/pay_multiple_options_kyc.yaml
new file mode 100644
index 0000000..2e9a69b
--- /dev/null
+++ b/maestro/pay-tests/.maestro/pay_multiple_options_kyc.yaml
@@ -0,0 +1,101 @@
+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
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Wait for payment options to load
+- extendedWaitUntil:
+ visible:
+ id: "pay-merchant-info"
+ timeout: 15000
+
+# 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: 30000
+
+# Data is autocompleted, just tap Add
+- tapOn: "Add"
+
+# Retry tap on "Add" if "Confirm your details" doesn't appear (webview can be slow)
+- runFlow:
+ when:
+ notVisible: "Confirm your details"
+ commands:
+ - tapOn: "Add"
+
+# Confirm your details popup
+- 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-tests/.maestro/pay_multiple_options_nokyc.yaml b/maestro/pay-tests/.maestro/pay_multiple_options_nokyc.yaml
new file mode 100644
index 0000000..1e73d85
--- /dev/null
+++ b/maestro/pay-tests/.maestro/pay_multiple_options_nokyc.yaml
@@ -0,0 +1,76 @@
+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
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Wait for payment options to load
+- extendedWaitUntil:
+ visible:
+ id: "pay-merchant-info"
+ timeout: 15000
+
+# 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-tests/.maestro/pay_single_option_nokyc.yaml b/maestro/pay-tests/.maestro/pay_single_option_nokyc.yaml
new file mode 100644
index 0000000..63cd064
--- /dev/null
+++ b/maestro/pay-tests/.maestro/pay_single_option_nokyc.yaml
@@ -0,0 +1,47 @@
+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
+- runFlow:
+ file: flows/pay_open_and_paste_url.yaml
+
+# Wait for payment options to load
+- extendedWaitUntil:
+ visible:
+ id: "pay-merchant-info"
+ timeout: 15000
+
+# 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
+
+# Verify payment dialog is dismissed and we're back at the main wallet screen
+- extendedWaitUntil:
+ visible:
+ id: "button-scan"
+ timeout: 10000
+
+- stopRecording
diff --git a/maestro/pay-tests/.maestro/pay_single_option_nokyc_deeplink.yaml b/maestro/pay-tests/.maestro/pay_single_option_nokyc_deeplink.yaml
new file mode 100644
index 0000000..d7e2953
--- /dev/null
+++ b/maestro/pay-tests/.maestro/pay_single_option_nokyc_deeplink.yaml
@@ -0,0 +1,41 @@
+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
+
+# Wait for payment options to load
+- extendedWaitUntil:
+ visible:
+ id: "pay-merchant-info"
+ timeout: 15000
+
+# 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-tests/.maestro/scripts/cancel-payment.js b/maestro/pay-tests/.maestro/scripts/cancel-payment.js
new file mode 100644
index 0000000..dcd548b
--- /dev/null
+++ b/maestro/pay-tests/.maestro/scripts/cancel-payment.js
@@ -0,0 +1,21 @@
+// Cancels a WalletConnect Pay payment via the API.
+// Expects WPAY_CUSTOMER_KEY, WPAY_MERCHANT_ID, and PAYMENT_ID env vars from Maestro.
+
+if (typeof WPAY_CUSTOMER_KEY === 'undefined') throw new Error('Missing env var: WPAY_CUSTOMER_KEY');
+if (typeof WPAY_MERCHANT_ID === 'undefined') throw new Error('Missing env var: WPAY_MERCHANT_ID');
+if (typeof PAYMENT_ID === 'undefined') throw new Error('Missing env var: PAYMENT_ID');
+
+var response = http.post('https://api.pay.walletconnect.com/v1/payments/' + PAYMENT_ID + '/cancel', {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Api-Key': WPAY_CUSTOMER_KEY,
+ 'Merchant-Id': WPAY_MERCHANT_ID,
+ },
+ body: JSON.stringify({}),
+});
+
+if (response.status < 200 || response.status >= 300) {
+ throw new Error('Cancel API returned HTTP ' + response.status + ': ' + response.body);
+}
+
+console.log('Payment cancelled: ' + PAYMENT_ID);
diff --git a/maestro/pay-tests/.maestro/scripts/create-payment.js b/maestro/pay-tests/.maestro/scripts/create-payment.js
new file mode 100644
index 0000000..1fc11ae
--- /dev/null
+++ b/maestro/pay-tests/.maestro/scripts/create-payment.js
@@ -0,0 +1,32 @@
+// Creates a WalletConnect Pay payment via the API.
+// Expects WPAY_CUSTOMER_KEY and WPAY_MERCHANT_ID env vars from Maestro.
+// Sets output.gateway_url and output.payment_id for use in subsequent flow steps.
+
+if (typeof WPAY_CUSTOMER_KEY === 'undefined') throw new Error('Missing env var: WPAY_CUSTOMER_KEY');
+if (typeof WPAY_MERCHANT_ID === 'undefined') throw new Error('Missing env var: WPAY_MERCHANT_ID');
+
+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: typeof WPAY_AMOUNT !== 'undefined' ? WPAY_AMOUNT : '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;
+output.payment_id = data.paymentId;
diff --git a/maestro/pay-tests/README.md b/maestro/pay-tests/README.md
new file mode 100644
index 0000000..c68c397
--- /dev/null
+++ b/maestro/pay-tests/README.md
@@ -0,0 +1,274 @@
+# Maestro Pay Tests
+
+Shared [Maestro](https://maestro.mobile.dev/) E2E test flows for **WalletConnect Pay**. These tests verify the full payment lifecycle — from scanning a payment link to confirming on-chain — and are designed to run on any wallet that integrates WalletConnect Pay (React Native, Kotlin, Swift, Flutter).
+
+## How It Works
+
+This action copies shared Maestro test flows and helper scripts into your workspace. Your CI workflow then runs `maestro test` against your built app. The flows use accessibility IDs (testIDs) to interact with UI elements, so every wallet platform must implement the same set of IDs on the corresponding components.
+
+## Prerequisites
+
+- **Maestro CLI** installed — use [`WalletConnect/actions/maestro/setup`](../setup) action
+- **App built and installed** on simulator/emulator
+- **Test mode enabled** in the wallet app (to expose the URL input field)
+
+## Required TestIDs
+
+Every wallet platform must add these accessibility identifiers to the corresponding UI elements. The tests will fail if any are missing.
+
+### Scanner / URL Entry
+
+| TestID | Element | Description |
+|---|---|---|
+| `button-scan` | Scan button on home screen | Opens the scanner/QR modal |
+| `input-paste-url` | Text input in scan modal | For pasting payment URLs (**test mode only** — see below) |
+| `button-submit-url` | Submit button in scan modal | Submits the pasted URL (**test mode only**) |
+
+### Payment Modal — Header
+
+| TestID | Element | Description |
+|---|---|---|
+| `pay-button-back` | Back arrow button | Returns to previous step |
+| `pay-button-close` | Close (X) button | Dismisses the payment modal |
+
+### Payment Modal — Merchant Info & Loading
+
+| TestID | Element | Description |
+|---|---|---|
+| `pay-merchant-info` | Merchant display | Shows merchant name and payment amount |
+| `pay-loading-message` | Loading text | Shown during payment processing |
+
+### Payment Modal — Option Selection
+
+| TestID | Element | Description |
+|---|---|---|
+| `pay-option-{index}` | Payment option (unselected) | 0-based index from the payment options array |
+| `pay-option-{index}-selected` | Payment option (selected) | Same element when selected |
+| `pay-info-required-badge` | "Info required" badge | Shown on options that require KYC |
+| `pay-button-info` | Info (?) button in header | Explains KYC requirement |
+| `pay-button-continue` | Continue button | Proceeds after selecting a payment option |
+
+### Payment Modal — Review Screen
+
+| TestID | Element | Description |
+|---|---|---|
+| `pay-review-token-{networkName}` | Token/network display | Dynamic — lowercase network name (e.g. `pay-review-token-base`) |
+| `pay-button-pay` | Pay button | Confirms and submits the payment |
+
+### Payment Modal — Result Screen
+
+| TestID | Element | Description |
+|---|---|---|
+| `pay-result-container` | Result screen wrapper | Container for the result view |
+| `pay-result-success-icon` | Success icon | Checkmark shown on successful payment |
+| `pay-result-insufficient-funds-icon` | Insufficient funds icon | Shown when wallet balance is too low |
+| `pay-result-expired-icon` | Expired icon | Shown for expired payment links |
+| `pay-result-cancelled-icon` | Cancelled icon | Shown for cancelled payments |
+| `pay-result-error-icon` | Generic error icon | Shown for other errors (e.g. already completed) |
+| `pay-button-result-action-success` | "Got it!" button (success) | Dismisses the success result |
+| `pay-button-result-action-insufficient_funds` | Action button (insufficient funds) | Dismisses the insufficient funds error |
+| `pay-button-result-action-expired` | Action button (expired) | Dismisses the expired error |
+| `pay-button-result-action-cancelled` | Action button (cancelled) | Dismisses the cancelled error |
+| `pay-button-result-action-generic` | Action button (generic error) | Dismisses the generic error |
+
+### Dynamic TestID Patterns
+
+Some testIDs include dynamic values:
+
+- **`pay-option-{index}`** / **`pay-option-{index}-selected`** — `index` is 0-based from the payment options array. Example: `pay-option-0`, `pay-option-1-selected`
+- **`pay-review-token-{networkName}`** — lowercase network name. Example: `pay-review-token-base`, `pay-review-token-ethereum`
+- **`pay-button-result-action-{type}`** — one of: `success`, `insufficient_funds`, `expired`, `cancelled`, `generic`
+
+## Test Input Field Requirement
+
+Each wallet must add a **text input field** and **submit button** inside the scan/QR modal. This is required for Maestro to bypass camera/QR scanning and submit payment URLs directly.
+
+**Important:** This input should only be visible when a test mode flag is enabled (e.g. `ENV_TEST_MODE=true`). It should never appear in production builds.
+
+### Reference Implementation (React Native)
+
+From `ScannerOptionsModal.tsx` in the React Native wallet sample:
+
+```tsx
+import Config from 'react-native-config';
+
+const showTestInput = Config.ENV_TEST_MODE === 'true';
+
+// Inside the modal component's render:
+{showTestInput && (
+
+
+
+
+)}
+```
+
+The key points for any platform:
+1. Gate visibility behind a test/debug build flag
+2. Use `input-paste-url` as the accessibility ID for the text input
+3. Use `button-submit-url` as the accessibility ID for the submit button
+4. On submit, pass the URL to the same handler that processes scanned QR codes or deep links
+
+## Required Secrets
+
+These secrets must be configured in your repository for the tests to create and manipulate payments:
+
+| Secret | Description |
+|---|---|
+| `WPAY_CUSTOMER_KEY_SINGLE_NOKYC` | API key for single-option, no-KYC merchant |
+| `WPAY_MERCHANT_ID_SINGLE_NOKYC` | Merchant ID for single-option, no-KYC merchant |
+| `WPAY_CUSTOMER_KEY_MULTI_NOKYC` | API key for multi-option, no-KYC merchant |
+| `WPAY_MERCHANT_ID_MULTI_NOKYC` | Merchant ID for multi-option, no-KYC merchant |
+| `WPAY_CUSTOMER_KEY_MULTI_KYC` | API key for multi-option, KYC-required merchant |
+| `WPAY_MERCHANT_ID_MULTI_KYC` | Merchant ID for multi-option, KYC-required merchant |
+
+Each merchant pair represents a different test configuration. The tests use these to create payments with specific option counts and KYC requirements.
+
+## Test Catalog
+
+| Flow | Description | Merchant Config |
+|---|---|---|
+| `pay_single_option_nokyc` | Happy path: single payment option, no KYC | SINGLE_NOKYC |
+| `pay_single_option_nokyc_deeplink` | Same as above but opened via deep link | SINGLE_NOKYC |
+| `pay_multiple_options_nokyc` | Select between multiple options, no KYC | MULTI_NOKYC |
+| `pay_multiple_options_kyc` | Multiple options with KYC webview | MULTI_KYC |
+| `pay_cancel_from_review` | Server-side cancellation on review screen | SINGLE_NOKYC |
+| `pay_cancel_from_kyc` | Server-side cancellation during KYC | MULTI_KYC |
+| `pay_kyc_back_navigation` | Back/close button navigation in KYC | MULTI_KYC |
+| `pay_insufficient_funds` | Payment amount exceeds wallet balance | SINGLE_NOKYC |
+| `pay_double_scan` | Re-scan same QR after completion | SINGLE_NOKYC |
+| `pay_expired_link` | Hardcoded expired payment URL | None (hardcoded) |
+| `pay_cancelled` | Hardcoded cancelled payment URL | None (hardcoded) |
+
+All flows are tagged with `pay` for filtering via `--include-tags`.
+
+## Deep Link Support
+
+The `pay_single_option_nokyc_deeplink` test uses Maestro's `openLink` command to open a `https://pay.walletconnect.com` URL. Your wallet must be configured to handle these URLs as deep links / universal links for this test to work.
+
+## Local Development
+
+For running tests locally during development. Requires [Maestro CLI](https://maestro.mobile.dev/) installed on your machine.
+
+### Setup
+
+1. **Create your secrets file** (one-time):
+ ```bash
+ cp .env.maestro.example .env.maestro
+ # Fill in the WPAY_* values (get them from your team or the WalletConnect Pay dashboard)
+ ```
+
+2. **Run tests** (auto-downloads flows if not present):
+ ```bash
+ ./scripts/run-maestro-pay-tests.sh
+ ```
+
+That's it. The script will automatically download the shared test flows from this repo if they're not already present, load secrets from `.env.maestro`, and run all pay-tagged tests.
+
+### Other local commands
+
+```bash
+# Run a single test
+./scripts/run-maestro-pay-tests.sh .maestro/pay_cancelled.yaml
+
+# Re-download flows (e.g. after an update)
+./scripts/setup-maestro-pay-tests.sh
+
+# Download flows from a specific branch
+./scripts/setup-maestro-pay-tests.sh feat/my-branch
+```
+
+## CI Usage
+
+### iOS (composite action)
+
+```yaml
+steps:
+ - uses: actions/checkout@v4
+
+ # ... your platform-specific build steps ...
+
+ - name: Copy shared Pay test flows
+ uses: WalletConnect/actions/maestro/pay-tests@main
+
+ - name: Install Maestro
+ uses: WalletConnect/actions/maestro/setup@main
+
+ # ... boot simulator, install app ...
+
+ - name: Run Pay E2E tests
+ uses: WalletConnect/actions/maestro/run@main
+ with:
+ app-id: com.example.wallet.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 }}
+```
+
+### Android (emulator runner)
+
+The `reactivecircus/android-emulator-runner` action runs everything inside a `script:` block, which cannot call composite actions. Use `maestro/pay-tests` and `maestro/setup` *before* the emulator step, then run `maestro test` inline:
+
+```yaml
+steps:
+ - uses: actions/checkout@v4
+
+ # ... your platform-specific build steps ...
+
+ - name: Copy shared Pay test flows
+ uses: WalletConnect/actions/maestro/pay-tests@main
+
+ - name: Install Maestro
+ uses: WalletConnect/actions/maestro/setup@main
+
+ - name: Run E2E tests on Android Emulator
+ id: maestro
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: 34
+ arch: x86_64
+ script: |
+ adb install path/to/app.apk
+ $HOME/.maestro/bin/maestro test \
+ --env APP_ID="com.example.wallet.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 maestro-artifacts \
+ --debug-output maestro-artifacts \
+ .maestro/
+
+ - name: Upload Maestro artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: maestro-android-artifacts
+ path: |
+ maestro-artifacts/
+ retention-days: 14
+```
diff --git a/maestro/pay-tests/action.yml b/maestro/pay-tests/action.yml
new file mode 100644
index 0000000..1bb4a08
--- /dev/null
+++ b/maestro/pay-tests/action.yml
@@ -0,0 +1,40 @@
+name: Maestro Pay Tests
+description: Copy shared WalletConnect Pay E2E test flows into the consumer workspace
+
+inputs:
+ target-dir:
+ description: 'Directory to copy test flows into (relative to workspace root)'
+ required: false
+ default: '.maestro'
+
+outputs:
+ maestro-dir:
+ description: 'Absolute path to the directory containing the test flows'
+ value: ${{ steps.copy.outputs.maestro-dir }}
+
+runs:
+ using: composite
+ steps:
+ - name: Copy shared Maestro Pay test flows
+ id: copy
+ shell: bash
+ run: |
+ set -euo pipefail
+ ACTION_DIR="${{ github.action_path }}"
+ TARGET_DIR="${{ github.workspace }}/${{ inputs.target-dir }}"
+
+ mkdir -p "$TARGET_DIR/flows" "$TARGET_DIR/scripts"
+
+ # Copy root-level pay test flows
+ cp "$ACTION_DIR"/.maestro/pay_*.yaml "$TARGET_DIR/"
+
+ # Copy shared sub-flows
+ cp "$ACTION_DIR"/.maestro/flows/pay_*.yaml "$TARGET_DIR/flows/"
+
+ # Copy scripts
+ cp "$ACTION_DIR"/.maestro/scripts/*.js "$TARGET_DIR/scripts/"
+
+ echo "Copied Maestro Pay test flows to $TARGET_DIR"
+ ls -la "$TARGET_DIR"
+
+ echo "maestro-dir=$TARGET_DIR" >> "$GITHUB_OUTPUT"
diff --git a/maestro/run/action.yml b/maestro/run/action.yml
new file mode 100644
index 0000000..a9ea474
--- /dev/null
+++ b/maestro/run/action.yml
@@ -0,0 +1,69 @@
+name: Maestro Pay Test Run
+description: Execute Maestro Pay E2E tests with WalletConnect Pay secrets and upload artifacts
+
+inputs:
+ app-id:
+ description: 'Application bundle ID (iOS) or package name (Android)'
+ required: true
+ maestro-dir:
+ description: 'Directory containing Maestro test flows'
+ required: false
+ default: '.maestro'
+ tags:
+ description: 'Maestro --include-tags value (comma-separated)'
+ required: false
+ default: 'pay'
+ wpay-customer-key-single-nokyc:
+ description: 'WalletConnect Pay Customer API key (single-offramp-option, no-KYC)'
+ required: true
+ wpay-merchant-id-single-nokyc:
+ description: 'WalletConnect Pay merchant ID (single-offramp-option, no-KYC)'
+ required: true
+ wpay-customer-key-multi-nokyc:
+ description: 'WalletConnect Pay Customer API key (multiple-offramp-options, no-KYC)'
+ required: true
+ wpay-merchant-id-multi-nokyc:
+ description: 'WalletConnect Pay merchant ID (multiple-offramp-options, no-KYC))'
+ required: true
+ wpay-customer-key-multi-kyc:
+ description: 'WalletConnect Pay Customer API key (multiple-offramp-options, KYC-required)'
+ required: true
+ wpay-merchant-id-multi-kyc:
+ description: 'WalletConnect Pay merchant ID (multiple-offramp-options, KYC-required)'
+ required: true
+ artifact-name:
+ description: 'Name for the uploaded artifact'
+ required: false
+ default: 'maestro-artifacts'
+
+runs:
+ using: composite
+ steps:
+ - name: Run Maestro tests
+ id: maestro
+ shell: bash
+ run: |
+ set -o pipefail
+ maestro test \
+ --env APP_ID="${{ inputs.app-id }}" \
+ --env WPAY_CUSTOMER_KEY_SINGLE_NOKYC="${{ inputs.wpay-customer-key-single-nokyc }}" \
+ --env WPAY_MERCHANT_ID_SINGLE_NOKYC="${{ inputs.wpay-merchant-id-single-nokyc }}" \
+ --env WPAY_CUSTOMER_KEY_MULTI_NOKYC="${{ inputs.wpay-customer-key-multi-nokyc }}" \
+ --env WPAY_MERCHANT_ID_MULTI_NOKYC="${{ inputs.wpay-merchant-id-multi-nokyc }}" \
+ --env WPAY_CUSTOMER_KEY_MULTI_KYC="${{ inputs.wpay-customer-key-multi-kyc }}" \
+ --env WPAY_MERCHANT_ID_MULTI_KYC="${{ inputs.wpay-merchant-id-multi-kyc }}" \
+ --include-tags "${{ inputs.tags }}" \
+ --test-output-dir maestro-artifacts \
+ --debug-output maestro-artifacts \
+ "${{ inputs.maestro-dir }}" 2>&1 | tee maestro-output.log
+
+ - name: Upload Maestro artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ inputs.artifact-name }}
+ path: |
+ maestro-artifacts/
+ maestro-output.log
+ if-no-files-found: warn
+ retention-days: 14
diff --git a/maestro/setup/action.yml b/maestro/setup/action.yml
new file mode 100644
index 0000000..ac65f91
--- /dev/null
+++ b/maestro/setup/action.yml
@@ -0,0 +1,27 @@
+name: Maestro Setup
+description: Install the Maestro CLI for mobile E2E testing
+
+inputs:
+ version:
+ description: 'Maestro version to install (e.g. 1.39.15). Leave empty for latest.'
+ required: false
+ default: ''
+
+runs:
+ using: composite
+ steps:
+ - name: Install Maestro CLI
+ shell: bash
+ run: |
+ set -euo pipefail
+ if command -v maestro &>/dev/null; then
+ echo "Maestro already installed: $(maestro --version)"
+ exit 0
+ fi
+
+ if [ -n "${{ inputs.version }}" ]; then
+ export MAESTRO_VERSION="${{ inputs.version }}"
+ fi
+
+ curl -fsSL "https://get.maestro.mobile.dev" | bash
+ echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"