ScreenSync MCP guide

Testing Android In-App Purchases with Claude Code and MCP

Automate Google Play Billing Library 7.x in-app purchases, subscriptions, pending transactions, and token acknowledgments on physical Android devices using Claude Code and ScreenSync MCP.

A laptop showing code connects via glowing data cables to a smartphone on a desk in a dark development office.

Automating in-app purchases and digital subscriptions represents the highest-stakes quality assurance challenge in mobile engineering. For mobile applications, in-app billing is not merely another feature; it is the core revenue engine and financial lifecycle of the software. A subtle defect in purchase acknowledgment can trigger immediate automatic refunds, lost customer trust, and severe account standing penalties on the Google Play Developer Console. Yet, across the mobile development ecosystem, in-app billing remains one of the least automated and most error-prone areas of software delivery. When an application invokes Google Play Billing Library 7.x to launch a purchase flow, the payment sheet that appears does not execute inside the application's process boundary. It inflates inside the operating system's privileged com.android.vending process—the Google Play Store app itself. Traditional in-process test runners like Espresso and Robolectric are structurally sandboxed to the host application's memory space, leaving them completely blind to the Google Play bottom sheet. Consequently, engineering teams either rely on fragile mock objects that fail to replicate real-world billing lifecycles or relegate purchase verification to tedious manual QA. By combining terminal-based AI coding agents like Claude Code with the Model Context Protocol (MCP) through ScreenSync MCP, mobile engineering teams can bridge the gap between application business logic and system-level purchase modals, executing deterministic, end-to-end billing tests on authentic physical Android hardware.

Key Takeaways

  • Traversing out-of-process purchase sheets: ScreenSync MCP extracts system-level accessibility node trees via get_ui_hierarchy, allowing Claude Code to inspect and interact with Google Play's com.android.vending bottom sheet without encountering in-process runner barriers.
  • Deterministic license test execution: Automate end-to-end purchasing flows using Google Play Console license test accounts, simulating instant approvals, card declines, and multi-day pending payment states deterministically on real devices.
  • The critical 3-day acknowledgment lifecycle: Validate that client applications and server backends acknowledge purchases within Google Play's mandatory three-day window via BillingClient.acknowledgePurchase to prevent automatic transaction refunds and entitlement revocation.
  • Subscription replacement mode testing: Programmatically verify complex subscription lifecycles, including base plan migrations, prorated upgrades (ReplacementMode.CHARGE_PRORATED_PRICE), downgrades, grace periods, and account holds.
  • Native MediaProjection visual verification: High-fidelity display frames stream from the physical Android phone to the workstation desktop hub at approximately 94 milliseconds median latency without consuming ADB bandwidth or requiring device rooting.
  • Unified cross-surface test automation: ScreenSync MCP combines 29 mobile and desktop tools (12 control_*, 3 os_*, 14 capture/telemetry tools) with 197 browser tools to verify mobile purchase dispatch and backend Google Cloud Pub/Sub webhook processing in a single automated session.

Why In-Process Test Suites Fail at Google Play Billing Sheets

In-process test suites like Espresso and Robolectric fail at Google Play Billing sheets because the payment bottom sheet executes inside Google Play Store's external com.android.vending process rather than the application's sandbox. Sandboxed test runners cannot traverse out-of-process system windows, while synthetic billing mocks bypass real Google Play server communication, token issuance, and acknowledgment lifecycles entirely.

To understand why automated in-app purchase testing has historically eluded continuous integration pipelines, one must examine the process architecture of Android and Google Play Services. When a developer compiles an Android application and executes a standard Espresso test runner via AndroidJUnitRunner, the test framework injects an instrumentation APK into the application's sandboxed virtual machine process. Espresso relies on direct access to the application's view hierarchy and main looper message queue to synchronize assertions with user interface rendering.

However, when the application calls BillingClient.launchBillingFlow(), the Play Billing client communicates with Google Play Services via inter-process communication (IPC) through Android Interface Definition Language (AIDL) interfaces. The operating system suspends the application's UI interactivity and inflates a floating modal bottom sheet managed entirely by Google Play:

Foreground Window: com.android.vending/com.google.android.finsky.billing.acquire.AcquireActivity
Application Process: com.example.screensync.billing (Suspended / Obscured)

Because Espresso's view matchers (onView(withId(R.id.button_buy))) are strictly confined to the application's decor view, Espresso cannot see or click any element within the com.android.vending window. Attempting to click the Google Play "1-tap buy" button throws an immediate NoMatchingViewException.

To work around this fundamental sandbox constraint, development teams typically resort to three suboptimal strategies, each introducing dangerous quality gaps:

  1. Synthetic Billing Client Mocks: Developers mock BillingClient using libraries like MockK or Mockito, intercepting queryProductDetailsAsync and injecting synthetic Purchase objects into PurchasesUpdatedListener. While this verifies internal state machine routing, it does not test Google Play Billing at all. Real-world edge cases—such as mismatched product IDs, out-of-date Google Play cache, missing license tester accounts, network dropouts during IPC, and invalid purchase signatures—are completely bypassed.
  2. Brittle UIAutomator Selector Scripts: Teams attempt to use UIAutomator to click the system sheet. However, Google Play periodically updates the layout, resource IDs, and copywriting of com.android.vending across different regions and Android OS releases. When Google changes a button from "Subscribe" to "Continue" or re-architects the payment options list, UIAutomator scripts break with UiObjectNotFoundException, creating immense maintenance overhead.
  3. Cloud Device Farm Lag: Third-party device farms struggle with in-app purchase automation because test Google accounts require ongoing two-factor authentication (2FA), device passkeys, and Google Play Services licensing that frequently expires or flags automated IP addresses as suspicious bot traffic.

