> For the complete documentation index, see [llms.txt](https://docs.amply.tools/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.amply.tools/changelog.md).

# Changelog

User-visible changes to the Amply SDKs and dashboard.

## Latest releases

* **iOS SDK** — 0.16.0
* **Android SDK** — 0.16.0 (published to Maven Central as `tools.amply:sdk-android:0.16.0`)
* **React Native SDK** — 0.16.0 (ships the 0.16.0 native SDKs)

## 0.16.0

All platforms (iOS · Android · React Native).

**Built-in campaigns.** A campaign marked **Built-in** in Amply can ship inside your app build, so the app shows it on its very first launch, even with no internet. Mini-app screens are included. Turn on **Built-in** on the campaign, run `npx @amplytools/cli bundle` in your app project, commit the file it writes and build. No code change. Expo apps add the Amply config plugin to `app.json`, which copies the files into the native projects at prebuild. The first successful download from Amply replaces the bundle for good. See [Built-in campaigns](/developer-guide/built-in-campaigns.md) and [Built-in](/user-guide/campaigns.md#built-in).

**Where the campaigns came from.** `ConfigFetchFinished` gains `configSource` (`network`, `cache`, `bundle`, `forcedBundle` or `empty`) and, when the campaigns come from the bundle, `bundleGeneratedAt`. `success` now describes the download only: it is `false` whenever the download failed, also when the app falls back to campaigns it already has. It used to report `true` after a fallback. See [Events](/reference/events.md#configfetchfinished).

**Also changed, for every app.** A download that answers but isn't a valid campaign list (a captive Wi-Fi login page, for example) no longer clears the campaigns the app already has. A download with no campaigns is now saved too, so a campaign you stopped no longer comes back from an older saved copy when the app is offline.

## React Native 0.15.1

React Native only. The package's CommonJS entry point now matches its TypeScript source. In 0.15.0 it was missing some exports (among them `handleUrl`, `getLastCampaignTouch` and `setAppLanguage`), so Jest and other tools that load the package outside Metro could not find them. Apps built with Metro were not affected. Nothing to migrate.

## 0.15.0

All platforms (iOS · Android · React Native). Additions only — nothing to migrate.

**Show after.** A campaign can wait 0 to 3 seconds after its triggering event before it appears, set in the **When** step. Use it to let a screen settle, to let your own celebration play first, or to stay out of the way of a tap. See [Show after](/user-guide/campaigns.md#show-after) for what happens during the pause.

**Test on device.** Open any campaign, drafts included, on your own phone by scanning a QR code from the campaign page. Nothing is counted, and with **Check targeting** on, a phone that doesn't match sees which audience conditions failed and why. The app forwards incoming links to the new `handleUrl`; set the app's **Deeplink scheme** in the application settings. See [Test on device](/user-guide/testing-and-rollout.md#test-on-device) and [Forwarding links to Amply](/developer-guide/handling-deeplinks.md#forwarding-links-to-amply).

**Last campaign touch.** `getLastCampaignTouch(withinMs)` returns the campaign that most recently reached the user — its id, its name, whether it was shown or engaged with, and when — if that was within the window you pass (up to 30 days). Use it to credit a purchase to a campaign even when the purchase didn't start from the campaign's link. Touches are forgotten when the app switches to a different user. See the [iOS](/reference/sdk-ios.md#last-campaign-touch), [Android](/reference/sdk-android.md#last-campaign-touch) and [React Native](/reference/sdk-react-native.md#last-campaign-touch) references, and the recipe [Purchases from campaigns in your analytics](/recipes/purchases-from-campaigns-in-analytics.md).

**The campaign's name on campaign events.** `CampaignShown` and `CampaignResolved` now carry `campaignName` next to `campaignId`, so a listener that forwards them to your analytics can label the campaign without a lookup. `CampaignResolved` has new results for a delayed show that did not happen — `SkippedPreempted`, `SkippedWhileDelayed`, `SkippedBackgrounded`, `SkippedSessionEnded`, `SkippedNoLongerEligible` and `SkippedCampaignChanged`. See [Events](/reference/events.md#campaignresolved). On React Native, `formatSystemEventLabel` includes the name in the `CampaignShown` label.

**Event uploads are exact.**

* **Delivered events are no longer sent again.** An upload could send events Amply had already received — most often the start of the next session — so the same event could arrive more than once.
* **A session's last events are no longer lost offline.** When the app went to the background without a connection, the final events of that session were treated as delivered and dropped. They now wait and go with the next launch.
* **Nothing recorded during an upload is skipped.** An event recorded while an upload was on its way could be treated as sent without being sent. Each upload now marks exactly the events it carried, and only once Amply has accepted them.
* **Backlogs catch up.** Events left behind by a crash or a long time offline are now sent while the app runs, a batch at a time, instead of waiting behind the current session.

**Switching user.** User id changes now take effect in the order your app makes them, even when several land in quick succession.

## 0.14.0

**App language.** Sheets, stories and mini-apps show the translation for the language your app shows, including script and region variants — Traditional and Simplified Chinese, Brazilian and European Portuguese, Latin American Spanish. Apps that pick their language themselves tell Amply with `setAppLanguage`, in any common format. A new **App language** audience rule targets by it. See [App language](/developer-guide/app-language.md).

**Device language is the phone's.** The `language` and `locale` device values — and `{@device.language}` / `{@device.locale}` in campaign links — now report the phone's system language on both platforms. On iOS they used to report the app's language.

**Country on Android** follows the phone's region, not a per-app language choice.

## 0.13.0

All platforms (iOS · Android · React Native). No API change.

**Values in campaign links.** A deeplink campaign — or an *Open deep link* button in a mini-app made in Builder — can carry the campaign's ID, name or type, a custom property, a few device facts and the triggering event's name or parameters. The SDK fills them in on the device before your listener gets the link. See [Managing deeplinks](/user-guide/managing-deeplinks.md) and [Handling deeplinks](/developer-guide/handling-deeplinks.md). Campaigns that use them reach apps on SDK 0.13.0 and newer only.

**CocoaPods:** the `AmplySDK` 0.13.0 pod now requires iOS 15.0 or later. The Swift Package Manager package still declares iOS 14.0. React Native already required iOS 15.1, so it is not affected.

## 0.9.1

All platforms (iOS · Android · React Native). **Recommended for every app on 0.8.0 or later** — no API change and nothing to migrate.

**Events are no longer sent twice.** When an upload succeeded but the response did not carry a server time the SDK could read, the SDK treated the whole upload as failed and sent the same events again on its next attempt. The server had already accepted and stored them, so this did not retry a delivery — it duplicated one.

That matters beyond tidiness, because duplicated events are counted. An audience rule on how many times something happened, and a frequency cap that limits how often a campaign may show, both read those counts — so an affected app could have seen a campaign trigger earlier than its rule intended, or a cap reached sooner.

Three situations reached it: a response with no timestamp at all, a response whose body omitted it, and — new in 0.8.0, which is why this is worth upgrading from — a network proxy that rewrites the timestamp to something implausible. The clock reading became more precise in 0.8.0 and, with it, stricter, which widened what counted as unreadable.

Reading the server time stays best-effort on both paths; delivery no longer depends on it.

## 0.9.0

All platforms (iOS · Android · React Native).

**Detaching a listener now works.** Previously, asking the SDK to stop calling a listener could silently have no effect on iOS: the listener stayed registered and the SDK kept calling it long after the code that owned it was gone. Three things followed from that, all fixed here.

* **React Native apps no longer crash on a bundle reload.** In development, reloading the JavaScript bundle while the app was running crashed it — reliably, every reload. Nothing to change in your app: the fix is entirely inside the native bridge and the **JavaScript API is unchanged**.
* **Deeplinks are delivered once, not once per reload.** Each reload used to leave the previous listener behind, so a single deeplink arrived several times over.
* **A gate registered by a screen that has gone away no longer hangs the next call.** It used to wait out the full timeout — up to a minute — showing nothing. It now resolves immediately, the way that gate was registered to resolve, and costs the campaign no impression.

**Native API change (iOS · Android · KMP).** Registering a listener now returns a token, and you pass that token back to detach:

* `registerDeepLinkListener` / `removeDeepLinkListener`
* `setSystemEventsListener` / `clearSystemEventsListener`
* `setLogListener` / `clearLogListener` — `setLogListener` no longer accepts `null`
* `registerGate` / `unregisterGate` — **`unregisterGate` is new**; there was previously no way to withdraw a gate at all

Keep the token wherever you keep the listener. Anything scoped to the whole process — a listener set up once at launch and never removed — needs no change beyond storing the return value. See [iOS](/developer-guide/ios-integration.md) and [Android](/developer-guide/android-integration.md) for the per-platform shape.

**React Native apps need no code changes** — the bridge holds the tokens for you.

## 0.8.0

All platforms (iOS · Android · React Native). A fix release: no new capabilities, several defects that could cost you data.

**A failing listener can no longer take the SDK down with it.** A log listener that threw could end the process. On Android and React Native a listener that throws is now contained. On native iOS, catch exceptions inside your own listener — an Objective-C exception raised in your code still ends the process, and the SDK cannot intercept that. React Native apps hit this without writing a log listener at all: setting `debug` or `logLevel` installs one for you.

**`CampaignShown` was announced twice.** If you mirror campaign impressions into your own analytics — which this guide recommends — an impression could be delivered to your listener a second time, so your funnel counted shows that happened once. Fixed. `SessionFinished` could also be missing from the stream entirely; it is now always emitted before the session ends.

**One copy of the SDK, however many times you start it.** If your app constructed the SDK more than once — a second initialization, or a React Native reload — two copies could run side by side: two sessions started where there should have been one, and one copy could stop the other's events from ever being sent, so a whole session went missing from your data. A second construction now attaches to the SDK that is already running, and any configuration passed the second time is ignored.

**Repeated sends of the same events are fixed.** Each background/foreground cycle used to leave the previous session's uploader running, and with events still waiting, several of those could send the same batch — so the same event could arrive more than once. Uploading now stops with the session it belongs to. Data already collected from apps on earlier versions may still contain those repeats, so counts from those versions can read higher than the real number.

**You can now withdraw a listener.** `removeDeepLinkListener` and `clearSystemEventsListener` join the register/set calls they mirror. This matters if you register from something that gets torn down and rebuilt while your app keeps running — registering a deep link listener **adds** one rather than replacing it, so without withdrawing, the old listeners stayed registered and every later deep link went to all of them. React Native apps need no code change for the fixes above; the JavaScript API is unchanged.

**Frequency caps.**

* Two audience conditions that differed only by the type of their value could share a counter, so a campaign with a frequency cap could show when it should not. Affects campaigns published before SDK 0.6.0, where the value type was not recorded.
* A campaign impression resolved while the app was being backgrounded could go uncounted. Each one lost allowed one extra show for the rest of that install's life, and they accumulate. Such impressions are now recorded; a small number can still be missed if the device is under pressure at that moment.

**Also fixed.** Event timestamps now use the server-corrected clock from the first event of every launch, instead of the device clock until the correction loaded. A gated call (`trackGated`) can no longer wait indefinitely behind other campaign work — it fails open on a deadline, like the other steps on that path.

**Breaking, native only.** Two declarations changed on the native surface, so a Swift/Kotlin app touching them needs an edit: `HttpClientInterface.extractServerTime` is removed (it only ever returned local time), and the session-tracker state stream now carries a `SessionEvent` enum instead of integer constants. Neither is reachable from React Native.

## 0.7.1

React Native only.

* Custom `configBaseUrl` / `backendBaseUrl` passed at initialization were being dropped instead of applied. If you point the SDK at your own endpoints, this release is required.
* The Expo config plugin no longer warns on every prebuild.

## 0.7.0

All platforms (iOS · Android · React Native).

**Frequency caps hold under load.** Two triggers arriving in quick succession could both pass a cap that allows only one show. Shows are now counted before the next campaign is evaluated.

**Sessions survive background/foreground churn.** Several defects where a session could be ended while the app was still using it, or where events fired during that window were rejected and lost. Events tracked around backgrounding now land in the session they belong to.

**Event-history targeting got much faster.** Answering a campaign's event conditions no longer rescans everything the app has ever recorded, so the cost no longer grows with how long the app has been installed.

**Fixed:** `getDataSetSnapshot` with `@triggeredEvent` counted every event sharing the name, including system events, and reported a type that disagreed with its own count. Campaign matching was never affected — this is the number you read while debugging.

## 0.6.1

All platforms (iOS · Android · React Native).

Internal improvements to how event conditions are evaluated on the device — no visible changes to authoring or behavior.

Event conditions now require apps built with SDK 0.6.1 or later. On older app builds (including 0.6.0), campaigns that use them simply never match — nothing breaks; they start matching once the app updates its SDK.

## 0.6.0

All platforms (iOS · Android · React Native).

**Event-history targeting** — campaign audience rules can now target what users have done, directly on their events. No extra code and no custom property to maintain: pick an event in the rule builder and the condition evaluates on the device, live and offline, like all targeting.

* **Counts** — "did `Purchase` at least 3 times", "`ExportClicked` has never happened". Operators: exactly, not exactly, more than, fewer than, at least, at most — plus has happened (ever) / has never happened. Counts cover the lifetime of the install.
* **First and last occurrence** — relative ("last `WorkoutCompleted` more than 14 days ago", with an "…or never" variant; "within the last 7 days") and absolute (before / after a date, whole calendar days in UTC).
* **Property filters** — narrow a condition to matching occurrences ("`Purchase` where `product` equals `pro`"); only matching occurrences count, for both counts and first/last dates.

Event conditions require apps built with SDK 0.6.0 or later. On older app builds, campaigns that use them simply never match — nothing breaks.

Fixes in this release:

* Campaigns limited to "no more than N impressions total" now reliably stop at N.
* Install-date conditions using "is" with a specific date now match the entire calendar day.
* A fresh install no longer briefly reports an incorrect install date.

See [Targeting and audiences](/concepts/targeting-and-audiences.md) for the model and [Targeting an audience](/user-guide/targeting-an-audience.md) for the dashboard walkthrough.

## 0.5.0

All platforms (iOS · Android · React Native).

**Mediated-gate contract** — campaigns can now be gated on an outcome your app controls.

* `track` — fire-and-forget event tracking, unchanged from earlier releases.
* `trackGated` — tracks an event and suspends the caller until the campaign action at that moment resolves; lets the app await a campaign's outcome (for example, a rewarded ad completing) before continuing its own flow.
* `registerGate` — registers a named gate your app exposes; Amply uses it to attach outcome-bearing actions to the moments you declare.

See [Gating an action](/concepts/gating-an-action.md) for the concept and [Gating actions](/developer-guide/gating-actions.md) for the SDK integration.

Install steps and compatibility notes are in the [Quickstart](/getting-started/quickstart.md) and [Developer Guide](/developer-guide/installation.md).

## See full history

Complete release notes, including older versions and bug-fix releases, live on GitHub:

* iOS SDK — [github.com/amply-tools/amply-sdk-ios/releases](https://github.com/amply-tools/amply-sdk-ios/releases)
* React Native SDK — [github.com/amply-tools/amply-react-native/releases](https://github.com/amply-tools/amply-react-native/releases)
* Kotlin / Android SDK — published to Maven Central under `tools.amply:sdk-android`; release notes are attached to the matching tags on GitHub.

Dashboard releases are rolled out continuously and are not versioned publicly.

## Related

* [Quickstart](/getting-started/quickstart.md) — install the current SDK
* [Developer Guide](/developer-guide/installation.md) — SDK reference
