How to Test Checkout Process on Android (Complete Guide)
Checkout is the moment when a user decides to commit money or personal data to an app. Failures here translate directly into lost revenue, abandoned carts, and damage to brand trust. On Android the ch
Why Checkout Testing Matters on Android
Checkout is the moment when a user decides to commit money or personal data to an app. Failures here translate directly into lost revenue, abandoned carts, and damage to brand trust. On Android the checkout surface is especially fragile because it often mixes native UI, WebView components, third‑party payment SDKs, and deep‑link handling. A single uncaught exception in a payment gateway callback can leave the user staring at a blank screen while the transaction is already processed on the server, leading to duplicate charges or refunds.
Beyond revenue, checkout touches privacy and compliance. If the app mishandles credit‑card numbers, CVV, or billing address fields, it may violate PCI‑DSS or GDPR. Accessibility gaps (missing content descriptions, poor contrast) can block users with disabilities from completing a purchase, exposing the app to legal risk under WCAG 2.1 AA.
Testing checkout therefore needs to verify:
- Financial correctness – amount, tax, discounts, currency conversion.
- State consistency – cart → order → inventory → fulfillment.
- Error handling – network loss, invalid card, expired OTP, server‑side validation.
- UX flow – button states, progress indicators, fallback navigation.
- Accessibility & security – screen‑reader labels, touch target size, data encryption.
A systematic approach catches these issues before they reach production, reduces hot‑fix cycles, and gives confidence when releasing new payment methods or promotional codes.
Core Components of an Android Checkout Flow
Understanding the moving parts helps you design targeted tests. A typical Android checkout consists of the following layers:
| Layer | Responsibility | Typical Android APIs / Libraries |
|---|---|---|
| UI Presentation | Collects user input, shows order summary, validates fields | Jetpack Compose, XML layouts, Material Components, WebView |
| Business Logic | Calculates totals, applies coupons, checks inventory | ViewModel, Use‑Case classes, Repository pattern |
| Network Client | Sends order payload, receives payment token, handles retries | Retrofit, OkHttp, Coroutines, Volley |
| Payment SDK | Bridges to gateway (Stripe, PayPal, Razorpay, etc.) | SDK‑specific AAR/JAR, often launches its own Activity |
| Persistence | Stores cart, order ID, pending payment state | Room, DataStore, SharedPreferences |
| Deep Link / Intent Handling | Receives redirect from payment gateway (success/failure) | Intent filters, Firebase Dynamic Links, App Links |
| Analytics / Logging | Emits events for funnel tracking | Firebase Analytics, custom event bus |
Each layer can introduce failure modes. For example, a UI bug may hide the “Pay” button when the soft keyboard is open; a logic bug may apply a coupon twice; a network timeout may leave the order in a “pending” forever‑spinning state; a payment SDK may not forward the result Intent correctly, causing the app to miss the success callback.
UI Presentation Details
- Input fields often use
TextInputLayoutwithTextInputEditText. Validation is performed on focus loss or button click. - The order summary may be a
RecyclerViewshowing line items; each item can be clicked to edit quantity. - Progress indicators are typically
ProgressBaror circular indicators wrapped in aConstraintLayout. - Accessibility requires
contentDescriptionon icons, properlabelForattributes, and minimum 48 dp touch targets.
Business Logic Details
- Discount calculation may involve stacking rules (percentage off, fixed amount, minimum spend).
- Tax calculation can depend on shipping address jurisdiction, requiring a call to a tax service.
- Inventory check may be optimistic (reserve stock) or pessimistic (deduct immediately).
Network Client Details
- Retrofit interfaces define
@POSTendpoints with@Bodypayloads (often JSON). - Interceptors add authentication headers, log requests/responses, and implement exponential back‑off.
- Response handling distinguishes HTTP 200 (order created) from HTTP 400‑422 (validation errors) and HTTP 500 (server error).
Payment SDK Details
- Most SDKs launch a custom
ActivityviastartActivityForResult(or the new Activity Result API). - The app must implement
onActivityResultor register a callback to receiveRESULT_OK/RESULT_CANCELEDand extra data (payment token, error code). - Some SDKs use a
BroadcastReceiverfor asynchronous results (e.g., Apple Pay on Android via Google Pay).
Persistence Details
- A pending order is often stored with a status field (
CREATED,PAYMENT_PENDING,PAYMENT_SUCCESS,PAYMENT_FAILED). - On app kill/restore, the ViewModel reads this state to decide whether to show a retry button or navigate to order‑history.
Deep Link / Intent Handling Details
- After payment, the gateway redirects to a custom scheme (e.g.,
myapp://payment/success?orderId=123). - The manifest declares an
with. - The receiving
Activityextracts the query parameters and updates the UI accordingly.
Understanding these pieces lets you map each test scenario to a specific layer, making failure diagnosis faster.
Test Matrix: Categories and Scenarios
A comprehensive checkout test matrix covers functional correctness, error handling, performance, accessibility, and security. Below is a table that organizes scenarios by category and includes the expected verdict (PASS/FAIL) for a healthy implementation. Use this as a baseline when writing manual or automated tests.
| Category | ID | Scenario | Preconditions | Steps | Expected Outcome |
|---|---|---|---|---|---|
| Happy Path | HP1 | Successful credit‑card payment | User logged in, cart with two items, valid card on file | 1. Navigate to checkout 2. Review order summary 3. Tap “Pay with Card” 4. Enter CVV 5. Confirm payment | Order confirmation screen shows order ID, email receipt sent, cart cleared |
| Happy Path | HP2 | Apply valid coupon | Cart total > $50, coupon “SAVE10” active | 1. Open coupon field 2. Enter “SAVE10” 3. Tap Apply 4. Proceed to payment | Discount line appears, total reduced by 10%, tax recalculated |
| Error Path | EP1 | Declined card | Card returns issuer_declined from gateway | 1. Enter card details that trigger decline 2. Tap Pay | Error dialog shows “Card declined, try another payment method”, fields remain editable |
| Error Path | EP2 | Network loss during payment request | Device airplane mode enabled after tapping Pay | 1. Enable airplane mode 2. Tap Pay 3. Wait 10 s | Loading spinner stops, toast shows “Unable to connect, please check network”, retry button appears |
| Error Path | EP3 | Invalid CVV format | CVV field accepts only 3‑4 digits | 1. Enter “ABC” in CVV 2. Tap Pay | Inline validation shows “CVV must be 3‑4 numbers”, Pay button stays disabled |
| Edge Case | EC1 | Quantity zero after decrement | User reduces quantity of an item to zero via “‑” button | 1. In cart, tap “‑” on item until quantity shows 0 2. Observe UI | Item removed from list, subtotal updated, “Continue Shopping” button enabled |
| Edge Case | EC2 | Shipping address change after tax calculation | User edits address after tax shown | 1. Proceed to shipping step 2. Edit zip code to a different tax jurisdiction 3. Return to summary | Tax amount updates instantly, total reflects new tax |
| Accessibility | AC1 | Missing content description on Pay button | Button uses only icon | 1. Turn on TalkBack 2. Focus on Pay button | TalkBack announces “Pay button, double tap to activate” |
| Accessibility | AC2 | Insufficient contrast on error text | Error message uses #EEEEEE on white background | 1. Enable high‑contrast mode 2. View error message | Text meets WCAG AA contrast ratio (≥ 4.5:1) |
| Security | SE1 | Card number masked in logs | App logs raw PAN for debugging | 1. Make a purchase 2. Capture logcat | Logs show ** ** 1234 only, no full PAN |
| Security | SE2 | OTP sent over unencrypted channel | App uses HTTP for OTP request | 1. Initiate OTP flow 2. Capture network traffic | All requests use HTTPS, TLS 1.2+ |
| Performance | PF1 | Checkout screen loads under 2 s on low‑end device | Device: Android 8.0, 2 GB RAM, Snapdragon 450 | 1. Launch app, go to cart, tap Checkout | Screen fully interactive within 2 seconds, no jank frames (> 16 ms) |
| Performance | PF2 | Concurrent network calls do not block UI | Simulate slow latency (200 ms) on payment API | 1. Enable network throttling 2. Tap Pay | UI remains responsive, progress indicator animates smoothly |
You can expand this matrix with additional rows for gift‑card redemption, loyalty‑point usage, subscription upgrades, and cross‑border currency conversion. Each row should map to a specific test case (manual step‑by‑step or automated script).
Manual Testing Approach Step‑by‑Step
Manual testing remains valuable for exploratory checks, especially when new UI components or third‑party SDKs are integrated. Follow this procedure to cover the matrix above without writing code.
1. Environment Preparation
- Device matrix – Test on at least three physical devices representing low, mid, and high tier (e.g., Pixel 4a, Samsung Galaxy A12, Pixel 8 Pro).
- OS versions – Include Android 9 (API 28), Android 12 (API 31), and Android 14 (API 34) to catch version‑specific behavior.
- Network conditions – Use the built‑in Android Network Speed emulator (via
adb shell cmd network-manager profile set) or a tool like Clumsy on Windows to simulate 3G, LTE, and packet loss. - Accessibility tools – Enable TalkBack, Switch Access, and Font Size Large in Settings → Accessibility.
- Security proxies – Run mitmproxy or Charles on your laptop, configure the device Wi‑Fi to point to the proxy, and install the proxy’s CA certificate on the device to inspect HTTPS traffic.
2. Baseline Sanity
- Launch the app, log in with a test account that has a known payment method on file (or add a test card via the app’s payment‑method screen).
- Add two distinct products to the cart, ensuring at least one has a tax‑able category and another is tax‑exempt.
- Navigate to the checkout screen and verify that the order summary shows correct subtotal, tax, and total.
If any of these steps fail, log the defect and stop further checkout testing until the base flow is stable.
3. Happy Path Execution
- Follow the HP1 and HP2 scripts from the matrix.
- After each step, take a screenshot and note any UI lag, misaligned elements, or missing feedback.
- Verify that the payment SDK launches, collects the CVV, and returns to the app with a success result.
- Check the order‑confirmation screen for: order ID, timestamp, itemized list, and a “View Receipt” button that opens a PDF or web view.
- Confirm that the cart is empty and that a push notification or email (if integrated) is received.
4. Error Path Injection
- For EP1 (declined card), use the test card numbers provided by your payment gateway (e.g.,
4000 0000 0000 0002for Stripe decline). - For EP2 (network loss), toggle airplane mode after tapping Pay but before the SDK finishes its request. Observe whether the app shows a retry option and whether it resumes correctly when connectivity returns.
- For EP3 (invalid CVV), deliberately enter non‑numeric characters and ensure inline validation triggers before the SDK is invoked.
Record whether the app leaves the user in a recoverable state (i.e., not stuck on a loading spinner) and whether error messages are accessible.
5. Edge‑Case Exploration
- EC1: Repeatedly tap the decrement button until quantity hits zero, then tap it once more to confirm the item disappears.
- EC2: After tax is displayed, edit the shipping address field (e.g., change zip code from
90210to02139) and return to the summary; watch for a recalculation. - Test screen rotation at each step (portrait ↔ landscape) to ensure state is preserved.
- Test multitasking: press Home, open another app, then return to the checkout via recents; ensure the UI is intact and no data loss.
6. Accessibility Checks
- With TalkBack enabled, navigate through every focusable element. Listen for meaningful descriptions (e.g., “Coupon field, edit text, empty”).
- Use the Accessibility Scanner app to identify missing content descriptions, insufficient touch target size, and contrast issues.
- Verify that error messages are announced when TalkBack reads them (e.g., “Invalid CVV, please enter 3‑4 digits”).
7. Security & Privacy Verification
- Enable Developer Options → Show layout bounds to ensure no views overlay input fields that could capture taps maliciously.
- Use adb logcat while making a purchase and grep for keywords like
card,number,cvv,token. Ensure none of these appear in plain text. - Through the proxy, confirm that all requests to the payment gateway and your backend use
https://and that the TLS version is ≥ 1.2. - Check that any locally stored payment‑method data (if the app caches a token) is encrypted using EncryptedSharedPreferences or Android Keystore.
8. Performance Observation
- Enable Developer Options → Profile GPU rendering and watch the bars while navigating checkout.
- Use adb shell dumpsys gfxinfo
to obtain frame‑timing statistics; look for > 16 ms frames (jank). - Simulate a slow network (e.g., 150 ms latency, 5 % loss) with
adb shell cmd network-manager profile set latency 150 loss 5and ensure the UI still shows a progress indicator and does not freeze.
9. Post‑Test Cleanup
- Clear the test cart, remove any test payment methods, and log out.
- If you used a proxy, uninstall its CA certificate from the device to avoid interfering with other testing.
By following these steps you will have exercised each cell of the matrix manually, capturing issues that automated scripts may miss due to hard‑coded expectations or lack of exploratory behavior.
Automated Testing with Espresso/UI Automator
While manual testing uncovers UX nuances, automated checks give you regression safety and enable CI gating. Android provides two main instrumentation frameworks: Espresso for UI‑thread synchronization within your app, and UI Automator for cross‑app interactions (e.g., handling system dialogs or payment‑SDK Activities). Below is a practical guide to building a reliable checkout test suite.
1. Project Setup
Add the following dependencies to your app/build.gradle (using the latest stable versions at time of writing):
dependencies {
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.5.1"
androidTestImplementation "androidx.test.uiautomator:uiautomator:2.3.0"
androidTestImplementation "androidx.test:core:1.5.0"
androidTestImplementation "androidx.test.ext:junit:1.1.5"
androidTestImplementation "androidx.test:runner:1.5.2"
}
Create a test source set under src/androidTest/java/com/example/app/checkout/CheckoutTest.kt.
2. Base Test Rule
Use ActivityScenarioRule to launch the checkout Activity directly, bypassing login if you have a test‑only bypass (e.g., a debug flag that injects a fake auth token).
@get:Rule
val checkoutRule = ActivityScenarioRule(CheckoutActivity::class.java)
@Before
fun setUp() {
// Optional: clear cart, set fake payment method via Dependency Injection
TestDependencyInjector.setFakePaymentMethod(
PaymentMethod.testCard(number = "4242424242424242", cvv = "123")
)
}
3. Happy Path Test (Espresso)
@Test
fun `successful credit card payment`() {
// Verify order summary
onView(withId(R.id.tv_subtotal)).check(matches(withText("$120.00")))
onView(withId(R.id.tv_tax)).check(matches(withText("$9.60")))
onView(withId(R.id.tv_total)).check(matches(withText("$129.60")))
// Apply coupon
onView(withId(R.id.et_coupon)).perform(typeText("SAVE10"), closeSoftKeyboard())
onView(withId(R.id.btn_apply_coupon)).perform(click())
onView(withId(R.id.tv_discount)).check(matches(withText("-$12.00")))
onView(withId(R.id.tv_total)).check(matches(withText("$117.60")))
// Initiate payment
onView(withId(R.id.btn_pay_card)).perform(click())
// Payment SDK launches its own Activity; use UI Automator to wait for it
val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val payButton = uiDevice.findObject(By.clazz("android.widget.Button")
.textContains("Pay"))
assertNotNull(payButton)
payButton.click()
// Simulate entering CVV in the SDK's UI (if exposed)
val cvvField = uiDevice.findObject(By.res("com.stripe.android:id/cvv"))
cvvField?.setText("123")
uiDevice.findObject(By.clazz("android.widget.Button")
.textContains("Confirm")).click()
// Return to app – wait for order confirmation screen
onView(withId(R.id.tv_order_confirmation))
.check(matches(withText(containsString("Order #"))))
onView(withId(R.id.btn_view_receipt)).check(matches(isDisplayed()))
}
Explanation
- The test starts with Espresso to validate the app’s own UI.
- When the payment SDK launches a separate Activity, Espresso loses synchronization; we switch to UI Automator to interact with that external window.
- After the SDK finishes, we return to the app and resume Espresso assertions.
4. Error Path – Declined Card
@Test
fun `declined card shows retry`() {
// Configure test card that triggers decline
TestDependencyInjector.setFakePaymentMethod(
PaymentMethod.testCard(number = "4000000000000002", cvv = "123")
)
onView(withId(R.id.btn_pay_card)).perform(click())
// UI Automator: wait for error dialog from SDK
val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val errorDialog = uiDevice.wait(
Until.findObject(By.textContains("Card declined")), 5000)
assertNotNull(errorDialog)
// Ensure the Pay button is re‑enabled
onView(withId(R.id.btn_pay_card)).check(matches(isEnabled()))
onView(withId(R.id.btn_pay_card)).perform(click())
// Second attempt with a good card
TestDependencyInjector.setFakePaymentMethod(
PaymentMethod.testCard(number = "4242424242424242", cvv = "456")
)
onView(withId(R.id.btn_pay_card)).perform(click())
// Expect success
onView(withId(R.id.tv_order_confirmation))
.check(matches(withText(containsString("Order #"))))
}
5. Network Failure Simulation
Espresso cannot directly throttle network; instead, use OkHttp’s MockWebServer or configure the device via adb. For a reproducible test, inject a failing network layer:
@Test
fun `network loss displays retry toast`() {
// Force the repository to return an IOException
TestDependencyInjector.setNetworkResult(Result.error(IOException()))
onView(withId(R.id.btn_pay_card)).perform(click())
onView(withId(R.id.toast_message))
.inRoot(ToastMatcher())
.check(matches(withText(containsString("Unable to connect"))))
onView(withId(R.id.btn_retry)).check(matches(isDisplayed()))
}
ToastMatcher is a custom matcher that allows Espresso to inspect toast messages.
6. Accessibility Assertions
Espresso‑contrib provides matches(isDisplayed()) and check(matches(isFocusable())). Combine with AccessibilityChecks.enable() to run automated accessibility scans on each view hierarchy:
@Before
fun enableAccessibilityChecks() {
AccessibilityChecks.enable()
}
This will cause the test to fail if any view lacks a content description, has insufficient contrast, or uses a touch target smaller than 48 dp.
7. Performance Frame Timing
Use the adb shell cmd gfxinfo command from a test rule to assert jank limits:
@After
fun assertFrameTiming() {
val output = ShellUtil.runCmd(
"adb shell dumpsys gfxinfo ${BuildConfig.APPLICATION_ID}"
)
val jankFrames = extractJankFrames(output) // parse the “Janky” line
assertTrue("Excessive jank: $jankFrames frames > 16ms", jankFrames <= 2)
}
You can adjust the threshold based on device class.
8. CI Integration
- Run the instrumentation suite on Firebase Test Lab or GitHub Actions using the
gradle connectedAndroidTesttask. - Archive screenshots on failure via
androidx.test.core.app.ApplicationProvider.getApplicationContext()andActivityScenario.getResult(). - Tag each test with
@LargeTest,@MediumTest, or@SmallTestto control execution time.
By combining Espresso for in‑app flows and UI Automator for external SDK windows, you obtain a deterministic regression suite that covers the majority of matrix rows while staying fast enough for every pull request.
Leveraging SUSA for Persona‑Driven Exploration
Even the most comprehensive automated suite can miss edge cases that arise from real‑world user behavior—especially when users who deviate from the happy‑path assumptions. SUSA (SUSATest) offers an autonomous, persona‑driven exploration mode that can surface those hidden defects without writing additional test scripts.
How SUSA Works in the Context of Checkout
- Ingestion – You point SUSA at either the APK (
susatest-agent run --apk app-debug.apk) or a staging URL for a WebView‑based checkout flow. - Persona Engine – SUSA ships with built‑in behavior profiles: *Curious* (tries every UI element), *Impatient* (rapid taps, skips loading spinners), *Novice* (relies on hints, avoids advanced gestures), *Adversarial* (inputs malformed data, attempts SQL‑injection‑like strings), *Elderly* (long press durations, avoids small touch targets), and *Accessibility* (uses TalkBack navigation, high contrast).
- Exploration Loop – For each persona, SUSA drives the app, automatically handling dialogs, granting permissions, and filling fields with values drawn from the persona’s data set (e.g., the Adversarial persona injects strings like
' OR 1=1--). - Observation – While exploring, SUSA monitors for crashes, ANRs, unhandled exceptions, accessibility violations (via Android’s Accessibility Test Framework), and security issues (clear‑text logging of PAN, insecure HTTP).
- Flow Tracking – It recognizes logical screens (cart, shipping, payment, confirmation) and marks a checkout flow as PASS only if it reaches a confirmation screen with a valid order ID and clears the cart.
- Learning – Screens visited and dead ends are stored locally; subsequent runs focus on unexplored branches, improving coverage over time.
Practical Example: Discovering a Hidden Coupon‑Stacking Bug
Suppose your app allows only one coupon per order, but the backend mistakenly accepts a second coupon when the user rapidly taps the “Apply” button twice before the first request completes. A scripted test that applies the coupon once and waits for the network response will never see the race condition.
Running SUSA with the Impatient persona:
susatest-agent run --apk app-release.apk \
--persona impatient \
--timeout 15m \
--output ./susa-report
SUSA’s Impatient profile simulates a tap interval of ~100 ms and will often double‑tap the Apply button while the first network call is still in flight. In the generated report you might see:
[IMPATIENT] Crash detected at com.example.app.viewmodel.CouponViewModel.applyCoupon
java.lang.IllegalStateException: Coupon already applied
at CouponViewModel.kt:58
...
[IMPATIENT] Accessibility warning: Button 'Apply Coupon' missing contentDescription
(WCAG 2.1 AA failure)
The crash reveals a missing guard in the ViewModel that assumes idempotency. The accessibility note is an added bonus.
Security‑Focused Persona
The Adversarial persona submits strings with special characters, percent‑encoded payloads, and overly long inputs to fields like coupon code, promo code, and even the phone number field in the shipping address. If your app forwards these values directly to a logging statement or a backend query without sanitization, SUSA will flag:
[ADVERSARIAL] Potential security issue: Clear-text logging of user input
Tag: CheckoutLogger, Message: "Coupon entered: <script>alert(1)</script>"
You can then investigate the logging utility and replace it with a masked version.
Integrating SUSA into Your CI
Because SUSA produces a JUnit‑compatible XML report, you can plug it into existing pipelines:
# .github/workflows/susa.yml
name: SUSA Exploration
on: [push, pull_request]
jobs:
explore:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Android SDK
uses: android-actions/setup-android@v2
- name: Run SUSA
run: |
pip install susatest-agent
susatest-agent run --apk app/build/outputs/apk/debug/app-debug.apk \
--persona curious --persona impatient --output ./susa-report
- name: Upload report
uses: actions/upload-artifact@v3
with:
name: susa-report
path: ./susa-report
A failing SUSA run (any crash, ANR, or security finding) can be configured to block the merge, giving you a safety net that complements unit and instrumentation tests.
Limitations and Complementarity
- SUSA does not replace deterministic assertions; it excels at finding *unknown* failure modes.
- Its exploration is guided by heuristics; highly constrained flows (e.g., mandatory OTP entry) may need a persona that knows to wait for the SMS simulator.
- For reproducible regression, capture the exact sequence of actions that led to a defect and convert it into an Espresso/UI Automator test.
In practice, a team might run Susa nightly, triage any new findings, and promote the most critical ones into the automated test suite. This creates a feedback loop where the autonomous explorer continuously enriches your regression safety net.
Edge Cases That Appear Only in Production
Even with exhaustive lab testing, certain defects only surface when the app runs in the hands of real users on varied devices, networks, and locales. Below are several production‑only checkout pitfalls that have tripped teams, along with detection strategies.
1. Carrier‑Specific SMS OTP Delays
Some carriers batch SMS delivery, causing OTP arrival times of 30‑60 seconds or more. If your flow auto‑advances after a fixed 5‑second timer, users will never see the code and will abandon the checkout.
Detection
- Use a device farm (e.g., Firebase Test Lab) with SIM cards from different carriers.
- Introduce a configurable OTP‑delay flag in your test harness and verify that the UI shows a “Resend code” button after the expected window and does not navigate away prematurely.
2. Locale‑Dependent Number Formatting
In Arabic locales (ar-EG), numbers are rendered with Eastern Arabic numerals (٠١٢…) and the decimal separator may be ٫. If your UI parses the amount using DecimalFormat.getInstance() without specifying Locale.US, the total may be interpreted incorrectly, leading to a mismatch between displayed and sent values.
Detection
- Run your Espresso tests with
@Localerule set tonew Locale("ar", "EG"). - Assert that the value sent to the backend (captured via MockWebServer) matches the expected numeric amount after conversion.
3. Battery‑Optimization Killing Background Services
On Xiaomi, OnePlus, or Huawei devices, aggressive battery‑saving policies may stop your IntentService that listens for the payment gateway’s redirect URL if the app is swiped from recent apps. The user completes the payment on the gateway site but never returns to your app, leaving the order in a perpetual “pending” state.
Detection
- Use
adb shell dumpsys battery unplugto simulate unplugging, then enableadb shell dumpsys deviceidle force-idle. - Launch the checkout, start the payment SDK, swipe the app away, and observe whether a
BroadcastReceiverregistered forandroid.intent.action.VIEWwith your custom scheme still fires.
4. NFC Payment Interference
Some Android devices ship with NFC enabled by default, and tapping the phone against a payment terminal while the checkout screen is active can trigger the Google Pay UI, overlaying your own payment screen. If your app does not handle the RESULT_CANCELED from the NFC flow, the user may think the payment succeeded when it did not.
Detection
- Enable NFC on a test device, tap a NFC tag that launches a dummy payment activity while your checkout is visible.
- Verify that your app receives the callback and shows an appropriate error or allows the user to retry.
5. Push‑Notification Interference During CVV Entry
A heads‑up notification (e.g., incoming message) can steal focus while the user is typing the CVV, causing the soft keyboard to dismiss and the CVV field to lose focus. If your validation only runs on focus loss, the user may tap Pay with an incomplete CVV, resulting in a server‑side error that is not caught client‑side.
Detection
- Use
adb shell cmd notification post --title "Test" --text "msg"to fire a notification during the CVV entry step. - Confirm that the CVV field regains focus after the notification is dismissed and that the Pay button remains disabled until a valid CVV is entered.
6. Shared‑Preferences Corruption on Low‑Storage Devices
When internal storage falls below ~10 %, Android may throw SQLiteFullException when writing to SharedPreferences via the framework’s backup mechanism. If your app stores a pending order token there, the write may silently fail, causing the order to be lost after a reboot.
Detection
- Fill the device storage with large files (
dd if=/dev/zero of=/storage/emulated/0/bigfile bs=1M count=2000). - Run the checkout flow, reboot the device, and verify that the order token is still retrievable (or that the app shows a clear “Your session expired” message).
7. Time‑Zone Switching Mid‑Checkout
A user traveling across time zones may have the device clock change while the order is pending. If your backend validates the order timestamp against
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free