ScreenSync MCP resolves this architectural hurdle by decoupling test execution from the application process sandbox. Operating as a Model Context Protocol (MCP) server running on your local workstation, ScreenSync MCP interfaces directly with the Android Debug Bridge (ADB) daemon and the system accessibility manager. When Claude Code requests get_ui_hierarchy, ScreenSync MCP dumps the authoritative node hierarchy of whatever window is currently active on screen—including com.android.vending. Claude Code can semantically identify purchase buttons, select payment instruments, and dispatch verified native touch gestures via control_tap without relying on fragile hardcoded selectors.

In-App Purchase Testing Dimension Mocked BillingClient (Unit Tests) In-Process Espresso + UIAutomator Cloud Device Farms (SauceLabs / BrowserStack) ScreenSync MCP + Claude Code (Physical Device)
System Payment Sheet Interaction Impossible; bypassed via in-memory mock callbacks Fragile; breaks when Google Play Store alters UI resource IDs Flaky; high network latency causes modal timeouts Native; semantic node inspection via get_ui_hierarchy
Real Google Play Token Generation False; uses hardcoded mock purchase tokens Inconsistent; depends on pre-configured cloud accounts Unstable; Google anti-bot systems flag remote farm IPs Authentic; generates real cryptographically signed Play tokens
Testing 3-Day Acknowledgment Logic Synthetic; cannot verify automatic Play Store auto-refund Complex; requires manual cron scripting Unsupported; cloud sessions terminate before refund triggers Deterministic; verifies acknowledgePurchase and refund hooks
Subscription Replacement Modes Simulated; fails to replicate Google Play proration math Brittle; navigating multiple subscription tiers fails High failure rate due to cached Google Play Store state Comprehensive; verifies prorated upgrades and base plans
Slow Test Card / Pending States Mocked; cannot simulate delayed Google Play webhooks Difficult; requires manual coordinator scripts High timeout risk on asynchronous cloud test queues Effortless; agent selects slow test card and monitors state
Visual Confirmation & Tearing None; headless execution Low frame rate screen capture with heavy ADB lag Compressed WebRTC video stream prone to blur Native MediaProjection mirror (~94 ms latency) via get_latest_screenshot

Google Play Billing Library 7.x Architecture: Flows, Tokens, and Acknowledgments

Google Play Billing Library 7.x structures in-app monetization around asynchronous product queries, billing flow launches, purchase updates, and mandatory token acknowledgments. Applications must acknowledge verified purchases within three days via BillingClient.acknowledgePurchase or Google Play automatically refunds the transaction, revokes the user's entitlement, and cancels the purchase state across server records.

Mastering automated testing for Google Play Billing requires a thorough grasp of the Play Billing Library 7.x state machine. Version 7.x enforces strict separation between one-time products (BillingClient.ProductType.INAPP) and recurring subscriptions (BillingClient.ProductType.SUBS), deprecating legacy SKU-based APIs in favor of structured product details and offer tokens.

The Play Billing Lifecycle

The standard Google Play billing transaction proceeds through five sequential phases:

  1. Client Connection & Query: The application initializes a BillingClient instance and invokes startConnection(). Once onBillingSetupFinished() returns BillingResponseCode.OK, the app queries available products using queryProductDetailsAsync() with a list of QueryProductDetailsParams.Product.
  2. Launching the Flow: When the user initiates a purchase, the app constructs BillingFlowParams using the retrieved ProductDetails (and specific offerToken for subscriptions) and calls launchBillingFlow(activity, params). This triggers Google Play to inflate the com.android.vending payment sheet.
  3. Purchases Updated Callback: The user completes or cancels the interaction in Google Play. Google Play Services delivers the asynchronous result to the application's registered PurchasesUpdatedListener.onPurchasesUpdated().
  4. Server Verification & Entitlement Provisioning: For secure transactions, the client sends the purchaseToken, orderId, and signature to the backend server. The backend queries the Google Play Developer API (purchases.products.get or purchases.subscriptionsv2.get) to verify purchase authenticity and provisions the entitlement in the database.
  5. Mandatory Purchase Acknowledgment: Once the entitlement is granted, the app (or backend) must acknowledge the purchase via BillingClient.acknowledgePurchase() or purchases.products.acknowledge. If a purchase is not acknowledged within 3 days (72 hours), Google Play automatically issues a full refund to the user and revokes the purchase token.

Production Kotlin Implementation

The following production-ready Kotlin class demonstrates a robust BillingManager implementing Google Play Billing Library 7.x with defensive acknowledgment and state emission:

package com.example.screensync.billing

import android.app.Activity
import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.android.billingclient.api.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

