Skip to content

Release 4.x.x - #32

Merged
vladd-g merged 37 commits into
mainfrom
release/4.0.0
Sep 9, 2026
Merged

vladd-g merged 37 commits into
mainfrom
release/4.0.0

Conversation

@mirzemehdi

@mirzemehdi mirzemehdi commented Jul 13, 2026 •

Copy link
Copy Markdown
Collaborator

KMPAdapty 4.0.0 (Paywall → Flow)

4.0.0 reorganizes the SDK around flows instead of paywalls.

The paywall API is deleted, not deprecated — your code won't compile until you port it. The substitutions are mechanical, and this guide lists them all.


TL;DR

  • getPaywall → getFlow, AdaptyPaywall → AdaptyFlow, AdaptyUIPaywallView → AdaptyUIFlowView, paywallView* callbacks → flowView*.
  • Some defaults changed — see Default behavior changes.
  • Onboarding APIs still work, but are now @Deprecated.

What is a flow?

A flow groups one or more paywall variations with flow-level metadata and per-language remote configs. Where getPaywall() returned one paywall, getFlow() returns an AdaptyFlow whose paywalls are AdaptyFlowPaywalls.

val flow = (Adapty.getFlow("placement_id") as? AdaptyResult.Success)?.value ?: return
val paywall: AdaptyFlowPaywall? = flow.paywalls.firstOrNull()

Installation

4.0.0 is a pre-release, so pin the exact version — Gradle does not resolve pre-releases through dynamic ranges (+, latest.release):

[versions]
adapty-kmp = "4.0.0-beta.1"

[libraries]
adapty-kmp = { module = "io.adapty:adapty-kmp", version.ref = "adapty-kmp" }
adapty-kmp-ui = { module = "io.adapty:adapty-kmp-ui", version.ref = "adapty-kmp" }

Core API (Adapty)

Removed Replacement
getPaywall(placementId, locale, fetchPolicy, loadTimeout) getFlow(placementId, fetchPolicy, loadTimeout)
getPaywallForDefaultAudience(placementId, locale, fetchPolicy) getFlowForDefaultAudience(placementId, fetchPolicy)
getPaywallProducts(paywall) getPaywallProducts(flow)
logShowPaywall(paywall) logShowFlow(flow)
createWebPaywallUrl(paywall) createWebPaywallUrl(flowPaywall) or createWebPaywallUrl(product)
openWebPaywall(paywall, openIn) openWebPaywall(flowPaywall, openIn) or openWebPaywall(product, openIn)

locale is gone from flow fetching — it's resolved at render time. Delete the argument.

getPaywallProducts, createWebPaywallUrl and openWebPaywall keep their paywall names: those are the v4 wire names.

// Before
val paywall = (Adapty.getPaywall("placement_id", locale = "en") as? AdaptyResult.Success)?.value ?: return
val products = Adapty.getPaywallProducts(paywall)
Adapty.logShowPaywall(paywall)

// After
val flow = (Adapty.getFlow("placement_id") as? AdaptyResult.Success)?.value ?: return
val products = Adapty.getPaywallProducts(flow)
Adapty.logShowFlow(flow)

Purchases, profile, and fallbacks (makePurchase, restorePurchases, getProfile, identify, updateProfile, setFallback) are unchanged.


UI API (AdaptyUI)

Removed Replacement
createPaywallView(paywall, …) → AdaptyUIPaywallView createFlowView(flow, …) → AdaptyUIFlowView
presentPaywallView / dismissPaywallView presentFlowView / dismissFlowView
setPaywallsEventsObserver setFlowsEventsObserver
register/unregisterPaywallEventsListener register/unregisterFlowEventsListener
AdaptyUI.createNativePaywallView(…) → AdaptyNativePaywallView AdaptyUI.createNativeFlowView(…) → AdaptyNativeFlowView
AdaptyUIPaywallPlatformView(paywall, …) AdaptyUIFlowPlatformView(flow, …)
— AdaptyUI.requestAppReview(), AdaptyUI.openWebUrl(url, openIn) (new)

Porting your observer

AdaptyUIPaywallsEventsObserver is deleted; implement AdaptyUIFlowsEventsObserver instead. Each callback has a direct equivalent: paywallView* becomes flowView* and takes AdaptyUIFlowView. One is named differently:

  • paywallViewDidFailRendering → flowViewDidReceiveError (widened to any flow error, not just rendering)

One event is new: flowViewDidReceiveAnalyticEvent(view, name, paramsJsonString). It defaults to a no-op.

Same for AdaptyUIFlowPlatformView: your onDid… params carry over, onDidFailRendering becomes onDidReceiveError, and onDidReceiveAnalyticEvent is added.

// Before
val paywall = (Adapty.getPaywall("placement_id") as? AdaptyResult.Success)?.value ?: return
AdaptyUIPaywallPlatformView(paywall = paywall, onDidFinishPurchase = { _, _, _ -> })

