---
title: 'In-App Purchases and Ads'
source: 'https://academia.sh/en/courses/platform-services-and-integrations/in-app-purchases-and-ads'
course: 'Platform Services and Integrations'
language: en
updated: '2026-10-21T05:02:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that a purchase update is redelivered until it is acknowledged'
    - 'Shows that an acknowledged purchase arrives once'
    - 'Measures that a refunded purchase is lost'
    - 'Compares the delivery of the 14 lessons measured across the course'
---

# In-App Purchases and Ads

In an in-app purchase, a purchase that is not acknowledged is redelivered every time the platform queries purchases. The lesson measures an unacknowledged purchase arriving duplicated, an acknowledged one arriving once, and a refunded one being lost; it then gathers the delivery of the 14 lessons in a single table.

In the previous lesson the last value won and the old value was superseded. A purchase does the opposite: the platform reports a purchase to the app and hands over the same update again until the app acknowledges it. The question is the course's question: **how many copies of this sent event reached the receiver?** Here the event is a purchase update, and the receiver is the app's code that processes purchases.

```kotlin
// Arrival.kt — classifies how many copies of a sent event reached the receiver
class Signal(val key: String, val seq: Int)

class Arrival(val label: String, val sent: Signal, received: List<Signal>) {
    val copies: Int = received.count { it.seq == sent.seq }
    val verdict: String = when {
        copies > 1 -> "duplicated"
        copies == 1 -> "once"
        received.any { it.key == sent.key && it.seq > sent.seq } -> "superseded"
        else -> "lost"
    }

    fun line() {
        println("$label: key=${sent.key} | copies=$copies -> $verdict")
    }
}

fun countArrivals(arrivals: List<Arrival>) {
    for (name in listOf("once", "duplicated", "superseded", "lost")) {
        println("$name: ${arrivals.count { it.verdict == name }}")
    }
}
```

## Purchase and Acknowledgement

An in-app purchase is the user paying inside the app through the app store. The result of the payment reaches the app as an update. **Acknowledgement** is the app saying, "I processed this purchase and delivered the item." A purchase that is not acknowledged counts as unprocessed, and the platform returns it again every time the app queries purchases.

On the platform, acknowledgement is a call on the billing library's client class.

```kotlin
// Platform code: BillingClient
val params = AcknowledgePurchaseParams.newBuilder()
    .setPurchaseToken(purchase.purchaseToken)
    .build()
grantItem(purchase)
billingClient.acknowledgePurchase(params) { result ->
    if (result.responseCode != BillingClient.BillingResponseCode.OK) retryAcknowledge(purchase)
}
```

This piece is platform code, and we make no claim here about output, duration, or counts. The model builds the platform's rule; the model is not the platform itself. In the model, acknowledgement is a sequence number entering a set; redelivery is adding the unacknowledged purchases to the list again; the platform's querying of purchases is represented in the model by an open.

```kotlin
// Billing.kt — an unacknowledged purchase is redelivered at every open
fun redeliver(purchases: List<Signal>, acknowledged: Set<Int>, opens: Int): List<Signal> =
    (1..opens).flatMap { open ->
        if (open == 1) purchases else purchases.filter { it.seq !in acknowledged }
    }

fun expire(received: List<Signal>, refunded: Set<Int>): List<Signal> =
    received.filter { it.seq !in refunded }
```

```kotlin
// Probe.kt — the receiver counts the list the app processed for the sent purchases
fun probe(sent: List<Signal>, received: List<Signal>): List<Arrival> =
    sent.mapIndexed { i, s -> Arrival("event-${i + 1}", s, received) }
```

**SE1.** The model assumes that every purchase is delivered once at the first open. On the platform, delivery can also arrive while the app is running, and the number of opens varies in real use.

## An Unacknowledged Purchase

Three purchases are made and the app acknowledges none of them. The app is opened twice.

```kotlin
// Main.kt — an unacknowledged purchase is redelivered at every open
fun main() {
    val coins = Signal("purchase.coins", 1)
    val premium = Signal("purchase.premium", 2)
    val adFree = Signal("purchase.adfree", 3)
    val sent = listOf(coins, premium, adFree)
    val arrivals = probe(sent, redeliver(sent, emptySet(), 2))
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
```

```text
event-1: key=purchase.coins | copies=2 -> duplicated
event-2: key=purchase.premium | copies=2 -> duplicated
event-3: key=purchase.adfree | copies=2 -> duplicated
once: 0
duplicated: 3
superseded: 0
lost: 0
```