sealed class BillingUiState {
    object Idle : BillingUiState()
    object Loading : BillingUiState()
    data class ProductsLoaded(val products: List<ProductDetails>) : BillingUiState()
    data class PurchaseSuccess(val purchase: Purchase) : BillingUiState()
    data class PurchasePending(val purchase: Purchase) : BillingUiState()
    data class Error(val message: String, val responseCode: Int) : BillingUiState()
}

class BillingManager(
    private val context: Context,
    private val coroutineScope: CoroutineScope
) : PurchasesUpdatedListener, DefaultLifecycleObserver {

    private val _uiState = MutableStateFlow<BillingUiState>(BillingUiState.Idle)
    val uiState = _uiState.asStateFlow()

    private val billingClient: BillingClient = BillingClient.newBuilder(context)
        .setListener(this)
        .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
        .build()

    override fun onStart(owner: LifecycleOwner) {
        startBillingConnection()
    }

    override fun onDestroy(owner: LifecycleOwner) {
        if (billingClient.isReady) {
            billingClient.endConnection()
        }
    }

    fun startBillingConnection() {
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    queryAvailableProducts()
                } else {
                    _uiState.value = BillingUiState.Error(
                        "Billing setup failed: ${billingResult.debugMessage}",
                        billingResult.responseCode
                    )
                }
            }

            override fun onBillingServiceDisconnected() {
                // Retry connection with exponential backoff in production
                _uiState.value = BillingUiState.Error("Billing service disconnected", -1)
            }
        })
    }

    fun queryAvailableProducts() {
        coroutineScope.launch {
            _uiState.value = BillingUiState.Loading
            val productList = listOf(
                QueryProductDetailsParams.Product.newBuilder()
                    .setProductId("premium_upgrade_lifetime")
                    .setProductType(BillingClient.ProductType.INAPP)
                    .build(),
                QueryProductDetailsParams.Product.newBuilder()
                    .setProductId("pro_subscription_monthly")
                    .setProductType(BillingClient.ProductType.SUBS)
                    .build()
            )

            val params = QueryProductDetailsParams.newBuilder()
                .setProductList(productList)
                .build()

            val productDetailsResult = withContext(Dispatchers.IO) {
                billingClient.queryProductDetails(params)
            }

            if (productDetailsResult.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                _uiState.value = BillingUiState.ProductsLoaded(productDetailsResult.productDetailsList ?: emptyList())
            } else {
                _uiState.value = BillingUiState.Error(
                    "Product query failed: ${productDetailsResult.billingResult.debugMessage}",
                    productDetailsResult.billingResult.responseCode
                )
            }
        }
    }

    fun launchPurchase(activity: Activity, productDetails: ProductDetails, selectedOfferToken: String? = null) {
        val productDetailsParamsList = if (productDetails.productType == BillingClient.ProductType.SUBS) {
            val offerToken = selectedOfferToken 
                ?: productDetails.subscriptionOfferDetails?.firstOrNull()?.offerToken
                ?: return

            listOf(
                BillingFlowParams.ProductDetailsParams.newBuilder()
                    .setProductDetails(productDetails)
                    .setOfferToken(offerToken)
                    .build()
            )
        } else {
            listOf(
                BillingFlowParams.ProductDetailsParams.newBuilder()
                    .setProductDetails(productDetails)
                    .build()
            )
        }

        val flowParams = BillingFlowParams.newBuilder()
            .setProductDetailsParamsList(productDetailsParamsList)
            .build()

        billingClient.launchBillingFlow(activity, flowParams)
    }

    override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
        when (billingResult.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                purchases?.forEach { handlePurchase(it) }
            }
            BillingClient.BillingResponseCode.USER_CANCELED -> {
                _uiState.value = BillingUiState.Error("User canceled transaction", billingResult.responseCode)
            }
            else -> {
                _uiState.value = BillingUiState.Error(
                    "Purchase error: ${billingResult.debugMessage}",
                    billingResult.responseCode
                )
            }
        }
    }

    private fun handlePurchase(purchase: Purchase) {
        when (purchase.purchaseState) {
            Purchase.PurchaseState.PURCHASED -> {
                if (!purchase.isAcknowledged) {
                    coroutineScope.launch {
                        val acknowledgeParams = AcknowledgePurchaseParams.newBuilder()
                            .setPurchaseToken(purchase.purchaseToken)
                            .build()

                        val ackResult = withContext(Dispatchers.IO) {
                            billingClient.acknowledgePurchase(acknowledgeParams)
                        }

                        if (ackResult.responseCode == BillingClient.BillingResponseCode.OK) {
                            _uiState.value = BillingUiState.PurchaseSuccess(purchase)
                        } else {
                            _uiState.value = BillingUiState.Error("Failed to acknowledge purchase", ackResult.responseCode)
                        }
                    }
                } else {
                    _uiState.value = BillingUiState.PurchaseSuccess(purchase)
                }
            }
            Purchase.PurchaseState.PENDING -> {
                // Payment requires out-of-band completion (e.g., cash payment, bank transfer)
                _uiState.value = BillingUiState.PurchasePending(purchase)
            }
            else -> {
                _uiState.value = BillingUiState.Error("Unspecified purchase state", -1)
            }
        }
    }
}

When building apps with modern reactive toolchains, developers can pair this billing architecture with testing Jetpack Compose apps with Claude Code and MCP to verify that composable buttons instantaneously reflect entitlement state changes upon purchase completion.