// After
val flow = (Adapty.getFlow("placement_id") as? AdaptyResult.Success)?.value ?: return
AdaptyUIFlowPlatformView(flow = flow, onDidFinishPurchase = { _, _, _ -> })

Default behavior changes

These don't cause compile errors, so check them at runtime:

Event v3 4.0.0
Close button dismisses dismisses
Android system back dismisses keeps open — dismiss yourself in flowViewDidPerformAction to restore
Purchase completed dismisses (unless cancelled) does not auto-dismiss
Error (none) dismisses
URL tapped opened by the SDK opened natively

To keep the old close-on-purchase behavior:

override fun flowViewDidFinishPurchase(
    view: AdaptyUIFlowView,
    product: AdaptyPaywallProduct,
    purchaseResult: AdaptyPurchaseResult
) {
    if (purchaseResult !is AdaptyPurchaseResult.UserCanceled) {
        mainUiScope.launch { view.dismiss() }
    }
}

Per-view observers (from registerFlowEventsListener or AdaptyUIFlowPlatformView) run in addition to the global observer, not instead of it — your callback observes an event, it doesn't replace the default.


Optional: system requests and observer mode

New in 4.0.0, with no v3 equivalent — skip unless you need them. Flows can ask your app for an OS permission or an in-app review, and observer mode can hand purchases to your app from inside a flow view. Both are registered globally, because each is a request the flow waits on an answer for.

AdaptyUI.setSystemRequestsHandler(object : AdaptyUISystemRequestsHandler {
    override suspend fun handlePermission(
        view: AdaptyUIFlowView,
        permission: AdaptyUIPermission,
        customArgs: Map<String, String>?,
    ): AdaptyUIPermissionResult = when (permission) {
        AdaptyUIPermission.PUSH ->
            if (requestNotifications()) AdaptyUIPermissionResult.granted()
            else AdaptyUIPermissionResult.denied("user declined")
        // Unknown / platform-specific / future ids arrive verbatim
        else -> AdaptyUIPermissionResult.denied("unsupported: ${permission.value}")
    }

    // Optional — defaults to the native review prompt
    override suspend fun handleAppReviewRequest(view: AdaptyUIFlowView) {
        myOwnReviewPrompt()
    }
})

AdaptyUI.setObserverModeResolver(object : AdaptyUIObserverModeResolver {
    override fun observerModeDidInitiatePurchase(
        view: AdaptyUIFlowView,
        product: AdaptyPaywallProduct,
        onStartPurchase: () -> Unit,
        onFinishPurchase: () -> Unit,
    ) {
        onStartPurchase()
        myPurchaseFlow(product) { onFinishPurchase() }
    }

    override fun observerModeDidInitiateRestore(
        view: AdaptyUIFlowView,
        onStartRestore: () -> Unit,
        onFinishRestore: () -> Unit,
    ) {
        onStartRestore()
        myRestoreFlow { onFinishRestore() }
    }
})

If you register a handler you must answer: handlePermission and both resolver methods are abstract. With no handler registered, a permission request resolves as denied when the flow tears down, and observer mode does nothing when the user taps buy. onStartPurchase / onFinishPurchase only drive the loading indicator — report the transaction to Adapty yourself.


Models

  • AdaptyFlow.paywalls — the flow's AdaptyFlowPaywall variations.
  • AdaptyUIPermission wraps the raw permission id, with the known ids as constants (AdaptyUIPermission.PUSH). Unknown, platform-specific (Android phone / sms) and future ids arrive verbatim via permission.value, so match with an else branch.
  • AdaptyUIPermissionResult.granted(detail) / .denied(detail) — what handlePermission returns.
  • AdaptyConfig.ServerCluster.CN — new, alongside DEFAULT and EU.

Native embedded views

// Android
val nativeView = AdaptyUI.createNativeFlowView(
    context = context,
    viewModelStoreOwner = activity,
    flow = flow,
    observer = myFlowObserver,
)
// nativeView.view → AdaptyFlowView; call nativeView.dispose() when done

// iOS
val nativeView = AdaptyUI.createNativeFlowView(flow = flow, observer = myFlowObserver)
// nativeView.viewController → UIViewController; call nativeView.dispose() when done

Deprecated onboarding

The whole onboarding surface is now @Deprecated — methods (getOnboarding, getOnboardingForDefaultAudience, createOnboardingView, presentOnboardingView, dismissOnboardingView, createNativeOnboardingView, setOnboardingsEventsObserver, register/unregisterOnboardingEventsListener), the observer (AdaptyUIOnboardingsEventsObserver), the composable (AdaptyUIOnboardingPlatformView), and the models (AdaptyOnboarding, AdaptyUIOnboardingView, AdaptyUIOnboardingMeta, AdaptyOnboardingsAnalyticsEvent, AdaptyOnboardingsInput, AdaptyOnboardingsStateUpdatedParams, AdaptyNativeOnboardingView).