This lesson's delivery: duplicated. All three purchases arrived as two copies. In earlier lessons, duplicated came from a lost response or from a return; here the duplication is the platform's rule: the platform returns an unacknowledged purchase every time it is queried, and this continues until acknowledgement arrives. A difference from M30/K05: there, neutralizing a duplicated request and making the result single was measured; this course measures the copy count, not the result. A receiver that deduplicates the purchase or processes it idempotently has still received a duplicated event.

## Acknowledgement Breaks the Chain

The same three purchases are made, but the app acknowledges the first two.

```kotlin
// Main.kt — an acknowledged purchase arrives once, an unacknowledged one at every open
fun main() {
    val coins = Signal("purchase.coins", 1)
    val premium = Signal("purchase.premium", 2)
    val adFree = Signal("purchase.adfree", 3)
    val sent = listOf(coins, premium, adFree)
    val arrivals = probe(sent, redeliver(sent, setOf(1, 2), 2))
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
```

```text
event-1: key=purchase.coins | copies=1 -> once
event-2: key=purchase.premium | copies=1 -> once
event-3: key=purchase.adfree | copies=2 -> duplicated
once: 2
duplicated: 1
superseded: 0
lost: 0
```

The two acknowledged purchases did not come after the first open and were counted as once. The unacknowledged third purchase came again at the second open. The rule does not see the cause: it cannot tell whether the third was not processed or the acknowledgement was lost; it sees only that two copies arrived.

This behavior is protection. An unacknowledged purchase is a situation where the user's money has been taken but the item has not been delivered; redelivery is the app's second chance to deliver the item. The cost is the copy: the code that delivers the item must be ready to see the same purchase twice.

## Subscription Renewal

A subscription is the same product renewed period by period. Each renewal is a new purchase update and carries its own sequence number. A renewal does not replace the earlier one, because an unacknowledged earlier period is still redelivered.

```kotlin
// Main.kt — unacknowledged subscription periods do not supersede the earlier one
fun main() {
    val first = Signal("subscription.pro", 1)
    val second = Signal("subscription.pro", 2)
    val third = Signal("subscription.pro", 3)
    val sent = listOf(first, second, third)
    val arrivals = probe(sent, redeliver(sent, setOf(1), 2))
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
```

```text
event-1: key=subscription.pro | copies=1 -> once
event-2: key=subscription.pro | copies=2 -> duplicated
event-3: key=subscription.pro | copies=2 -> duplicated
once: 1
duplicated: 2
superseded: 0
lost: 0
```

The first period was acknowledged, so it is once; the next two periods are duplicated. A greater sequence of the same key arrived, but no superseded arose, because for superseded the smaller sequence must never arrive. Configuration behaved that way in the previous lesson; with a subscription, the earlier period keeps arriving too.

## The Bounding Measurement

Redelivery does not go on forever. An unacknowledged purchase is refunded after some time, and the purchase is canceled without the item being delivered. The platform determines the duration; we write no number in the model or in this lesson. The update of a refunded purchase no longer comes.

```kotlin
// Main.kt — the delivery of a refunded purchase does not come
fun main() {
    val coins = Signal("purchase.coins", 1)
    val premium = Signal("purchase.premium", 2)
    val adFree = Signal("purchase.adfree", 3)
    val theme = Signal("purchase.theme", 4)
    val sent = listOf(coins, premium, adFree, theme)
    val received = expire(redeliver(sent, setOf(1), 2), setOf(2, 3))
    val arrivals = probe(sent, received)
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
```

```text
event-1: key=purchase.coins | copies=1 -> once
event-2: key=purchase.premium | copies=0 -> lost
event-3: key=purchase.adfree | copies=0 -> lost
event-4: key=purchase.theme | copies=2 -> duplicated
once: 1
duplicated: 1
superseded: 0
lost: 2
```

The second and third purchases were refunded and never reached the app; since no copy with the same key took their place, they were written as lost, not superseded. The acknowledged one is once, and the unacknowledged fourth, which has not expired, stays duplicated. Lost here is not a loss for the user: the money has been returned. The rule does not tell this apart; it sees only that the copy did not arrive.

**SE2.** The model assumes the refund happened before the first delivery. On the platform, a refund can also come after delivery; in that case the copy has already arrived, and taking the item back is a separate job.