Google Play Billing Scenario Target Product Type State Machine Status Google Play Bottom Sheet Action Verification Assertion Mechanism
One-Time Lifetime Purchase INAPP PURCHASED "1-tap buy" button tap Assert purchase.isAcknowledged == true and entitlement unlocked
Instant User Cancellation INAPP or SUBS USER_CANCELED Close sheet / Hardware Back Assert UI resets to pricing table; no billing exception thrown
Slow Test Card (Deferred) INAPP or SUBS PENDING Select "Slow test card" Assert app displays pending banner; feature locked until approved
Test Card Declining INAPP or SUBS Payment Declined Select "Test card, always declines" Assert Google Play displays decline error; app receives failure code
Monthly Subscription Purchase SUBS PURCHASED Tap "Subscribe" Assert recurring billing date rendered; premium tier unlocked
Subscription Upgrade (Prorated) SUBS Prorated Migration Select higher tier + Confirm Assert ReplacementMode applied; new billing amount confirmed
App Cold Restart Sync INAPP & SUBS Restored Entitlements Relaunch app via control_launch_app Assert queryPurchasesAsync() synchronizes state on startup

How ScreenSync MCP Bridges Claude Code to the Play Store Purchase Modal

ScreenSync MCP bridges Claude Code to Google Play Store purchase modals by extracting the active window's complete accessibility node tree via get_ui_hierarchy and capturing native MediaProjection video frames. Claude Code inspects com.android.vending bottom sheet elements, triggers simulated test card purchases via control_tap, and asserts UI state transitions without requiring root privileges.

The primary architectural achievement of ScreenSync MCP is its ability to operate across three distinct operational layers simultaneously:

System Layer Protocol / Medium Performance Baseline Primary Function During In-App Billing QA
Reasoning Agent Layer Stdio JSON-RPC Stream Sub-millisecond latency Claude Code evaluates test logic, decides next steps, and inspects UI nodes
Visual Capture Layer MediaProjection API / HTTP 3000 ~94 ms median latency Streams real-time frames from phone to hub without root and without ADB capture overhead
OS & Hardware Control Layer ADB Daemon / Platform Tools USB or Wi-Fi sockets Dispatches control_tap, control_swipe, get_ui_hierarchy, and get_logcat

1. Traversing External com.android.vending Dialogs via get_ui_hierarchy

When an application invokes launchBillingFlow(), the Android Window Manager places the Google Play payment activity at the top of the z-order stack. Unlike in-process test runners that crash when searching for views outside the host application, ScreenSync MCP queries the Android Accessibility Framework at the OS level. The resulting XML tree captures the exact structure of the Google Play purchase modal:

<node index="0" package="com.android.vending" class="android.widget.FrameLayout">
  <node index="0" class="android.widget.LinearLayout" resource-id="com.android.vending:id/bottom_sheet_container">
    <node index="0" class="android.widget.TextView" resource-id="com.android.vending:id/item_title" 
          text="Premium Lifetime Access (ScreenSync MCP Demo)" />
    <node index="1" class="android.widget.TextView" resource-id="com.android.vending:id/item_price" 
          text="$0.00" />
    <node index="2" class="android.widget.TextView" resource-id="com.android.vending:id/account_name" 
          text="[email protected]" />
    <node index="3" class="android.widget.TextView" resource-id="com.android.vending:id/payment_method" 
          text="Test card, always approves" />
    <node index="4" class="android.widget.Button" resource-id="com.android.vending:id/continue_button" 
          text="1-tap buy" clickable="true" bounds="[60,1820][1020,1960]" />
  </node>
</node>

Claude Code parses this node structure semantically. If the developer prompts Claude to test a successful purchase, Claude Code reads the bounds of the 1-tap buy button and issues a control_tap directly to its center coordinates. If the test requires simulating a declined card, Claude Code taps the payment method selector, picks "Test card, always declines" from the list, and taps purchase.

2. High-Speed Visual Capture via Native MediaProjection

While structural node trees provide the assertions for automated logic, visual verification confirms actual UI layout integrity. ScreenSync MCP captures real-time video frames using Android's native MediaProjection API (Android 7.0 or higher). Display capture requires zero device rooting and consumes zero ADB bandwidth, maintaining a median latency of approximately 94 milliseconds over a local area network (LAN). When testing whether a checkout screen displays an animated progress spinner, a celebratory confetti overlay, or a persistent error snackbar upon purchase completion, Claude Code invokes get_latest_screenshot to confirm visual fidelity.

3. The 226-Tool Ecosystem Advantage

ScreenSync MCP provides 226 MCP tools to developer agents:

  • 29 Phone & Desktop Tools: Comprising 12 device control tools (control_tap, control_swipe, control_type, control_key, control_open_url, control_launch_app), 3 operating system automation tools (os_mouse_click, os_type, os_hotkey), and 14 capture and telemetry tools (get_latest_screenshot, get_ui_hierarchy, get_logcat, record_screen, get_device_status).
  • 197 Browser Tools: The web_* suite operates through an authenticated Chrome/Edge browser extension. In enterprise billing setups where in-app purchases trigger backend cloud events, Claude Code can seamlessly jump between native mobile purchase execution on the smartphone and desktop web consoles (such as Google Cloud Pub/Sub topics or Stripe billing webhooks) in a single unified script. For a comprehensive inventory of all available capabilities, explore our complete Android MCP tool reference deep dive.

