Releases: Purchasely/Purchasely-Android
Release list
6.1.1
Purchasely Android SDK 6.1.1
✨ Improvements
- Accessibility. TalkBack now announces the rendered text of a button or a label all the time.
- Internal test and build maintenance, with no change to the public API.
Full changelog: 6.1.0...6.1.1
6.1.0
Purchasely Android SDK 6.1.0
6.1.0 is an additive release. There are no breaking changes, so an existing integration needs no code change.
The main feature is Web2App: your app now knows the result of a web subscription redemption, and can draw its own result screen.
🔗 Web-to-app funnels (redemption)
A user buys a subscription on your website, taps the link in the confirmation email, and lands in your app. The SDK now tells your app when the redemption settles.
Purchasely.Builder(applicationContext)
.apiKey("your-api-key")
.stores(listOf(GoogleStore()))
.webRedemptionListener(appHandlesRedemptionAlert = false) { result ->
when (result) {
// `replay` is a re-tap of a link already redeemed. Unlock, but do not
// thank the user twice.
is PLYWebRedemptionResult.Success -> unlockContent(result.context, result.replay)
is PLYWebRedemptionResult.Failure -> showError(result.errorMessage)
}
}
.build()
.start()Your listener receives onRedemptionCompleted(result) on the main thread, exactly once per settled redemption. The result carries the outcome and what the redemption granted, so you can unlock the content immediately.
appHandlesRedemptionAlert = false(default): the SDK shows its own success or failure popin, then calls your listener when the user closes it.appHandlesRedemptionAlert = true: the SDK shows no popin. Your listener receives the result as soon as the redemption settles, and your app owns the full post-redemption experience.
The listener is available on the builder only, because a redemption can settle during start(). The SDK holds it until Purchasely.close(), so do not let it capture an Activity.
PLYWebRedemptionResult is a sealed class with two cases:
Success(context, replay)—contextdescribes what the redemption granted, and itssubscriptioncarries the redeemed subscription when the server sent one.replayistruewhen the user taps a link that was already redeemed.Failure(errorCode, errorMessage)— the server-provided code and message.
User attributes from the web funnel. A successful redemption can restore the built-in and custom user attributes of the web purchase. The SDK applies them before the entitlements refresh, so every later event and every audience already sees them. Read them with the getters you already use.
Three notes:
- A redemption deeplink does not obey
allowDeeplink. A user who taps the link in the email always gets the subscription. - When a link has expired,
errorMessagecan contain a masked email address, for examplej***@example.com. Show it to the user. Do not send it to your analytics. - The redemption token never enters a log line, an analytics event, or the deeplink waiting list. Deduplication uses a SHA-256 hash of the token.
Two new analytics events: REDEMPTION_CONSUMED and REDEMPTION_FAILED. The SDK also sends the consumed event when the user taps an already redeemed link. A replay is a success, and the replay flag tells the two apart. Add the two cases if your app switches over the event type.
Read an event payload with event.properties.toMap(). The redemption payload types are opaque on purpose, so do not read their fields directly.
Documentation: Web-to-app funnels (redemption)
Also in this release
- Set the anonymous user id yourself.
anonymousUserId(id, override)on the builder gives the SDK thejava.util.UUIDyour app already uses. The SDK keeps an id that is already on the device, unless you passoverride = true. The SDK stores the id you pass in uppercase, and an id it generates itself in lowercase, so compare an anonymous user id case-insensitively. - API proxy for restricted regions.
proxy(api)routes the API traffic through your ownhttpsURL, for a region whereapi.purchasely.iois not reachable. The paywall and the tracking hosts stay on production.proxy(api = null)clears a proxy that an earlierbuild()set.
Fixes
start()always invokes its callback, exactly once. Aconfigure()that returned early no longer leaves the callback pending.- An inline
PLYPresentationViewno longer restores another paywall's state after a configuration change. A banner could render the full-screen paywall inside its banner-sized slot. - A screen that is re-attached no longer reports a second
PRESENTATION_VIEWED. - Every
handleDeeplink()of a session works. The consumed marker is scoped to the intent that carried the URL, so it no longer swallows every deeplink after the first one. - The redemption outcome alert reports to your listener when the user dismisses it.
Full list of the changes: docs.purchasely.com/changelog
6.0.2
6.0.1
Purchasely Android SDK 6.0.1
The stable release of the v6 major version. v6 modernizes the presentation API, reworks the action interceptor, makes storeless and observer-first the defaults, and tightens initialization. It supersedes the 6.0.0-rc.1 → 6.0.0-rc.3 release-candidate cycle — there is no separate 6.0.0 build; 6.0.1 is the version to integrate against.
This is a major release with breaking changes — read the full migration guide before upgrading from v5.
implementation("io.purchasely:core:6.0.1")
implementation("io.purchasely:google-play:6.0.1")
// implementation("io.purchasely:player:6.0.1") // optional🆕 New in 6.0.1 (since 6.0.0-rc.3)
- Theme mode is now configurable at initialization —
themeMode(PLYThemeMode)is available on bothPurchasely.Builderand the Kotlin DSL, applied before any paywall can render. Matches iOS. Purely additive: the default staysPLYThemeMode.SYSTEM, and runtimePurchasely.setThemeMode(...)afterstart()is unchanged.Purchasely { context(applicationContext) apiKey("your-api-key") themeMode(PLYThemeMode.DARK) } // or, fluent Builder: Purchasely.Builder(context).apiKey("…").themeMode(PLYThemeMode.DARK).build().start()
setDefaultPresentationDismissHandler(...)now acceptsnullto unregister — the parameter is nullable, so a previously-set default dismiss handler can be cleared (passingnullfrom Java no longer risks a runtime null-check). Matches iOS unregistration behavior.
All other work landed since rc.3 is internal (module restructuring, test hardening) and has no effect on the public API.
✨ Highlights & New Features
- New
PLYPresentationAPI — a complete builder / preload / display lifecycle with an observablestate: StateFlow<PLYPresentationState>(Idle → Loading → Loaded → Displayed → Dismissed/Error). Preload early, display later, no extra network call. PLYPresentationSession— everydisplay(...)returns a session handle. Fire-and-forget from Java, orawait()it from a coroutine to suspend until dismissal and get aPLYPresentationOutcome(structured concurrency +try/catch).- Granular action interceptor —
interceptAction<PLYPresentationAction.Purchase> { … }replaces the monolithicsetPaywallActionsInterceptor. Type-safe parameters per action, no casting. Available as a member ofPurchaselyin three forms:- reified Kotlin coroutine form:
Purchasely.interceptAction<PLYPresentationAction.Purchase> { info, action -> … }(resolves from thePurchaselyimport — no extra import needed); - Kotlin callback form without coroutines:
Purchasely.interceptAction(PLYPresentationAction.Purchase::class.java) { info, action, result -> … }; - Java callback form (same
Class-based overload).
- reified Kotlin coroutine form:
- Kotlin DSL entrypoint —
Purchasely { context(...); apiKey(...); … }configures and starts in one block, with editor-time Lint checks (PurchaselyMissingContext,PurchaselyMissingApiKey,PurchaselyFullModeWithoutStores). - Storeless integration is now first-class — screens, analytics, campaigns, deeplinks, and user attributes all work with no store configured.
- Automatic deeplink interception — the SDK reads the foreground activity's intent and routes its own URIs. No
handleDeeplink()call required (opt out viaautomaticDeeplinkHandling). - Independent campaign gating — new
allowCampaignsflag decouples campaign display from deeplink handling. synchronize()completion callbacks —synchronize(onSuccess, onError)for Observer mode; the subscriptions cache is refreshed beforeonSuccessfires.- Structured transition dimensions —
PLYTransitionsupports px (dp) and percentage for drawer/popin height and popin width. A dimension explicitly set to0px/0%is honored as-is; an absent dimension falls back to the surface default (drawer 60%, popin 50%, widthmatch_parent). - Theme mode —
PLYThemeMode.LIGHT/DARK/SYSTEMviasetThemeMode(...)/getThemeMode()at runtime, and (new in 6.0.1) viathemeMode(...)atBuilder/DSL init. - Logging — custom loggers now receive all messages regardless of
logLevel; newlogcatEnabledflag controls Logcat independently.
🔧 Behavioral Fixes
- Observer-mode auto-sync on interceptor
SUCCESS— in Observer mode, the SDK callssynchronize()automatically after a paywall Purchase or Restore action your interceptor handled and reported asSUCCESS, matching iOS. Do not callsynchronize()yourself inside the interceptor — it would double-sync.display(...)then resolves to aPURCHASEDoutcome with the plan (orRESTORED) rather thanCANCELLEDwith a null plan. (MOB-260) open_presentationno longer drops your dismiss callback — when a paywall action navigates to another presentation that has no callback of its own, the originalonDismissed/ close callback is preserved instead of being overwritten by a no-op, so a latercloseAllScreens()correctly reaches your handler. The secondary screen also reaches a terminalDismissedstate so observers no longer block indefinitely.- Default presentation dismiss handler fires for deeplinks —
setDefaultPresentationDismissHandler(...)now delivers a fullPLYPresentationOutcome(withpresentationpopulated) and correctly fires for deeplink-triggered presentations, which the v5 handler never did. - Bottom safe area is opt-in and no longer doubled — the navigation-bar safe area is driven per component by
safe_area_bottom(symmetric withsafe_area_top), so it no longer stacks with an unconditional inset. Layouts that need bottom spacing must setsafe_area_bottomon their bottom container. - User switch & logout/login — subscription refresh and dismissal on user switch are fixed; stale refresh jobs are cancelled so a fast identity change no longer races an in-flight refresh, and a fast
logout()→login()no longer lets the new user inherit the previous user's attributes. - Translations — corrected the Indonesian locale (
id→in, the JVM's legacy code) and added the missing web-checkout strings across languages.
⚠️ Breaking Changes (v5 → v6)
Initialization & running mode
- Default running mode is now
Observer(wasFull). Add.runningMode(PLYRunningMode.Full)if you want Purchasely to handle/validate purchases. In Observer mode, presentations no longer auto-close after purchase/restore. PLYRunningMode.PaywallObserver→PLYRunningMode.Observer(rename).apiKeyis validated atstart()— null/blank leaves the SDK inert and firesPLYError.Configuration.- Init callback signature simplified —
start { error -> }(dropped the leadingBoolean). Java:Function2<Boolean,PLYError,Unit>→Function1<PLYError,Unit>.
Action interceptor
- Removed
setPaywallActionsInterceptor(),PLYPresentationInfo,PLYPaywallActionHandler/PLYCompletionHandler,PLYPaywallActionListener/PLYProcessActionListener. PLYPresentationActionis now a sealed class (was enum);PLYPresentationActionParametersremoved (params live on each subclass).processAction(false/true)→PLYInterceptResult.SUCCESS/NOT_HANDLED(+ newFAILED).- Kotlin reified
interceptAction<T>requires consumerjvmTarget = 11(or use theClass-based overload).
Presentation API
- All presentation types moved to
io.purchasely.ext.presentation.*(import-only change). PLYPresentationPropertiesremoved — configure via the builder/DSL.Purchasely.presentationView(...)removed →PLYPresentation { … }.preload { … }.PLYPresentation.id→screenId(alsotoMap()key"id"→"screenId").- Suspend
display()extension removed —Loaded.display()is now non-suspend. onClose→onCloseRequested;PLYPresentationClose→PLYPresentationCloseRequested.- Display callbacks now receive a single
PLYPresentationOutcome(carriespurchaseResult,plan,closeReason,error) instead of(result, plan)+ separate error. back()/close()are now onPLYPresentation(Loaded) only.setDefaultPresentationResultHandler→setDefaultPresentationDismissHandler(rename; delivers a fullPLYPresentationOutcome). The analytics presence marker is renamed accordingly (DEFAULT_PRESENTATION_RESULT_HANDLER→DEFAULT_PRESENTATION_DISMISSED_HANDLER).PLYProductViewResultdeprecated →PLYPurchaseResult.
Removed surfaces
- Subscription list & cancellation survey UI fully removed —
subscriptionsFragment(), allPLYSubscription*/cancellation fragments & views, theply/subscriptions&ply/cancellation_surveydeeplinks, and 5 relatedPLYEventsubclasses. Build custom UI fromuserSubscriptions()/userSubscriptionsHistory(). Purchasely.displaySubscriptionCancellationInstruction(...)removed — this helper dialog is dropped as part of aligning the Android surface with iOS. Route users to the store's native manage-subscription screen (e.g. Google Play's Manage subscriptions).- Purchase history —
purchaseHistory()andisPastSubscriber()removed →userSubscriptionsHistory(). PLYPlanintro*methods removed →offer*equivalents (direct rename).PLYPlanTagsINTRO_*/TRIAL_*removed →OFFER_*.
Behavioral changes
allowDeeplinknow defaults totrue(wasfalse). Set.allowDeeplink(false)to keep v5 deferred behavior. Preview deeplinks (?preview=1) always display immediately.- Storeless errors — purchase/restore now return
PLYError.NoStoreConfigured(wasPLYError.Unknown"No store found"). - User attribute mutators return
Deferred<Boolean>(`setUserAttribut...
6.0.0-rc.3
Warning
v6 is currently in a release-candidate phase — use 6.0.0-rc.3, not 6.0.0. 6.0.0-rc.3 is the official build to integrate against for early integration and testing while v6 stabilizes. Use it to validate your migration and report any issues; the stable 6.0.0 will be announced once the RC cycle completes.
implementation("io.purchasely:core:6.0.0-rc.3")
implementation("io.purchasely:google-play:6.0.0-rc.3")
// implementation("io.purchasely:player:6.0.0-rc.3") // optionalPurchasely Android SDK 6.0.0-rc.3
The third release candidate of the v6 major version. It builds on 6.0.0-rc.2 with bug fixes, API refinements, and a toolchain bump. The full set of v6 breaking changes is documented in the migration guide; the sections below cover only what changed since rc.2.
⚠️ Breaking Changes (since rc.2)
interceptAction<T>/removeActionInterceptor<T>are now members ofPurchasely, not top-level extension functions. If an earlier6.0.0-rcbuild had you add an explicit import for the reified Kotlin overloads, delete it — the member call now resolves from thePurchaselyimport you already have. No call-site change is needed.- import io.purchasely.ext.interceptAction // remove - import io.purchasely.ext.removeActionInterceptor // remove Purchasely.interceptAction<PLYPresentationAction.Purchase> { info, purchase -> … } // unchanged
Purchasely.displaySubscriptionCancellationInstruction(activity, themeId)removed. This helper dialog is dropped as part of aligning the Android public surface with iOS. Route users to the store's native manage-subscription screen instead (e.g. Google Play's Manage subscriptions).
✨ Highlights & Fixes
- Observer-mode auto-sync on interceptor
SUCCESS— in Observer mode, the SDK now callssynchronize()automatically after a paywall Purchase or Restore action that your interceptor handled and reported asSUCCESS, matching iOS. Previously the transaction was never reported unless you calledsynchronize()yourself, corrupting analytics and subscription state. Remove any manualsynchronize()you added inside the interceptor — it would now double-sync. (MOB-260) open_presentationno longer drops your dismiss callback — when a paywall action navigates to another presentation that has no callback of its own, the originalonDismissed/ close callback is now preserved instead of being overwritten by a no-op, so a latercloseAllScreens()correctly reaches your handler. The secondary screen also reaches a terminalDismissedstate so observers no longer block indefinitely.- Bottom safe area is opt-in again and no longer doubled — the navigation-bar safe area is now driven per component by
safe_area_bottom(symmetric withsafe_area_top). A previously unconditional inset stacked with the per-component one, doubling the CTA-to-bottom gap — large on 3-button-navigation devices, small on gesture-navigation ones. Layouts that need bottom spacing must setsafe_area_bottomon their bottom container, as they already do forsafe_area_top. - Kotlin interceptor without coroutines — a new
Class-based overload lets Kotlin callers register an interceptor without asuspendlambda, returning the outcome via aresult(…)callback:Purchasely.interceptAction(PLYPresentationAction.Purchase::class.java) { info, action, result -> … }. The reified coroutine form and the Java overload are unchanged. - Translations — corrected the Indonesian locale (
id→in, the JVM's legacy code) and added the missing web-checkout strings across languages.
📦 Publishing & Artifacts
- Fat-AAR now declares its external dependencies — the published
:corePOM and Gradle module metadata now correctly listkotlinx-serialization-jsonandandroidx.core:core-ktx, which were previously stripped along with the internal modules merged into the AAR. Consumers no longer risk missing these transitive dependencies. - Javadoc jar populated again — the published
-javadoc.jarwas effectively empty in rc.2; it now ships the full Dokka output, so IDE documentation resolves against the artifact.
🛠 Build Requirements
- compileSdk
36(Android 16) — up from 35.targetSdkstays 35, so there is no runtime behavior change. - Kotlin
2.3.21toolchain — up from 2.2.x, picked up via AGP 9's built-in Kotlin. The published modules still pinlanguageVersionto 2.0, so consuming apps only need Kotlin 2.0+ — no consumer change required. - Bundled bumps: kotlinx-coroutines
1.11.0, kotlinx-serialization1.11.0, Dokka2.2.0, Gradle wrapper9.6.1. - Unchanged from rc.2: Java target 11, minimum Gradle 9.3.0.
6.0.0-rc.2
Warning
v6 is currently in a release-candidate phase — use 6.0.0-rc.2, not 6.0.0. 6.0.0-rc.2 is the official build to integrate against for early integration and testing while v6 stabilizes. Use it to validate your migration and report any issues; the stable 6.0.0 will be announced once the RC cycle completes.
implementation("io.purchasely:core:6.0.0-rc.2")
implementation("io.purchasely:google-play:6.0.0-rc.2")
// implementation("io.purchasely:player:6.0.0-rc.2") // optionalPurchasely Android SDK 6.0.0-rc.2
The second release candidate of the v6 major version. It builds on 6.0.0-rc.1 with bug fixes and one API rename. The full set of v6 breaking changes is documented in the migration guide; the sections below cover only what changed since rc.1.
⚠️ Breaking Changes (since rc.1)
setDefaultPresentationResultHandler→setDefaultPresentationDismissHandler(rename, same signature). Matches iOS and reflects that it delivers a fullPLYPresentationOutcome(withpresentationpopulated), not a bare result. The handler now also correctly fires for deeplink presentations (previously it never did). The analytics presence marker is renamed accordingly (DEFAULT_PRESENTATION_RESULT_HANDLER→DEFAULT_PRESENTATION_DISMISSED_HANDLER).
✨ Highlights & Fixes
- Observer-mode purchase outcomes — when a paywall purchase/restore is handled by your action interceptor (returns
SUCCESS) and reported viasynchronize(),display(...)now resolves to aPURCHASEDoutcome with the plan (orRESTORED), instead ofCANCELLEDwith a null plan. Mirrors Full-mode behavior. - User switch refresh & dismissal — fixed the subscription refresh and dismissal flow on user switch; stale subscription-refresh jobs are now cancelled so a fast identity change no longer races an in-flight refresh.
- Logout → login attribute leak — logout cleanup (built-in attributes and, when requested, user attributes) now runs outside the cancellable refresh job, so a fast
logout()→login()sequence no longer lets the new user inherit the previous user's attributes. Built-in cleanup runs off the caller thread to avoid StrictMode/jank. - Transition dimensions honor
0— a drawer/popin height or popin width configured to0px/0%is now rendered as-is instead of silently falling back to the surface default (drawer 60%, popin 50%, widthmatch_parent). An absent dimension still uses the default.
🛠 Build Requirements
- Unchanged from rc.1: Minimum Gradle 9.3.0, Kotlin 2.2.x (K2), Java target 11.
5.7.5
6.0.0-rc.1
Warning
v6 is currently in a release-candidate phase — use 6.0.0-rc.1, not 6.0.0. 6.0.0-rc.1 is the official build to integrate against for early integration and testing while v6 stabilizes. Use it to validate your migration and report any issues; the stable 6.0.0 will be announced once the RC cycle completes.
implementation("io.purchasely:core:6.0.0-rc.1")
implementation("io.purchasely:google-play:6.0.0-rc.1")
// implementation("io.purchasely:player:6.0.0-rc.1") // optionalPurchasely Android SDK 6.0.0-rc.1
The first release candidate of the v6 major version. v6 modernizes the presentation API, reworks the action interceptor, makes storeless and observer-first the defaults, and tightens initialization. This is a major release with breaking changes — read the full migration guide before upgrading.
✨ Highlights & New Features
- New
PLYPresentationAPI — a complete builder / preload / display lifecycle with an observablestate: StateFlow<PLYPresentationState>(Idle → Loading → Loaded → Displayed → Dismissed/Error). Preload early, display later, no extra network call. PLYPresentationSession— everydisplay(...)returns a session handle. Fire-and-forget from Java, orawait()it from a coroutine to suspend until dismissal and get aPLYPresentationOutcome(structured concurrency +try/catch).- Granular action interceptor —
interceptAction<PLYPresentationAction.Purchase> { … }replaces the monolithicsetPaywallActionsInterceptor. Type-safe parameters per action, no casting. - Kotlin DSL entrypoint —
Purchasely { context(...); apiKey(...); … }configures and starts in one block, with editor-time Lint checks (PurchaselyMissingContext,PurchaselyMissingApiKey,PurchaselyFullModeWithoutStores). - Storeless integration is now first-class — screens, analytics, campaigns, deeplinks, and user attributes all work with no store configured.
- Automatic deeplink interception — the SDK reads the foreground activity's intent and routes its own URIs. No
handleDeeplink()call required (opt out viaautomaticDeeplinkHandling). - Independent campaign gating — new
allowCampaignsflag decouples campaign display from deeplink handling. synchronize()completion callbacks —synchronize(onSuccess, onError)for Observer mode; the subscriptions cache is refreshed beforeonSuccessfires.- Structured transition dimensions —
PLYTransitionsupports px (dp) and percentage for drawer/popin height and popin width. - Logging — custom loggers now receive all messages regardless of
logLevel; newlogcatEnabledflag controls Logcat independently.
⚠️ Breaking Changes
Initialization & running mode
- Default running mode is now
Observer(wasFull). Add.runningMode(PLYRunningMode.Full)if you want Purchasely to handle/validate purchases. In Observer mode, presentations no longer auto-close after purchase/restore. PLYRunningMode.PaywallObserver→PLYRunningMode.Observer(rename).apiKeyis validated atstart()— null/blank leaves the SDK inert and firesPLYError.Configuration.- Init callback signature simplified —
start { error -> }(dropped the leadingBoolean). Java:Function2<Boolean,PLYError,Unit>→Function1<PLYError,Unit>.
Action interceptor
- Removed
setPaywallActionsInterceptor(),PLYPresentationInfo,PLYPaywallActionHandler/PLYCompletionHandler,PLYPaywallActionListener/PLYProcessActionListener. PLYPresentationActionis now a sealed class (was enum);PLYPresentationActionParametersremoved (params live on each subclass).processAction(false/true)→PLYInterceptResult.SUCCESS/NOT_HANDLED(+ newFAILED).- Kotlin reified
interceptAction<T>requires consumerjvmTarget = 11(or use theClass-based overload).
Presentation API
- All presentation types moved to
io.purchasely.ext.presentation.*(import-only change). PLYPresentationPropertiesremoved — configure via the builder/DSL.Purchasely.presentationView(...)removed →PLYPresentation { … }.preload { … }.PLYPresentation.id→screenId(alsotoMap()key"id"→"screenId").- Suspend
display()extension removed —Loaded.display()is now non-suspend. onClose→onCloseRequested;PLYPresentationClose→PLYPresentationCloseRequested.- Display callbacks now receive a single
PLYPresentationOutcome(carriespurchaseResult,plan,closeReason,error) instead of(result, plan)+ separate error. back()/close()are now onPLYPresentation(Loaded) only.PLYProductViewResultdeprecated →PLYPurchaseResult.
Removed surfaces
- Subscription list & cancellation survey UI fully removed —
subscriptionsFragment(), allPLYSubscription*/cancellation fragments & views, theply/subscriptions&ply/cancellation_surveydeeplinks, and 5 relatedPLYEventsubclasses. Build custom UI fromuserSubscriptions()/userSubscriptionsHistory(). - Purchase history —
purchaseHistory()andisPastSubscriber()removed →userSubscriptionsHistory(). PLYPlanintro*methods removed →offer*equivalents (direct rename).PLYPlanTagsINTRO_*/TRIAL_*removed →OFFER_*.
Behavioral changes
allowDeeplinknow defaults totrue(wasfalse). Set.allowDeeplink(false)to keep v5 deferred behavior. Preview deeplinks (?preview=1) always display immediately.- Storeless errors — purchase/restore now return
PLYError.NoStoreConfigured(wasPLYError.Unknown"No store found"). - User attribute mutators return
Deferred<Boolean>(setUserAttribute,clearUserAttribute,increment/decrementUserAttribute, etc.) — safe to ignore.
🛠 Build Requirements
- Minimum Gradle 9.3.0, Kotlin 2.2.x (K2), Java target 11.
5.7.4
Bug fixes
- Anonymous → identified user transfer in Observer mode — purchases made before login are now correctly carried over to the user account on
userLogin()when the SDK runs inPaywallObservermode. Previously, receipts reported throughsynchronize()did not flag the user as having purchased, so the transfer step was skipped at next login. - Screen re-render crash — fixed
IllegalStateException: The specified child already has a parentthat could be thrown when a screen was rebuilt in rapid succession (orientation changes, repeated rendering). Children are now safely detached and released before being re-added. - Modal/Drawer flow crash — fixed an
IllegalStateExceptionraised when the modal or drawer from a Flowstate changed after the host fragment had been detached (e.g. fast back-press or activity teardown). The callback now no-ops instead of dereferencing a detached activity.
5.7.3
🐛 Bug Fixes
Flow system — process death survival
The flow system now correctly saves and restores its full navigation state when Android kills and restores the process. No more blank screens or lost navigation history after the OS reclaims memory in the background.
Flow system — purchase callback reliability
Fixed an issue where the display() callback could return CANCELLED instead of PURCHASED after a successful in-app purchase inside a multi-step flow.
Paywalls — plan picker race condition
Fixed an intermittent bug where plan picker labels could display the wrong plan's price (e.g. annual price on the monthly label) when Google Play products hadn't loaded yet.
WebView — crash on non-exported activities
Tapping a link inside a paywall WebView no longer crashes when a third-party app (e.g. OPay) registers a non-exported Activity for that URL scheme. The SDK now gracefully falls back to loading the URL in the WebView.