Common In-App Purchases Bugs and How to Catch Them
Common In-App Purchases Bugs and How to Catch Them
Common In-App Purchases Bugs and How to Catch Them
In‑app purchases (IAP) are a critical revenue path for mobile apps, yet they are also one of the most error‑prone areas. A single missed validation step or a mis‑handled edge case can turn a paying user into a frustrated one, trigger chargebacks, or even get the app removed from stores. This guide walks through the most common IAP bug patterns, explains why they appear, shows how they manifest to users, and gives concrete steps to reproduce, detect, fix, and prevent each issue. You’ll also find a test matrix that compares manual and automated techniques, a bug/symptom/fix table for quick reference, and a short checklist you can bookmark before every release.
---
Common In-App Purchases Bugs and How to Catch Them: Overview of the IAP Ecosystem
Why IAP Is Fragile
IAP sits at the intersection of client‑side code, platform‑specific billing libraries (StoreKit on iOS, Google Play Billing on Android), and a backend server that validates receipts. Each layer introduces its own failure modes:
- Client side – UI state, network handling, asynchronous callbacks.
- Platform library – sandbox vs production differences, caching of product info, deferred purchase callbacks.
- Server side – receipt verification logic, handling of renewal events, fraud detection.
Because the flow spans multiple processes and often involves real money, bugs that only surface under specific conditions (e.g., poor network, a particular locale, or a subscription renewal) are easy to miss with scripted tests that follow a single happy‑path.
What This Guide Covers
We will examine eight recurring bug categories, each broken down into:
- Root cause – why the defect originates.
- User symptom – what the user sees or experiences.
- Reproduction steps – how to trigger the bug reliably in a test environment.
- Detection techniques – manual checks, automated tests, and tooling hints.
- Fix & prevention – code changes, architectural safeguards, and test additions.
At the end of each major section you’ll find a short “Key Takeaways” bullet list.
---
Common In-App Purchases Bugs and How to Catch Them: Receipt Validation and Server Trust Issues
1.1 Missing or Incorrect Signature Verification
Root cause – The app accepts a receipt from StoreKit or Play Billing and forwards it to the backend, but the backend either skips signature verification or uses an outdated public key.
User symptom – A user completes a purchase, sees the confirmation dialog, but the app does not unlock the purchased content. In some cases the user is charged but receives nothing, leading to support tickets and possible refunds.
Reproduction –
- Use a sandbox/test account to make a purchase.
- Capture the raw receipt (via
SKPaymentTransaction.transactionReceipton iOS orPurchase.getOriginalJson()on Android). - Send a tampered receipt (e.g., flip a byte in the signature) to your backend.
- Observe whether the backend accepts it.
Detection –
- Manual – Use a tool like Charles Proxy or mitmproxy to intercept the receipt POST and modify the signature field.
- Automated – Write a unit test that feeds a malformed receipt to your verification function and asserts that it throws an exception.
Fix & prevention –
- Always verify the receipt signature using the platform’s official root certificates (Apple’s App Store root CA, Google’s Play public key).
- Keep the verification library up to date; subscribe to security bulletins from Apple and Google.
- Add a server‑side test suite that runs against both valid and invalid receipts on every CI build.
Key Takeaways
- Never trust client‑side receipt data without cryptographic verification.
- Automate signature checks in unit tests; treat a missing verification as a blocker.
1.2 Replay Attacks Using Old Receipts
Root cause – The backend validates a receipt but does not check whether it has already been consumed, allowing a user to reuse an old receipt to unlock content repeatedly.
User symptom – A power user discovers they can re‑purchase a consumable item without being charged again, effectively getting free currency or items.
Reproduction –
- Make a purchase and store the receipt.
- Restart the app, clear any local purchase state, and resend the stored receipt to the backend.
- Verify whether the backend grants the item a second time.
Detection –
- Manual – Use a REST client (e.g., curl) to replay the receipt multiple times.
- Automated – In an integration test, send the same receipt twice and assert that the second call returns an error or a “already consumed” status.
Fix & prevention –
- Maintain a persistent record (e.g., a database table with a unique constraint on the receipt’s
transactionIdororderId). - Before granting entitlement, check if the identifier already exists; if so, reject the request.
- For consumables, also verify that the quantity requested matches the remaining balance.
Key Takeaways
- Idempotency is essential: each receipt must be usable only once.
- Store receipt identifiers with a unique constraint and check them server‑side.
---
Common In-App Purchases Bugs and How to Catch Them: Product Identification and Localization Errors
2.1 Missing Product Identifiers in Store Response
Root cause – The app requests product details from StoreKit/Play Billing using an incorrect or outdated product ID list, causing the platform to return an empty product array.
User symptom – The store screen shows empty placeholders or “Item unavailable” messages, leading users to abandon the purchase flow.
Reproduction –
- Change one product ID in your code to a non‑existent string.
- Launch the store UI and observe that the product does not appear.
- Check the logs for
SKProductsRequestfailure orBillingClientresponse withBILLING_UNAVAILABLE.
Detection –
- Manual – Verify each product ID against the App Store Connect / Play Console list before a release.
- Automated – Write a test that calls
SKProductsRequest(orBillingClient.queryProductDetailsAsync) and asserts that the returned set matches the expected IDs.
Fix & prevention –
- Keep product IDs in a single source of truth (e.g., a JSON file synced with store metadata via a CI step).
- At app start, fetch the product list and compare it to the source of truth; fail fast if mismatches are found.
- Use feature flags to roll out new product IDs gradually, allowing a fallback to the previous set if the store returns errors.
Key Takeaways
- Product ID mismatches are caught easily by automated validation; treat them as build‑time checks.
- Always log the raw response from the store for debugging.
2.2 Localized Price Formatting Errors
Root cause – The app formats the price string manually (e.g., concatenating a currency symbol) instead of using the locale‑aware priceLocale provided by StoreKit or the priceAmountMicros/priceCurrencyCode from Play Billing.
User symptom – Users in certain regions see prices like “$9,99” (incorrect decimal separator) or the wrong currency symbol, causing confusion and abandoned checkouts.
Reproduction –
- Set the device locale to a region that uses a different decimal separator (e.g., French France).
- Navigate to the IAP screen and observe the price display.
Detection –
- Manual – Switch locales on a test device or emulator and capture screenshots.
- Automated – Use UI‑testing frameworks (XCUITest, Espresso) to read the price label and assert that it matches the format returned by
NumberFormatter.currencyStyle(iOS) orNumberFormat.getCurrencyInstance(locale)(Android).
Fix & prevention –
- Always display price using the locale object supplied by the store:
- iOS:
product.priceLocale+NumberFormatter. - Android:
productDetails.getPriceAmountMicros()andproductDetails.getPriceCurrencyCode()withNumberFormat.getCurrencyInstance(Locale). - Write a unit test that feeds a range of locales and verifies the output string matches the expected pattern.
Key Takeaways
- Never hard‑code currency symbols or decimal separators.
- Locale‑aware formatting eliminates price‑display bugs across all markets.
---
Common In-App Purchases Bugs and How to Catch Them: Purchase Flow Interruptions and Network Failures
3.1 User Cancels Mid‑Flow
Root cause – The app assumes that once the payment sheet is presented, the transaction will either succeed or fail with an error, ignoring the case where the user taps “Cancel” or closes the sheet.
User symptom – After canceling, the app still shows a loading spinner or incorrectly marks the purchase as pending, leaving the UI in an inconsistent state.
Reproduction –
- Trigger the purchase UI.
- Tap the cancel button (iOS) or back button (Android) before confirming payment.
- Observe whether the app returns to the previous screen and clears any loading indicators.
Detection –
- Manual – Use a test device to cancel at various points and verify UI state.
- Automated – In UI tests, invoke the purchase flow and send a cancel event; assert that the view model returns to the idle state and no entitlement is granted.
Fix & prevention –
- Handle the
paymentQueue(_:updatedTransactions:)callback with a transaction state of.failedand checktransaction.error?.code == SKError.paymentCancelled. - On Android, listen for
BillingResult.BillingResponseCode.USER_CANCELED. - Reset any optimistic UI state immediately upon receiving the cancel signal.
Key Takeaways
- Treat user cancellation as a first‑class outcome, not an error.
- Clear optimistic state and loading indicators in the cancel paths.
3.2 Network Loss After Payment Authorization
Root cause – The app receives a successful payment acknowledgment from the platform but loses network connectivity before it can send the receipt to the backend, causing the app to think the purchase failed while the platform already charged the user.
User symptom – The user sees an error message, but later discovers they were charged and the item never unlocked, prompting a refund request.
Reproduction –
- Enable airplane mode immediately after confirming the payment but before the app sends the receipt to your server.
- Observe the app’s behavior and check server logs for missing receipt.
Detection –
- Manual – Use a network throttling tool (e.g., Network Link Conditioner on iOS) to drop connectivity at the right moment.
- Automated – Mock the network layer to throw an exception after the purchase completion callback; assert that the app retries the receipt upload with exponential backoff and eventually succeeds or shows a clear “pending” state.
Fix & prevention –
- Store the receipt locally (e.g., in Keychain/EncryptedSharedPreferences) immediately after receiving it from the platform.
- Implement a background upload queue with retry logic; on app launch, flush any pending receipts.
- Show a “Purchase is being processed” UI that persists until the backend confirms entitlement.
Key Takeaways
- Never consider a purchase complete until the backend validates the receipt.
- Persist receipts locally and retry uploads robustly.
---
Common In-App Purchases Bugs and How to Catch Them: Subscription Management and Renewal Bugs
4.1 Failure to Handle Subscription Renewal Notifications
Root cause – The app only checks subscription status at launch or after a manual restore, ignoring push notifications or server‑to‑server renewal alerts from Apple/Google.
User symptom – A user whose subscription renews successfully sees the app still showing an expired state until they manually restart the app or tap “Restore Purchases.”
Reproduction –
- Set up a sandbox subscription with a short renewal period (e.g., 5 minutes).
- Let the subscription renew automatically.
- Without restarting the app, check whether the subscription status updates.
Detection –
- Manual – Monitor the app’s subscription UI over multiple renewal cycles in a sandbox environment.
- Automated – Use a test harness that simulates a renewal notification (e.g., sending a
didUpdateevent from StoreKit mock) and asserts that the subscription model updates accordingly.
Fix & prevention –
- Register for
AppStoreServerNotifications(iOS) or Real‑time Developer Notifications (RTDN) on Android and update the local subscription cache immediately. - Fall back to periodic polling (e.g., every 6 hours) only if push notifications cannot be guaranteed.
- Expose a clear “Last updated” timestamp in the subscription UI so users know the status is fresh.
Key Takeaways
- Renewal notifications are the source of truth; treat them as real‑time events.
- Combine push notifications with a fallback poll to avoid stale data.
4.2 Incorrect Handling of Subscription Downgrades/Upgrades Mid‑Cycle
Root cause – When a user changes subscription tier in the middle of a billing period, the app incorrectly applies the new price immediately or fails to prorate, resulting in either over‑charging or under‑delivering entitlements.
User symptom – After upgrading, the user either gets charged the full new price right away (feeling overcharged) or continues to see the old tier’s benefits until the next billing date.
Reproduction –
- Purchase a monthly subscription (Tier A).
- Midway through the period, initiate an upgrade to Tier B using the store’s upgrade flow.
- Verify whether the app reflects the new entitlements immediately and whether the backend receives a prorated transaction.
Detection –
- Manual – Use sandbox accounts with short subscription periods to observe upgrade behavior.
- Automated – Mock the upgrade transaction (including
oldTransactionfields) and assert that the backend processes a prorated charge and updates entitlement timestamps correctly.
Fix & prevention –
- On iOS, inspect
transaction.payment.productIdentifierandtransaction.originalTransactionto differentiate upgrades from new purchases. - On Android, use
Purchase.getOldPurchaseToken()to link the upgrade to the previous purchase. - Apply Apple’s or Google’s proration rules server‑side: calculate the effective price for the remaining period and grant the new tier immediately.
- Update the local subscription model to reflect the new tier and the next renewal date based on the prorated cycle.
Key Takeaways
- Subscription upgrades/downgrades require proration logic; never treat them as a simple new purchase.
- Preserve the original purchase token to correlate old and new transactions.
---
Common In-App Purchases Bugs and How to Catch Them: Restore Purchases and Promotional Offer Failures
5.1 Restore Purchases Returns Empty List
Root cause – The app calls the restore API but fails to map the returned transactions to local entitlements, often because it expects a specific product ID format that changed after a store update.
User symptom – A user who reinstalls the app or switches devices taps “Restore Purchases” and sees no previously bought items restored, forcing them to repurchase.
Reproduction –
- Make a non‑consumable purchase on Device A.
- Install the app on Device B, sign in with the same store account, and invoke the restore flow.
- Check whether the entitlements appear.
Detection –
- Manual – Perform the restore flow on a clean install and verify the UI.
- Automated – In a unit test, mock the restore response to return a known transaction and assert that the entitlement store updates correctly.
Fix & prevention –
- Always use the immutable
transactionIdentifier(iOS) orpurchaseToken(Android) as the key for restoring entitlements, not the product ID. - Validate that the restored transaction’s
originalPurchaseDatematches the expected date to avoid replay attacks. - Log the raw restore response for debugging when the list is empty.
Key Takeaways
- Restore logic must rely on permanent identifiers, not mutable product metadata.
- Test restore on a clean install as part of your release checklist.
5.2 Promo Code or Offer Not Applied
Root cause – The app presents a promotional offer (e.g., a discounted introductory price) but fails to send the correct offer identifier or token to the store, so the user pays the full price.
User symptom – The user sees a promo banner, taps “Buy,” but the checkout screen shows the regular price, leading to complaints and abandoned purchases.
Reproduction –
- Configure an introductory offer in App Store Connect (e.g., free trial for 3 days).
- In the app, initiate purchase flow for the product with the offer.
- Verify whether the price displayed matches the offer price.
Detection –
- Manual – Use a sandbox account to attempt the purchase and confirm the price shown.
- Automated – UI test that reads the price label and compares it to the expected offer price (using the same locale formatting logic as earlier).
Fix & prevention –
- Pass the offer identifier (
offerIDon iOS,offerTokenon Android) when callingSKPaymentorBillingClient.launchBillingFlow. - Validate on the server that the receipt contains the offer metadata (
in_appfield withoffer_type). - Add an automated test that purchases with an offer and asserts that the receipt includes the offer fields.
Key Takeaways
- Promotional offers require explicit identifiers; never rely on the default product price.
- Verify both UI price and receipt offer data in your test suite.
---
Common In-App Purchases Bugs and How to Catch Them: Deferred and Pending Purchase Edge Cases
6.1 Deferred Purchase (Ask to Buy) Not Handled
Root cause – On iOS, when a child initiates a purchase under “Ask to Buy,” StoreKit returns a transaction with state .deferred. Many apps treat this as an error or ignore it, leaving the user with no feedback.
User symptom – The child sees the purchase sheet disappear with no confirmation, and the parent never receives a notification, causing confusion.
Reproduction –
- Set up a family sharing group with a child account.
- Have the child attempt to buy a consumable.
- Observe whether the app shows a “Purchase pending approval” message.
Detection –
- Manual – Test with a child account in a sandbox family group.
- Automated – Mock StoreKit to return a deferred transaction and assert that the UI displays a pending state and does not grant entitlement.
Fix & prevention –
- Handle
.deferredstate by showing a clear message (“Your purchase is pending parent approval”) and disabling further purchase attempts for that product until the state changes. - Listen for subsequent transaction updates (
.purchasedor.failed) and update the UI accordingly.
Key Takeaways
- Deferred is a purchase is not a terminal state; treat it as a waiting period with appropriate UI feedback.
6.2 Pending Android Purchase (User Needs to Complete Payment)
Root cause – On Google Play, if a user selects a payment method that requires additional steps (e.g., carrier billing that needs SMS verification), the Billing Library returns a Purchase object with purchaseState == 0 (PENDING). Apps that only check for purchaseState == 1 (PURCHASED) treat this as a failure.
User symptom – The user sees an error message, but the purchase is actually awaiting completion; if they later finish the payment outside the app, they never receive the item.
Reproduction –
- Choose a payment method known to trigger pending state (e.g., select “PayPal” in a sandbox environment that requires manual approval).
- Complete the flow and observe whether the app grants the item immediately.
Detection –
- Manual – Use the Google Play sandbox with a test payment method that forces pending.
- Automated – Mock the Billing Library to return a pending purchase and assert that the app does not grant entitlement until a subsequent callback updates the state to PURCHASED.
Fix & prevention –
- Treat any purchase with state
PENDINGas a waiting state; show a UI indicator (“Waiting for payment completion”). - Register a
PurchaseUpdatedListenerand re‑evaluate the purchase when the state changes. - On app start, query
BillingClient.queryPurchasesAsyncto catch any pending purchases that may have been resolved while the app was offline.
Key Takeaways
- Pending purchases are legitimate intermediate states; never treat them as errors.
- Poll for purchase state changes on resume and after known payment‑method delays.
---
Common In-App Purchases Bugs and How to Catch Them: Persona‑Driven Autonomous Exploration vs Scripted Tests
Why Scripted Tests Miss IAP Bugs
Traditional automated tests often follow a predefined sequence: launch app → navigate to store → tap “Buy” → assert success. This approach works for the happy path but overlooks variations that real users exhibit:
- Curious users tapping every UI element, including hidden debug buttons that might trigger unintended purchase flows.
- Impatient users spamming the buy button, leading to race conditions.
- Novice users getting stuck on prompts and abandoning the flow.
- Adversarial users attempting to tamper with network packets or replay old receipts.
- Elderly or accessibility‑mode users interacting with larger touch targets or voice controls, which may expose layout‑related IAP bugs.
These behaviors can surface issues such as double‑tap race conditions, missing accessibility labels on purchase buttons, or incorrect handling of interrupted flows—scenarios that a single script never touches.
How Autonomous Exploration Helps
An autonomous QA agent (like the one offered by SUSA) explores the app without pre‑written scripts, guided by a set of persona profiles. Each profile defines a distinct interaction pattern:
| Persona | Interaction Traits | Typical IAP‑Related Findings |
|---|---|---|
| Curious | Taps every visible element, explores hidden menus | Discovers unintended purchase triggers, exposed debug purchase buttons |
| Impatient | Rapid repeated taps, quick navigation | Uncovers race conditions, double‑charge bugs, UI state leaks |
| Novice | Reads prompts slowly, often taps cancel or back | Finds unclear error messages, missing guidance during deferred/pending states |
| Adversarial | Modifies network traffic, attempts to resend old receipts | Detects insufficient receipt validation, replay‑attack vulnerabilities |
| Elderly | Uses larger touch targets, voice‑over navigation | Reveals accessibility label missing on IAP buttons, touch‑target too small |
| Accessibility | Relies on screen readers, high‑contrast mode | Checks that price strings and purchase states are announced correctly |
| Power user | Uses shortcuts, restores purchases frequently | Finds restore‑purchase failures, missing entitlement sync after reinstall |
| Price‑sensitive | Aborts when price display looks off | Catches localization/formatting errors, misleading promo pricing |
The agent automatically records each screen visited, each action taken, and any observable outcome (crash, ANR, toast, UI state change). When it encounters a purchase flow, it:
- Collects the raw receipt from the platform library.
- Attempts to send it to a mock backend (or the real backend in a staging environment).
- Variates the interaction: cancels mid‑flow, drops network, replays receipts, changes locale, triggers promotional offers, etc.
If the backend responds with an error or the UI shows an incorrect state, the agent logs a defect with steps to reproduce. Because the exploration is driven by personas, it produces a broader set of edge cases than a deterministic script.
Practical Example: Detecting a Race Condition with the Impatient Persona
*Scenario*: The app allows users to buy a consumable currency pack. The UI disables the buy button after the first tap, but a bug in the debounce logic enables a second tap before the disabling takes effect, resulting in two transactions being sent.
*Autonomous test*:
- The Impatient persona rapidly taps the buy button three times within 200 ms.
- The agent captures two successful transaction callbacks from StoreKit.
- It forwards both receipts to the backend, which accepts both and grants double the currency.
- The agent flags the defect: “Duplicate purchase granted due to insufficient debounce.”
*Manual reproduction*:
- Enable “Show touch feedback” in developer options, tap quickly, and verify the duplicate entries in your analytics or server logs.
*Fix*:
- Disable the button immediately on
touchDownand re‑enable only after receiving a final transaction callback (success, failure, or cancel). - Add a unit test that simulates rapid taps using a mock UI layer and asserts that only one transaction request is made.
Integrating Autonomous Findings into CI
You can export the agent’s discovery report as a JUnit‑style XML or JSON artifact and fail the build if any high‑severity IAP defect is detected. This gives you the confidence of scripted coverage plus the breadth of exploratory testing, without writing countless persona‑specific scripts.
Key Takeaways
- Persona‑driven autonomous exploration surfaces IAP bugs that scripted happy‑path tests miss, especially those tied to user behavior variations.
- Use the exploration output as a complementary gate in your CI pipeline, focusing on high‑risk areas like receipt handling, network interruptions, and promotional flows.
---
Building a Reliable IAP Test Matrix (Manual + Automated)
The following matrix maps each bug category to the most effective detection techniques. Use it to decide where to invest manual effort, where to automate, and where to leverage autonomous exploration.
| Bug Category | Manual Test Technique | Automated Unit / Integration Test | UI‑Test (XCUITest/Espresso) | Autonomous Exploration (Persona‑Driven) | Recommended Frequency |
|---|---|---|---|---|---|
| Receipt signature missing | Charles proxy tampering | Verify verification function throws on bad sig | N/A | Adversarial persona replays tampered receipt | Every commit (unit) + nightly (autonomous) |
| Replay attack | Manual resend of old receipt | Assert second submission rejected | N/A | Adversarial persona re‑uses receipt | Every commit (unit) + nightly |
| Missing product IDs | Spot‑check store IDs vs code | Assert returned set matches source of truth | N/A | Curious persona browses store screen | Each release (manual) + CI (automated) |
| Price localization errors | Locale switch + screenshot | Assert formatted string matches locale | Assert price label matches NumberFormatter output | Curious + Accessibility personas | Each release (manual) + nightly (UI) |
| User cancels mid‑flow | Tap cancel button | Assert state returns to idle, no entitlement | Assert loading spinner disappears | Impatient persona | Each release (manual) + nightly |
| Network loss after auth | Enable airplane mode post‑auth | Assert local receipt stored & retry queue processes | Assert pending UI shown & eventual success/failure | Impatient persona | Each release (manual) + nightly |
| Subscription renewal not handled | Wait for sandbox renewal | Assert subscription model updates on notification | N/A | Power‑user persona (frequent restores) | Each release (manual) + weekly (autonomous) |
| Subscription upgrade/downgrade proration | Mid‑cycle upgrade in sandbox | Assert prorated charge & correct entitlement dates | N/A | Power‑user persona | Each release (manual) + nightly |
| Restore purchases empty | Fresh install + restore flow | Assert entitlements restored from mocked transactions | Assert UI shows restored items | Novice + Power‑user personas | Each release (manual) + nightly |
| Promo offer not applied | Sandbox purchase with offer | Assert receipt contains offer fields | Assert UI price matches offer price | Curious + Price‑sensitive personas | Each release (manual) + nightly |
| Deferred purchase (Ask to Buy) | Child account sandbox | Assert UI shows pending, no entitlement | Assert pending label visible | Novice persona | Each release (manual) + nightly |
| Pending Android payment | Select pending‑payment method | Assert UI shows waiting state, no entitlement | Assert pending indicator | Impatient persona | Each release (manual) + nightly |
How to Use the Matrix
- Unit tests catch logical errors in verification, proration, and state transitions.
- UI tests validate that the correct screens, labels, and loading states are presented to the user.
- Autonomous exploration adds a behavior‑driven layer that catches issues arising from real‑world interaction patterns (e.g., rapid taps, locale switching, accessibility navigation).
- Schedule the matrix according to risk: unit tests on every commit, UI tests on each pull request, autonomous runs nightly or before major releases.
---
Quick Checklist for Shipping Safe IAP Features
Before you tag a release, run through this list. Mark each item as ✅ or ❌ and address any gaps.
- [ ] Receipt validation – Backend verifies signature and nonce; unit tests reject tampered receipt
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