Everything still works unchanged this release; a future major migrates onboardings into the Flow Builder. Suppress the warnings for now — no code change needed.


@mirzemehdi
mirzemehdi marked this pull request as ready for review July 13, 2026 19:27
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@StanislavMayorov

Copy link
Copy Markdown
Contributor

@titerman @eandreeva-twr please create a PR targeting release/4.0.0 that updates the README.md

@StanislavMayorov

Copy link
Copy Markdown
Contributor

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc1d136be5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapty/src/commonMain/kotlin/com/adapty/kmp/internal/AdaptyUIImpl.kt Outdated
Comment thread adapty-ui/src/commonMain/kotlin/com/adapty/kmp/ui/AdaptyUIFlowPlatformView.kt Outdated
@mirzemehdi

mirzemehdi commented Jul 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

@titerman @eandreeva-twr please create a PR targeting release/4.0.0 that updates the README.md

@titerman @eandreeva-twr if you need you can check migration guide file at https://github.com/adaptyteam/AdaptySDK-KMP/blob/a0344f9ba2c683ae5886860a119e4877b2429a8c/MIGRATION_GUIDE_4.0.0.md

@StanislavMayorov

Copy link
Copy Markdown
Contributor

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0344f9ba2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread gradle.properties Outdated
@StanislavMayorov

Copy link
Copy Markdown
Contributor

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 209ceaacd0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapty/src/commonMain/kotlin/com/adapty/kmp/models/AdaptyFlow.kt Outdated
Comment thread adapty-ui/src/commonMain/kotlin/com/adapty/kmp/ui/AdaptyUIFlowPlatformView.kt Outdated
Each embedded view now gets its own id. The id was derived from the
model (AdaptyFlow.instanceIdentity / AdaptyOnboarding.id), so embedding
the same flow twice produced one id for both views. The id keys the
per-view observer map and the native view manager, so the second view
evicted the first one's observer, events reached the wrong instance,
and disposing either unregistered the other. idForNativePlatformView
becomes createNativePlatformViewId(), minting a fresh id per view, and
that id is threaded through the platform views as an explicit param.
Mirrors React Native, which builds its view id from flow.id + useId().

Key the register/dispose effects on that id and read the callbacks
through rememberUpdatedState. Both effects were keyed on Unit, so
swapping the flow at the same composition position left the observer
registered under the old id with the first composition's lambdas —
callbacks silently stopped firing.
@StanislavMayorov

Copy link
Copy Markdown
Contributor

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f9ac6f520a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@StanislavMayorov

Copy link
Copy Markdown
Contributor

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e1140f8248

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapty-ui/src/commonMain/kotlin/com/adapty/kmp/ui/AdaptyUIFlowPlatformView.kt Outdated
Comment thread adapty/src/commonMain/kotlin/com/adapty/kmp/internal/AdaptyUIImpl.kt Outdated
vladd-g and others added 12 commits July 20, 2026 11:10
The native side defaults enable_safe_area_paddings to true, so an embedded
flow under an inset-consuming parent stayed blank: the insets guard never
settled. androidEnableSafeArea (Android-only) exposes the contract param.
The full-screen present path keeps the default true, while the embedded views
(AdaptyUIFlowPlatformView and AdaptyUI.createNativeFlowView) default to false,
since the host layout is expected to handle safe area and native paddings
would otherwise double up.
* Replace deprecated `capitalize()` calls with `replaceFirstChar`.
* Transition from `tasks.create` to `tasks.register` for lazy task configuration.
* Add `stripDebugInfo` task to remove debug symbols from the Swift bridge static library using `strip -S`.
* Improve task descriptions and refine the execution order to build, dedup, and then strip debug info.
…ll request #38 from adaptyteam/bugfix/open-issues

Fix log level handling and strip debug info from iOS bridge
Breaking:
- `Adapty.updateAttribution(attribution, source)` is now
  `updateExternalAttribution(attribution, provider)`, sending
  `update_external_attribution_data`.
- `AdaptyPaywallProductSubscription` is now `AdaptyProductSubscription`, following
  the `AdaptyProduct.Subscription` schema rename now that promoted products share
  the type.
- Paywall product requests send the offer identifier nested under
  `subscription.offer.offer_identifier` instead of `subscription_offer_identifier`.
- `createFlowView`, `createNativeFlowView` and `AdaptyUIFlowPlatformView` take a
  new optional `customLayoutId`.

Added:
- `Adapty.makePromotedPurchase` plus `AdaptyPromotedProduct` and
  `OnPromotedPurchaseListener`, fed by the `did_receive_promoted_purchase` event.
  Without a listener the native SDK completes the purchase itself.