## Ad Placement

An ad is a revenue path different from a purchase. The app reserves a slot, and the ad library loads the content into it. The loading comes from a server, can fail, and when it finishes is not in the app's hands.

```kotlin
// Platform code: ad placement
val slot = findViewById<FrameLayout>(R.id.ad_slot)
bannerView.loadAd(request)
slot.addView(bannerView)
```

This piece is also platform code, and we make no claim about output or counts. We do not measure ad impressions in this lesson: counting an impression depends on the ad network's definition and has no counterpart in the model. What we measure is the copy count of the purchase update.

## What the App Has to Do

- **Acknowledge the purchase after processing it.** The item is delivered first, and the acknowledgement is sent afterwards. Acknowledging first leaves the user charged but without the item if the process dies before the item is delivered; delivering the item and never acknowledging keeps the copy coming.
- **Record the purchase identifier.** When the second copy of the same purchase arrives, the item must not be delivered again. An app that records the identifier neutralizes the copy; the measurement still counts two copies.
- **Process subscription periods separately.** A new period does not replace the earlier one; both arrive.
- **Prepare for ad loading to fail.** If the ad does not come, the screen must not stay empty.

## Common Misconceptions

**The acknowledgement call delivers the purchase once.** It does, but only if the call succeeds. If the acknowledgement is lost, the purchase is duplicated again.

**A refunded purchase leaves a trace in the app.** It does not. In the measurement, two purchases never arrived and were written as lost.

**A subscription renewal supersedes the earlier period.** It does not. Two unacknowledged periods arrived as duplicated.

**Lost is always an error.** It is not. A refunded purchase is lost, and that is the right outcome.

## Summary

- This lesson's delivery: duplicated. Three unacknowledged purchases were redelivered when the platform queried them again (in the model, the second open): duplicated 3.
- An acknowledged purchase arrives once. With two acknowledged and one unacknowledged purchase, the result was once 2, duplicated 1.
- A subscription renewal does not supersede the earlier period. Once 1, duplicated 2.
- A refunded purchase is lost: once 1, duplicated 1, lost 2.
- A receiver that deduplicates the purchase also receives a duplicated event; the course measures the copy count, not the result.
- Ad placement is platform code and is not measured in this lesson.

## Course Wrap-Up

Across 14 lessons the course asked the same question: how many copies of this sent event reached the receiver? Each lesson's delivery is the first item in that lesson's `## Summary` section; the numbers in the bounding measurement column appear in the source lesson's output.

| Lesson | Measured event | Delivery | Bounding measurement |
|---|---|---|---|
| Notifications | notification | lost | a second notification with the same identifier replaces the first: superseded 1 |
| Cloud Messaging | message from the server | superseded | messages carrying no collapse key: once 4, superseded 1 |
| Location and Maps | location update | superseded | once the permission is revoked, later ones do not come: lost 3 |
| Camera and Media | captured photo | once | two taps, two separate sequences: once 3 |
| Sensors | sensor reading | superseded | once the listener is removed, readings stop: lost 2 |
| Deep Links and Shortcuts | deep link | duplicated | if the flag stays in memory, duplicated 2 |
| Widgets | widget update | superseded | no update comes to a removed widget: lost 2 |
| Encrypted Storage and the Keystore | stored secret | lost | a rewritten secret supersedes the old one: superseded 1, lost 1 |
| Network Security Configuration | outgoing network request | lost | a resent request supersedes the first: superseded 1, lost 1 |
| Authentication Flows | token refresh | duplicated | single flight drops the second refresh: lost 1 |
| App Signing | app version | once | versions with different keys: once 1, lost 3 |
| Analytics and Crash Reporting | analytics event | duplicated | when the queue fills, old events are dropped: lost 2 |
| Remote Configuration | configuration value | superseded | a value that is not activated: once 1, lost 2 |
| In-App Purchases and Ads | purchase update | duplicated | refunded purchase: lost 2 |

Of the 14 lessons, 2 close with delivery once, 4 with duplicated, 5 with superseded, and 3 with lost. The distribution of the core in the opening lesson is different: eight events came out as 3 once, 1 duplicated, 2 superseded, 2 lost. The two distributions do not count the same thing; the core counts eight events, and the table counts fourteen lessons.

The next course, M31/K06, moves on to the Testing, Performance and Distribution topic area.