Step-by-Step Setup: Play Console License Testing, Hub, and Claude Code

Configuring an automated Play Billing test environment requires establishing Google Play Console license testing accounts, launching the ScreenSync MCP desktop hub on Node.js 18, registering the stdio MCP server in Claude Code, and connecting a physical Android device. Establishing reverse port forwarding over USB ensures continuous frame streaming during network radio state transitions.

Engineering teams can deploy this automated testing harness on Windows, macOS, or Linux through five sequential steps:

Step 1: Configure License Testing in Google Play Console

To execute test purchases on physical devices without incurring actual credit card charges, configure license testing:

  1. Log in to the Google Play Console.
  2. Navigate to Setup > License testing.
  3. Add the Gmail address of the Google account currently signed into your physical Android test smartphone to the License testers list.
  4. Set License response to RESPOND_NORMALLY.
  5. Upload your app build containing Google Play Billing Library 7.x to an Internal testing track and ensure your tester account is enrolled in the testing track URL. (Google Play requires the app version code and signature on the device to match a published test track before enabling billing APIs).

Step 2: Launch the ScreenSync MCP Desktop Hub

Download the ScreenSync MCP desktop hub package from screensyncmcp.tech and unpack the archive to a local workstation folder. Execute the launcher script:

# On Windows (PowerShell or Command Prompt)
cd C:\tools\screensync-hub
.\start-hub.bat

# On macOS or Linux (Terminal)
cd ~/tools/screensync-hub
chmod +x start-hub.sh
./start-hub.sh

The desktop hub initializes an HTTP server on port 3000 to ingest local MediaProjection video frames and prepares a JSON-RPC Model Context Protocol server over stdio. The console output displays your unique authentication token (SCREEN_SYNC_TOKEN). For comprehensive documentation, visit the official ScreenSync MCP setup guide.

Step 3: Register ScreenSync MCP with Claude Code

Register the desktop hub with the Claude Code CLI in your terminal:

claude mcp add screensync node "/path/to/screensync-hub/screensync-hub.js"

If you are developing inside Claude Desktop, edit your claude_desktop_config.json file (Settings > Developer > Edit Config):

{
  "mcpServers": {
    "screensync": {
      "command": "node",
      "args": ["/path/to/screensync-hub/screensync-hub.js"],
      "env": {
        "SCREEN_SYNC_TOKEN": "your_authenticated_token_here"
      }
    }
  }
}

(Note: ScreenSync MCP integrates with local stdio MCP clients including Claude Code, Claude Desktop, Cursor, and VS Code agent mode. ChatGPT's web application requires remote HTTPS endpoints and does not support local stdio processes).

Step 4: Install the Android Application and Authorize MediaProjection

  1. Download the free ScreenSync MCP application (package com.screensync.mcp) from Google Play.
  2. Open the application. Allow local network mDNS discovery to automatically detect your desktop hub on the local Wi-Fi subnet, or scan the pairing QR code displayed in the hub console.
  3. Tap Start Projection and confirm Android's native MediaProjection screen capture consent prompt.
  4. Unlock Developer Options on your smartphone (Settings > About Phone > tap Build Number 7 times) and enable USB Debugging (or Wireless Debugging). If you prefer wireless operation, review our guide on pairing Android wireless debugging to an MCP server without USB.
  5. Verify ADB connectivity by running adb devices in your workstation terminal.

Step 5: Establish USB Reverse Port Forwarding

To ensure uninterrupted visual streaming even when toggling airplane mode or isolating network radios during billing failure tests, configure USB reverse port forwarding:

adb reverse tcp:3000 tcp:3000

With reverse port forwarding active, MediaProjection video frames route directly across the USB physical link to the hub, even when Wi-Fi is disabled on the phone.

Practical QA Workflows: One-Time Purchases, Subscriptions, and Pending States

Practical Google Play Billing QA workflows validate one-time in-app purchases, auto-renewing subscriptions, pending transaction states, and subscription upgrades with proration replacement modes. Claude Code automates these multi-step purchasing journeys on physical Android hardware by executing sequential UI hierarchy assertions, dispatching precise touch gestures, and verifying instantaneous state propagation across local application databases.

Workflow 1: Validating Happy-Path Lifetime In-App Purchase

The happy-path one-time purchase test verifies that triggering a purchase opens Google Play, accepts the test card, delivers the purchase token, acknowledges the purchase, and updates the application UI.

Developer Prompt:
"Claude, launch our app com.example.screensync.billing. Navigate to the Paywall screen. Assert that the 'Lifetime Premium' card displays $0.00 (license test rate). Tap 'Upgrade Now'. When the Google Play bottom sheet appears, assert the item title and tap '1-tap buy'. Verify that the Google Play sheet dismisses, our app displays 'Purchase Complete!', and the billing manager logs show successful acknowledgment."

Step 1: Launch and Navigate

Claude Code executes control_launch_app with package com.example.screensync.billing. Once rendered, it calls get_ui_hierarchy to locate the upgrade button and clicks it using control_tap:

<node text="Upgrade Now" resource-id="com.example.screensync.billing:id/btn_buy_lifetime" bounds="[120,1400][960,1540]" />

Step 2: Assert Google Play Sheet

Claude Code calls get_ui_hierarchy. It verifies that the foreground package transitioned to com.android.vending:

<node package="com.android.vending" resource-id="com.android.vending:id/bottom_sheet_container">
  <node text="Premium Lifetime Access (ScreenSync MCP Demo)" resource-id="com.android.vending:id/item_title" />
  <node text="1-tap buy" resource-id="com.android.vending:id/continue_button" />
</node>

Step 3: Complete Purchase and Assert Acknowledgment

Claude Code calls control_tap on the continue_button node. It monitors get_ui_hierarchy until the Google Play sheet dismisses and the application displays Purchase Complete!. It then calls get_logcat with filter BillingManager to assert:

D BillingManager: onPurchasesUpdated: responseCode=0, purchases=1
D BillingManager: PurchaseState.PURCHASED: token=inapp:com.example.screensync.billing:lifetime_test_token_8849
D BillingManager: acknowledgePurchase SUCCESS for token: lifetime_test_token_8849

Finally, Claude Code calls get_latest_screenshot to confirm that the premium badge renders without visual clipping.

Workflow 2: Testing Slow Test Cards and Pending Payment States

Google Play allows users in many countries to pay via cash vouchers, bank transfers, or UPI, where transactions remain in Purchase.PurchaseState.PENDING until the user deposits funds at a physical store. Applications must gracefully handle deferred entitlements without unlocking features prematurely.

Developer Prompt:
"Claude, test our deferred billing flow. Open the paywall and launch the lifetime purchase flow. In the Google Play bottom sheet, tap the payment method dropdown and select 'Slow test card, approves after a few minutes'. Tap '1-tap buy'. Assert that the app displays 'Payment Pending: Complete your payment to unlock Premium' and confirms the feature remains locked."

Step 1: Select Slow Test Card

Claude Code launches the flow, inspects get_ui_hierarchy, and taps the payment method selector. In the payment methods list, it locates:

<node text="Slow test card, approves after a few minutes" resource-id="com.android.vending:id/payment_method_title" />

Claude Code calls control_tap on this item, returns to the primary sheet, and taps continue_button.

Step 2: Assert Pending UI State

Google Play returns BillingResponseCode.OK with purchase.purchaseState == Purchase.PurchaseState.PENDING. Claude Code inspects get_ui_hierarchy and asserts:

<node text="Payment Pending: Complete your payment to unlock Premium" resource-id="com.example.screensync.billing:id/text_pending_status" />
<node text="Feature Locked" resource-id="com.example.screensync.billing:id/text_feature_status" />

Claude Code verifies that the application did not call acknowledgePurchase() yet (as pending purchases cannot be acknowledged until PURCHASED) and confirms that premium features remain disabled. If you are also testing background synchronization when the purchase resolves hours later, explore our guide on testing Android offline mode and sync with Claude Code and MCP.

Workflow 3: Subscription Upgrade and Proration Replacement Modes

Testing subscription tier upgrades requires validating that Google Play applies correct proration modes (such as ReplacementMode.CHARGE_PRORATED_PRICE or ReplacementMode.WITH_TIME_PRORATION) without creating duplicate active subscriptions.

Developer Prompt:
"Claude, purchase our monthly 'Pro' subscription using the test card. Assert that the account status updates to 'Pro Monthly'. Now, tap 'Upgrade to Executive Tier'. Verify that the Google Play sheet shows 'Starting today, you will be charged $19.99/month with prorated credit'. Tap 'Subscribe', assert that the app updates immediately to 'Executive Tier', and verify the old subscription token is replaced."

Execution and Verification

  1. Claude Code purchases the initial monthly tier.
  2. The agent triggers the upgrade button, which passes BillingFlowParams.SubscriptionUpdateParams.newBuilder().setOldPurchaseToken(oldToken).setSubscriptionReplacementMode(ReplacementMode.CHARGE_PRORATED_PRICE).build().
  3. Claude Code calls get_ui_hierarchy to assert that Google Play's confirmation sheet explains the proration terms accurately.
  4. Claude Code taps Subscribe, asserts that the new entitlement updates instantaneously in the local database, and evaluates get_logcat to confirm that the old subscription token was retired. For end-to-end framework patterns, see our autonomous mobile app testing Claude Code guide.
Billing QA Scenario Primary ScreenSync Tools Critical Assertion Mechanism High-Risk Defect Caught
Happy-Path Lifetime IAP get_ui_hierarchy, control_tap, get_logcat Node 1-tap buy + Log acknowledgePurchase SUCCESS App fails to acknowledge; user auto-refunded after 3 days
Instant User Cancel control_key (Back), get_ui_hierarchy Return to paywall; no modal hang App leaves spinner running indefinitely on canceled flow
Slow Test Card (Pending) control_tap, get_ui_hierarchy Node Payment Pending; feature locked App prematurely unlocks premium features on pending state
Card Decline Recovery control_tap, get_ui_hierarchy Node Payment declined; retry button active App crashes with unhandled BillingResult code
Subscription Proration get_ui_hierarchy, control_tap Google Play replacement mode disclosure modal App fails to pass oldPurchaseToken; charges double
Mid-Purchase Network Loss ADB shell (airplane mode), get_logcat Log SERVICE_DISCONNECTED; clean error banner App throws unhandled socket timeout; freezes main thread
Permission Check Coexistence get_ui_hierarchy, control_tap System dialog stack resolution App crashes if permission prompt and billing sheet overlap

Diagnosing Billing Failures with Logcat Telemetry and Play Developer API

Diagnosing billing failures requires combining real-time logcat streaming with backend purchase token verification against the Google Play Developer API. Claude Code captures asynchronous BillingResponseCode errors, inspects signature verification exceptions, and monitors Google Cloud Pub/Sub Real-Time Developer Notifications to ensure that client purchase tokens reconcile deterministically with backend billing databases.

Automating in-app purchases is only half the battle; diagnosing silent failures is where agentic tooling truly excels. When an in-app purchase flow fails, the issue may stem from an unconfigured license tester, an expired Google Play cache, a mismatched application signature, or a failure in backend token verification.

1. Capturing Real-Time Billing Response Codes via get_logcat

Claude Code uses ScreenSync MCP's get_logcat tool with focused regex filters to trap billing error codes instantaneously:

# Claude Code inspects logcat for Play Billing client messages
adb logcat -d -s Finsky:V BillingClient:V BillingManager:D

Key BillingResponseCode values Claude Code diagnoses during test execution:

  • BillingResponseCode.SERVICE_DISCONNECTED (-1): The connection to Google Play Services dropped. Claude Code verifies whether the application's exponential reconnection loop recovers without crashing.
  • BillingResponseCode.USER_CANCELED (1): The user dismissed the payment sheet. Claude Code verifies that the application resets its checkout button and clears any loading overlays.
  • BillingResponseCode.ITEM_ALREADY_OWNED (7): Occurs when testing non-consumable items that were already purchased during a previous test run. Claude Code catches this error code and executes an automated consumption call or queries existing purchases via queryPurchasesAsync() to restore the entitlement.
  • BillingResponseCode.DEVELOPER_ERROR (5): Indicates invalid parameters passed to launchBillingFlow()—such as an empty offerToken, an invalid product type, or unconfigured pending purchase support. Claude Code flags the exact method call in the repository and proposes an immediate architectural fix.

2. Testing Mid-Purchase Network Drops and App Kills

A critical vulnerability in mobile billing is how an application behaves if the user loses network connectivity or the OS kills the app while the Google Play bottom sheet is displayed. Claude Code automates this rigorous resilience check:

Developer Prompt:
"Claude, launch the subscription flow. Once the Google Play bottom sheet is visible, enable airplane mode via ADB. Tap 'Subscribe'. Assert that Google Play displays a connection error. Disable airplane mode, relaunch the app, and verify that the application recovers cleanly without throwing an unhandled exception."

Claude Code executes:

# Toggle Airplane Mode via ADB
adb shell cmd connectivity airplane-mode enable

The agent observes the failure modal via get_ui_hierarchy, restores connectivity via adb shell cmd connectivity airplane-mode disable, and restarts the application using control_launch_app. It asserts that the app executes queryPurchasesAsync() inside onResume(), reconciling any orphaned purchases seamlessly. When coordinating permission dialogs with billing screens, teams can also consult testing Android runtime permissions with Claude Code and MCP.

3. Cross-Surface Backend Token Verification with ScreenSync MCP Browser Tools

In enterprise architectures, mobile in-app purchases are validated by an application backend using Google Cloud Pub/Sub Real-Time Developer Notifications (RTDN). When Google Play processes a transaction, it publishes a notification message to the developer's Pub/Sub topic containing the purchaseToken and subscriptionNotification type.

Because ScreenSync MCP includes *197 `web_browser automation tools**, Claude Code can execute a physical purchase on the Android phone and concurrently navigate your cloud admin dashboard in Google Chrome or Edge. Claude Code usesweb_navigate,web_get_by, andweb_eval` to confirm that the backend server received the webhook, verified the cryptographic signature against the Google Play Developer API, and updated the user's account tier in PostgreSQL or Stripe. If you are evaluating multi-rail payment systems, compare this architecture with our analysis of end-to-end mobile payment testing with AI.

Implementation Checklist for Automated Google Play Billing Testing

The Google Play Billing testing implementation checklist provides engineering teams with an actionable verification matrix spanning license tester configuration, desktop hub pairing, purchase state assertions, and backend token reconciliation. Following these structured criteria guarantees that mobile applications handle edge cases, network drops, and billing cancellations gracefully prior to production release.

  • [ ] Add tester Google accounts to Google Play Console under Setup > License testing with license response set to RESPOND_NORMALLY.
  • [ ] Verify that the application version code and signing key match an active build uploaded to an internal testing track.
  • [ ] Ensure host development workstation runs Node.js 18+ with Android Platform Tools adb configured in system PATH.
  • [ ] Unpack the ScreenSync MCP desktop hub and execute start-hub.bat (Windows) or start-hub.sh (macOS/Linux).
  • [ ] Register ScreenSync MCP with Claude Code CLI via claude mcp add screensync node "/path/to/screensync-hub.js".
  • [ ] Install the ScreenSync MCP mobile application (package com.screensync.mcp) from Google Play onto an Android 7.0+ device.
  • [ ] Authorize native MediaProjection display recording on the Android phone for low-latency (~94 ms) local screen mirroring.
  • [ ] Configure USB reverse port forwarding via adb reverse tcp:3000 tcp:3000 to ensure uninterrupted video capture during network tests.
  • [ ] Implement Google Play Billing Library 7.x with enablePendingPurchases() explicitly declared during client initialization.
  • [ ] Assert that one-time purchases invoke acknowledgePurchase() within Google Play's mandatory 3-day refund window.
  • [ ] Test deferred billing lifecycles using Google Play's "Slow test card" to assert proper handling of PurchaseState.PENDING.
  • [ ] Verify subscription replacement modes (ReplacementMode.CHARGE_PRORATED_PRICE) during tier upgrades and downgrades.
  • [ ] Assert that BillingResponseCode.ITEM_ALREADY_OWNED recovers gracefully by querying existing purchases via queryPurchasesAsync().
  • [ ] Validate that cold application restarts reconcile unacknowledged purchases in onResume() before user interaction resumes.
Verification Stage Command / ScreenSync Tool Primary Assertion Target Operational Success Criteria
Tester License Validation get_ui_hierarchy Price displays test rate ($0.00) Test account active; license response normal
Billing Sheet Inflation get_ui_hierarchy Package: com.android.vending Google Play modal displays item title & buy button
Purchase Execution control_tap on "1-tap buy" Google Play sheet dismisses cleanly Order completed; PurchasesUpdatedListener triggered
Token Acknowledgment get_logcat with filter BillingManager Log: acknowledgePurchase SUCCESS Purchase acknowledged; entitlement secured
Pending State Handling control_tap (Slow card) + get_ui_hierarchy Node: Payment Pending visible Feature locked until cash voucher clears
Visual Confirmation get_latest_screenshot Native MediaProjection capture Premium badge visible; zero layout clipping
Network Resilience ADB airplane mode + control_launch_app queryPurchasesAsync restores state App synchronizes entitlements upon reconnection
FAQ

Questions, answered

Why can't Espresso or Robolectric test Google Play Billing purchases?

Espresso and Robolectric fail at Google Play Billing purchases because the payment bottom sheet executes inside Google Play Store's external com.android.vending process rather than the application's sandbox. In-process test runners cannot cross process boundaries to inspect system windows, leaving critical purchase confirmations, payment methods, and error dialogues completely unverified.

How does ScreenSync MCP interact with the Google Play purchase bottom sheet?

ScreenSync MCP interacts with the Google Play purchase bottom sheet by querying Android's Window Manager through system-level accessibility interfaces. The get_ui_hierarchy tool extracts the exact XML node hierarchy of the com.android.vending dialog, allowing Claude Code to locate buttons like '1-tap buy' or 'Subscribe' and trigger native touch events via control_tap.

Can Claude Code test Google Play subscriptions and renewal cycles?

Claude Code tests Google Play subscription lifecycles by executing purchase flows with Google Play Console license testing accounts. License test subscriptions renew in minutes rather than months, allowing Claude Code to automate upgrade flows, validate proration replacement modes, and assert real-time UI entitlements across rapid renewal and cancellation cycles.

What happens if an in-app purchase is not acknowledged within three days?

If an in-app purchase is not acknowledged within three days via BillingClient.acknowledgePurchase or the Google Play Developer API, Google Play automatically refunds the user and revokes the purchase token. Claude Code tests this critical state machine to ensure application backends acknowledge entitlements immediately upon verifying purchase signatures.

Does ScreenSync MCP require rooting the Android device to test in-app billing?

ScreenSync MCP requires zero device rooting. Real-time screen capture runs through Android's native MediaProjection API with user consent at approximately 94 milliseconds latency without ADB overhead. Device control tools like control_tap and control_swipe run over standard Android Debug Bridge commands via USB or wireless debugging on Android 7.0 or higher.

How do license test accounts work with automated Claude Code billing tests?

Google Play Console license testing accounts allow designated tester Gmail accounts to execute test purchases on physical devices without incurring actual financial charges. When Claude Code triggers the billing flow on a test account, Google Play displays test payment methods (such as 'Test card, always approves'), enabling deterministic end-to-end automation.

How does Claude Code handle pending transactions and slow test cards?

Claude Code tests pending transactions by selecting Google Play's 'Slow test card, approves after a few minutes' in the license testing bottom sheet. The agent asserts that the application enters Purchase.PurchaseState.PENDING, renders a deferred entitlement banner, and unlocks features once the background purchase reconciliation worker completes.

Can ScreenSync MCP verify backend purchase tokens and Play Developer API webhooks?

ScreenSync MCP provides 197 browser tools alongside 29 mobile tools. In a single automated session, Claude Code can trigger a physical purchase on an Android phone, capture the purchase token from get_logcat, and use web_* browser tools to verify that backend webhooks and Google Cloud Pub/Sub topics received the event.

Written by

ScreenSync MCP team

The ScreenSync MCP team at EpsolDev builds the Android app, the desktop MCP hub and the browser extension that let AI agents see and control a real phone. These guides come out of that work.