- `AdaptyConfig.Builder.withAdaptyAttributionEnabled`. Adapty Attribution is now
  opt-in: installation details are not collected, and `OnInstallationDetailsListener`
  does not fire, until it is enabled.
- `AdaptyFlow.hasViewConfiguration`, mirroring the iOS property.
- `custom_layout_id` on `adapty_ui_create_flow_view`.

Versions: cross_platform 4.0.2 -> 4.1.2, adapty-bom 4.1.0,
crossplatform 4.1.2, iOS SPM 4.1.1.
@mirzemehdi mirzemehdi changed the title Release 4.0.0 Release 4.x.x Aug 25, 2026
mirzemehdi and others added 10 commits August 26, 2026 13:41
…ndling

- Add `AdaptyExternalAttributionProvider`, an open value type carrying the raw
  provider id with the six known providers as constants, so ids added by the
  backend after this release round-trip without an SDK update. Mirrors the iOS
  entity of the same name.
- Rename `AdaptyProfile.appliedAttributionSources` to
  `appliedExternalAttributionProviders` and type it with the new entity, and take
  the entity instead of a `String` in `updateExternalAttribution`, matching iOS.
  The wire field stays `applied_attribution_sources`; the 4.1.2 schema does not
  rename it.
- Guard `makePromotedPurchase` with `isAndroidPlatform`, returning
  `DEVELOPER_ERROR` like the other iOS-only methods.
- Log instead of silently dropping a promoted purchase that arrives with no
  `OnPromotedPurchaseListener` registered. The native SDK completes the purchase
  itself only when no delegate is set, and the plugin layer always installs one
  (`EventHandler.register`), so that fallback never applies here and the KDoc
  claiming otherwise was wrong. Without a listener the user tapped Buy on the
  App Store page and nothing happened.
- Bump the iOS SPM pin to 4.1.2,
- Hold a promoted purchase that arrives before setOnPromotedPurchaseListener and
  replay it on registration, instead of dropping it. The collector starts in
  AdaptyImpl.init, and a promoted purchase cold-launches the app, so the intent
  normally lands between activate and registration — the drop was on the common
  path, not an edge case. Only the latest is kept, and it is delivered exactly
  once, on the main scope like a live one.
- Log the held purchase through ConsoleLogger rather than `logger`. applyLogLevel
  is a whitelist, not a minimum level: VERBOSE and DEBUG get ConsoleLogger and
  everything else gets EmptyLogger, and AdaptyConfig defaults to INFO, so the
  warning was invisible on a default setup. AdaptyLogger has no severity to raise.
- Trim the raw value in AdaptyExternalAttributionProvider, matching iOS, Flutter
  and Unity, so an id with stray whitespace reaches the backend the same way from
  every platform. This means it can no longer be a data class, since a data class
  cannot normalize a constructor val; equals, hashCode and toString are explicit
  again, as on iOS.
- Annotate AdaptyFlow with @ConsistentCopyVisibility so its generated copy matches
  its internal constructor. copy carried AdaptyFlowLayoutsConfigurationRequestResponse
  in its signature, putting an internal wire model's name in the BCV dumps, where
  renaming it on a future cross_platform bump would read as a public API change.
  No consumer could reach it — Kotlin callers cannot name an internal type.

The replay is covered by a regression test that fails without the retain.
…42)

Custom image/video assets passed via Res.getUri(...) arrive as file:// URLs,
which native could not load:
- iOS builds URL(filePath:) from the string, so a file:// prefix breaks it.
- Android's transformFileLocation forced every path through
  FileLocation.fromAsset(...), so real on-disk files were looked up as APK
  assets and failed.

Strip the file:// scheme to a plain path before sending to native, while
preserving file:///android_asset/... so Android can still map it to an APK
asset. Route Android's transformFileLocation by form: android_asset ->
fromAsset, file://|/ -> fromFileUri, else -> fromAsset.

Example app: pass locale into createFlowView for the custom paywall present,
and supply local video custom assets on the presented and native-view paths.
…42) (#43)

Custom image/video assets passed via Res.getUri(...) arrive as file:// URLs,
which native could not load:
- iOS builds URL(filePath:) from the string, so a file:// prefix breaks it.
- Android's transformFileLocation forced every path through
  FileLocation.fromAsset(...), so real on-disk files were looked up as APK
  assets and failed.

Strip the file:// scheme to a plain path before sending to native, while
preserving file:///android_asset/... so Android can still map it to an APK
asset. Route Android's transformFileLocation by form: android_asset ->
fromAsset, file://|/ -> fromFileUri, else -> fromAsset.

Example app: pass locale into createFlowView for the custom paywall present,
and supply local video custom assets on the presented and native-view paths.
@vladd-g
vladd-g merged commit f3feb41 into main Sep 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants