Skip to content

build: update to AGP 9 (AGP 9.2.1, Gradle 9.4.1, Kotlin 2.2.21) - #3964

Merged
AlvaroBrey merged 12 commits into
11.0-devfrom
build/agp9-bump
Aug 27, 2026
Merged

build: update to AGP 9 (AGP 9.2.1, Gradle 9.4.1, Kotlin 2.2.21)#3964
AlvaroBrey merged 12 commits into
11.0-devfrom
build/agp9-bump

Conversation

@AlvaroBrey

@AlvaroBrey AlvaroBrey commented Aug 13, 2026

Copy link
Copy Markdown
Member

Note

This targets branch 11.0-dev which will become the epic branch for the upcoming major release. Once this first PR is merged, I'll open a continuous one from that one to main until we are ready to release it altogether.

  • Moves the SDK's build toolchain to AGP 9:
Dependency From To
AGP 8.13.2 9.2.1
Gradle 8.14.5 9.4.1
Kotlin 2.0.21 2.2.21
Poko 0.17.2 0.20.2
Paparazzi 2.0.0-alpha02 2.0.0-alpha05
  • No public API change and no api*.txt diff.
  • Why this becomes a major: this transitively raises the miminum kotlin version for consumers to 2.1.0. up from 1.8.0.
  • Kotlin lands on 2.2.21, the minimum required by AGP 9,
  • AGP is capped at 9.2.1 rather than latest: AGP 9.3.x requires Gradle 9.5+, and Gradle 9.5+ embeds a Kotlin that a 2.2 compiler cannot read, bumping consumers to an even higher minimum Kotlin version.
  • Nothing downstream needs AGP 9 or Gradle 9, just the Kotlin bump. The published AAR metadata is unchanged (minCompileSdk=1, minAndroidGradlePluginVersion=1.0.0).

Companion docs PR: https://github.com/RevenueCat/docs/pull/1942
Companion PHC PR: RevenueCat/purchases-hybrid-common#1844

Checklist

  • If applicable, unit tests
  • If applicable, create follow-up issues for purchases-ios and hybrids
Agent description

Motivation

AGP 8 is increasingly behind, and AGP 9 removes a number of DSL surfaces we still used. The
prerequisite work already landed separately in #3931, #3961 and #3963.

AGP 9 has a runtime dependency on Kotlin Gradle Plugin 2.2.10 or higher, so a Kotlin bump is not
optional here. Everything above that floor is a choice, and this PR deliberately takes the minimum.

Why not Kotlin 2.3.x

An earlier revision of this branch used Kotlin 2.3.21. Measuring the cost changed the decision:

  • It breaks every React Native consumer. We publish kotlin-stdlib at compile scope and Gradle
    resolves consumers onto it. A compiler reads metadata at most one minor ahead. Kotlin 2.3.x
    publishes metadata 2.3.0, and React Native's own Gradle plugin pins the compiler: 2.0.21 through
    RN 0.79, 2.1.20 through RN 0.86, with only RN main on 2.2.0. Reproduced on a real RN 0.86 app:
    metadata version is 2.3.0 / compiler version 2.1.0 can read versions up to 2.2.0. Every RN app
    would have needed an explicit kotlin-gradle-plugin version in android/build.gradle.
  • It forces the K2 migration. Kotlin 2.3.21 rejects languageVersion 1.8 outright. 2.2.x still
    accepts it, so the frontend migration stays out of a version bump where it does not belong.
  • It does not avoid a second bump anyway. Kotlin 2.4 removes languageVersion 1.8 regardless.

At Kotlin 2.2.21 the published metadata is 2.2.0, which RN 0.81+ reads untouched.

Why AGP 9.2.1 and Gradle 9.4.1

This is the non-obvious constraint, and CI found it rather than local builds.

:codegen is a java-gradle-plugin module, so it compiles against the Kotlin that Gradle itself
embeds
. Embedded Kotlin by Gradle release:

Gradle Embedded Kotlin Readable by a 2.2 compiler (max 2.3.0)
9.3.1 2.2.21 yes
9.4.1 2.3.0 yes, exactly at the limit
9.5.1 2.3.20 no
9.7.0 2.4.0 no

Gradle 9.7.0 therefore failed prepare-tests with
metadata version is 2.4.0, but the compiler version 2.2.0 can read versions up to 2.3.0.

AGP 9.3.x requires Gradle 9.5.0 or higher (Minimum supported Gradle version is 9.5.0), which is
exactly the range we cannot read. AGP 9.2.1 is the newest release that runs on Gradle 9.4.1;
9.1.1 and 9.0.1 were also confirmed to work, 9.3.1 was confirmed not to.

So the AGP ceiling here is a consequence of the Kotlin floor, not an independent preference. When the
deferred Kotlin 2.3 bump happens, AGP and Gradle can move up with it.

Also in this PR

  • Standalone sample apps (CustomEntitlementComputationSample, MagicWeather, MagicWeatherCompose)
    move to Kotlin 2.2.21 and gain the compose-compiler plugin alias. They consume our published
    artifacts, so they need a compiler that can read our metadata.
  • Unit tests are re-enabled on the release variant via HasUnitTestBuilder.enableUnitTest, because
    AGP 9 only creates unit tests for the testBuildType variant and the published variant is release.
  • The androidDependencies cache-warm CI step is dropped; AGP 9 removed that task.
  • migrations/v11-MIGRATION.md documents the new Kotlin floor, mirroring the v9 guide.
  • README Requirements now says Kotlin 2.1.0+.

Deferred on purpose

  • Kotlin 2.3 and the K1 to K2 migration, until React Native ships the KGP 2.2 pin already on its
    main branch. Doing it sooner buys nothing and costs every RN app a migration step.
  • AGP 9 built-in Kotlin (previously build: migrate to AGP 9 built-in Kotlin #3965). Not breaking and not required; the
    android.builtInKotlin=false / android.newDsl=false opt-out only has to go at AGP 10.

Testing

  • assembleDebug across all modules; release AARs for :purchases and :ui:revenuecatui.
  • :purchases unit tests, :ui:debugview:verifyPaparazziDefaultsDebug, detektAll.
  • scripts/api-check.sh with zero api*.txt diff.
  • Published contract checked directly: POM carries kotlin-stdlib:2.2.21 at compile scope, the
    release AAR still reports minCompileSdk=1 and minAndroidGradlePluginVersion=1.0.0, and our own
    classes carry metadata mv = {1, 8, 0}.
  • Consumer floor measured against the real release AAR plus the stdlib we publish: Kotlin 2.1.21
    compiles; 2.0.21 and 1.9.24 fail. That is what the 2.1.0 claim in the README rests on.

Downstream

The Kotlin floor is what propagates, not AGP. Nothing downstream needs AGP 9 or Gradle 9.

The only direct consumer is purchases-hybrid-common, which re-exposes us at compile scope via
api(libs.purchases). RevenueCat/purchases-hybrid-common#1844 raises its Kotlin accordingly, and the
hybrid SDKs pick the floor up from there rather than from this repo.

Documentation follows separately:


Note

Medium Risk
Major toolchain and minimum Kotlin version changes affect all consumers and CI, though public API and published AAR AGP/minSdk metadata are intentionally unchanged.

Overview
This PR moves the SDK build to AGP 9.2.1, Gradle 9.4.1, and Kotlin 2.2.21, and bumps related tooling (Poko, Paparazzi, baseline profile plugin). gradle.properties opts out of AGP 9’s built-in Kotlin and new DSL for now.

The consumer-facing change is a higher Kotlin floor: README and migrations/v11-MIGRATION.md document Kotlin 2.1.0+ (up from 1.8.0), driven by published stdlib metadata—not a public API change.

Build-logic re-enables unit tests on library variants under AGP 9 via enableUnitTest, and keeps aarMetadata.minCompileSdk = 1 so published AAR requirements don’t tighten.

Compose sample apps switch to the Kotlin Compose compiler plugin and drop composeOptions.kotlinCompilerExtensionVersion. Several modules replace deprecated kotlinOptions with kotlin.compilerOptions.jvmTarget.

CircleCI drops the removed androidDependencies warmup and uses Gradle cache restore on standalone sample builds instead.

Reviewed by Cursor Bugbot for commit 1b6b4df. Bugbot is set up for automated code reviews on this repo. Configure here.

@AlvaroBrey AlvaroBrey self-assigned this Aug 13, 2026
@socket-security

socket-security Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedmaven/​org.jetbrains.kotlin/​kotlin-stdlib@​1.8.22 ⏵ 2.2.2128 -810090100100
Updatedmaven/​org.jetbrains.kotlin/​kotlin-test@​2.0.21 ⏵ 2.2.214810090100100
Updatedmaven/​org.jetbrains.kotlin/​kotlin-gradle-plugin@​2.0.21 ⏵ 2.2.2163 +3599100 +11100100

View full report

@socket-security

socket-security Bot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: maven org.jetbrains.kotlin:kotlin-daemon-client is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?maven/org.jetbrains.kotlin/kotlin-gradle-plugin@2.2.21maven/org.jetbrains.kotlin/kotlin-daemon-client@2.2.21

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore maven/org.jetbrains.kotlin/kotlin-daemon-client@2.2.21. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: maven org.jetbrains.kotlin:kotlin-gradle-plugin is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: examples/CustomEntitlementComputationSample/gradle/libs.versions.tomlmaven/org.jetbrains.kotlin/kotlin-gradle-plugin@2.2.21

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore maven/org.jetbrains.kotlin/kotlin-gradle-plugin@2.2.21. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: maven org.jetbrains.kotlin:kotlin-gradle-plugin is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: examples/CustomEntitlementComputationSample/gradle/libs.versions.tomlmaven/org.jetbrains.kotlin/kotlin-gradle-plugin@2.2.21

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore maven/org.jetbrains.kotlin/kotlin-gradle-plugin@2.2.21. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@emerge-tools

emerge-tools Bot commented Aug 13, 2026

Copy link
Copy Markdown

2 builds increased size

Name Version Download Change Install Change Approval
TestPurchasesUIAndroidCompatibility
com.revenuecat.testpurchasesuiandroidcompatibility
1.0 (1) 72.3 MB ⬆️ 82.7 kB (0.11%) 117.2 MB ⬆️ 320.1 kB (0.27%) N/A
SDKSizeTesting
com.revenuecat.testapps.sdksizetesting
1.0 (1) 12.5 MB ⬆️ 34.4 kB (0.28%) 36.2 MB ⬆️ 152.1 kB (0.42%) ⏳ Needs approval

TestPurchasesUIAndroidCompatibility 1.0 (1)
com.revenuecat.testpurchasesuiandroidcompatibility

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 320.1 kB (0.27%)
Total download size change: ⬆️ 82.7 kB (0.11%)

Largest size changes

Item Install Size Change Download Size Change
kotlin.jvm.internal.ClassReference$Companion ⬆️ 5.5 kB ⬆️ 2.0 kB
📝 com.android.tools.r8.DesugarVarHandle ⬆️ 7.0 kB ⬆️ 1.7 kB
📝 kotlin.ContextParametersKt__ContextKt ⬆️ 3.7 kB ⬆️ 1.3 kB
com.revenuecat.purchases.PurchasesOrchestrator ⬇️ -4.8 kB ⬇️ -1.1 kB
kotlin.collections.ArraysKt___ArraysKt ⬆️ 3.5 kB ⬆️ 921 B
View Treemap

Image of diff

SDKSizeTesting 1.0 (1)
com.revenuecat.testapps.sdksizetesting

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 152.1 kB (0.42%)
Total download size change: ⬆️ 34.4 kB (0.28%)

Largest size changes

Item Install Size Change Download Size Change
kotlin.jvm.internal.ClassReference$Companion ⬆️ 4.9 kB ⬆️ 1.6 kB
baseline.prof ⬆️ 1.5 kB ⬆️ 1.5 kB
📝 kotlin.ContextParametersKt__ContextKt ⬆️ 2.5 kB ⬆️ 808 B
Other ⬆️ 143.2 kB ⬆️ 30.5 kB
View Treemap

Image of diff


🛸 Powered by Emerge Tools

@emerge-tools

emerge-tools Bot commented Aug 13, 2026

Copy link
Copy Markdown

📸 Snapshot Test

31 modified, 590 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
TestPurchasesUIAndroidCompatibility
com.revenuecat.testpurchasesuiandroidcompatibility
0 0 0 0 339 0 N/A
TestPurchasesUIAndroidCompatibility Paparazzi
com.revenuecat.testpurchasesuiandroidcompatibility.paparazzi
0 0 31 0 251 0 ✅ Approved

🛸 Powered by Emerge Tools

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.22%. Comparing base (add9676) to head (1b6b4df).

Additional details and impacted files
@@             Coverage Diff              @@
##           11.0-dev    #3964      +/-   ##
============================================
+ Coverage     83.19%   83.22%   +0.03%     
============================================
  Files           448      448              
  Lines         18098    18095       -3     
  Branches       2736     2736              
============================================
+ Hits          15057    15060       +3     
+ Misses         2147     2141       -6     
  Partials        894      894              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@AlvaroBrey
AlvaroBrey changed the base branch from build/agp9-prep to graphite-base/3964 August 13, 2026 19:22
@AlvaroBrey
AlvaroBrey changed the base branch from graphite-base/3964 to main August 14, 2026 07:15
@AlvaroBrey
AlvaroBrey force-pushed the build/agp9-bump branch 2 times, most recently from 38b3a97 to 7e959d5 Compare August 14, 2026 09:13
@AlvaroBrey AlvaroBrey changed the title build: AGP 9.3.1 + Gradle 9.7.0 + Kotlin 2.3.21 + Poko 0.22.1 + Paparazzi alpha05 build: update to AGP 9 (AGP 9.2.1, Gradle 9.4.1, Kotlin 2.2.21) Aug 25, 2026
@AlvaroBrey AlvaroBrey added the pr:breaking Changes that are breaking label Aug 25, 2026
@AlvaroBrey
AlvaroBrey changed the base branch from main to 11.0-dev August 27, 2026 09:40
… alpha05

These cannot be separated. Kotlin 2.3 needs Poko 0.21+, which requires
Gradle 9.0, which is what AGP 9 runs on; and Paparazzi alpha02 breaks on
Gradle 9 while alpha05 ships Kotlin 2.3 metadata, so it cannot land
earlier either.

Forced along the way:

- kotlinLanguage 1.8 to 2.0. Kotlin 2.3 rejects language version 1.8
  outright, and anything below 2.0 selects the K1 frontend, so this is
  also the switch to K2.
- kotlinOptions{} is AGP DSL that AGP 9 removes, and the String jvmTarget
  form is gone in Kotlin 2.3, so three app modules move to the Kotlin
  compilerOptions DSL.
- sdksizetesting is a standalone build; its Kotlin only has to read the
  SDK's metadata, and a compiler reads one minor ahead, so 2.2.21 suffices.
- One opt-in the K1 frontend did not require at the call site.

AGP 9 enables built-in Kotlin and rejects the kotlin-android plugin.
Opting out keeps this change reviewable; migrating to built-in Kotlin is
the follow-up, and both flags are removed by AGP 10.
AGP 9 removed the task, whose only job here was to populate ~/.gradle
between the orb's restore and save. Deleting just the task would leave
restore immediately followed by save, and since the orb keys that cache on
the root build's files and save_cache is write-once, a job could claim the
key with an unpopulated cache.

Only the admob lane builds in the root build, so it keeps restore and
save. The other two lanes build nested Gradle projects and now restore
only.
The three nested example builds resolve purchases 10.17.0-SNAPSHOT from
the Sonatype snapshot repo and were pinned at Kotlin 1.8.22 / 1.9.0. A
compiler reads metadata at most one minor ahead, so once main publishes a
snapshot built with 2.3.x they stop compiling. Nothing here would have
caught it: their jobs only run in snapshot-deploy-sample-app-tests, on
main, after the snapshot is published.

Kotlin 2.0+ also requires the Compose Compiler Gradle plugin wherever
compose is enabled.
AGP 9 only creates unit tests for the testBuildType variant, so release
variants silently lose theirs. Release is what we publish, and the only
variant where BuildConfig.DEBUG is false, which gates debug logging, extra
request logging, WebView contents debugging and the release-only source
sets in ui/debugview and ui/revenuecatui.

HasUnitTestBuilder is still available in AGP 9.3.1, so the library
convention re-enables them for every module.

Also update the documented minimum Kotlin: the published kotlin-stdlib is
now 2.3.21, whose metadata a compiler reads only one minor ahead, so
consumers need 2.2.0.
AGP 9 requires KGP 2.2.10 or higher; 2.3.21 was our choice, not a
requirement, and it costs more than it saves.

Kotlin 2.3.21 rejects languageVersion 1.8 outright, which forced the K1
to K2 frontend migration into this bump. It also publishes stdlib
metadata 2.3.0, which no released React Native can read: RN pins KGP
2.0.21 through 0.79 and 2.1.20 through 0.86, and only RN main pins 2.2.0.
Every React Native app would have needed an explicit KGP override.

At 2.2.21 the published metadata is 2.2.0, which RN 0.81+ reads
untouched, and languageVersion stays at 1.8 so the K2 migration moves out
of this release. Poko follows to 0.20.2, the release compiled against
2.2.21; it runs on Gradle 9.7 despite 0.21.0 being what raised Poko's own
minimum Gradle to 9.0.

The opt-in on SubscriberAttributesManager.setAttributes was only needed
by the K2 frontend and is removed again.

Consumer floor verified in both directions against the release AAR plus
the stdlib we publish: a consumer on Kotlin 2.1.21 compiles, and 2.0.21
and 1.9.24 both fail with "metadata is 2.2.0". README and the new v11
migration guide say 2.1.0.
:codegen is a java-gradle-plugin module, so it compiles against the
Kotlin that Gradle embeds, and a compiler reads metadata at most one
minor ahead. Gradle 9.7.0 embeds Kotlin 2.4.0, which our 2.2.21 compiler
cannot read; CI failed in prepare-tests with "metadata version is 2.4.0,
but the compiler version 2.2.0 can read versions up to 2.3.0".

Embedded Kotlin by Gradle release: 9.3.1 ships 2.2.21, 9.4.1 ships 2.3.0,
9.5.1 ships 2.3.20, 9.7.0 ships 2.4.0. So Kotlin 2.2.x needs Gradle
9.4.1 or lower.

AGP 9.3.1 requires Gradle 9.5.0 or higher, so it cannot be used with a
Gradle we can read. AGP 9.2.1 is the newest release that runs on Gradle
9.4.1, verified along with 9.1.1 and 9.0.1.

Published AAR contract is unchanged: minCompileSdk=1,
minAndroidGradlePluginVersion=1.0.0, and no api*.txt diff.
AlvaroBrey and others added 3 commits August 27, 2026 15:08
Drop the "must be able to read the SDK's Kotlin metadata" comments from the four sample
catalogs, and both CircleCI cache comments. They justify a version choice or an absent step,
which belongs in a commit message rather than in the tree. The metadata ones were also stale:
they still claimed the SDK publishes 2.3.x.

`baselineprofile` 1.5.0-beta01 to 1.5.0-rc02. 1.4.1 still builds on AGP 9.2.1, but warns
"tested with versions below Android Gradle Plugin version 9.0.0-alpha01" on all seven projects
that apply the plugin; 1.5.0 raises that ceiling. rc02 rather than beta01 since it is published.

`enableUnitTest` uses a hard cast. `LibraryVariantBuilder` already extends `HasUnitTestBuilder`
in 9.2.1, so `as?` could only ever hide a future AGP dropping it, which is the exact regression
the line exists to prevent.

Remove `composeOptions.kotlinCompilerExtensionVersion` from the two samples that gained the
Compose plugin; it is ignored once that plugin is applied.

Reword the `android.newDsl` comment. It is `kotlin-android` that needs the old DSL, since it
casts the android extension to `BaseExtension`, not the other way round.
…e comments

The three `kotlin { compilerOptions { jvmTarget } }` blocks that replaced `kotlinOptions{}`
in `checkpointtester`, `rcttester` and `e2etests` were appended after `dependencies {}`,
leaving them 25 lines away from the `compileOptions` they have to agree with. Every other
build script in the repo puts Kotlin compiler config between `android {}` and
`dependencies {}`. Pure move: the compile tasks stay up to date across it.

Drop `# Can't update until we use more recent kotlin. 1.6.0 uses Kotlin 1.9.0` above
`kotlinxSerializationJSON`. Kotlin is 2.2.21 now, so the stated blocker is gone. Not bumping
that dependency here.

Trim the duplicate sentence from the v11 guide intro; the 2.1.0 minimum was stated three
times in 24 lines.
@AlvaroBrey
AlvaroBrey requested a review from a team August 27, 2026 13:52
@AlvaroBrey

Copy link
Copy Markdown
Member Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 563bcdd. Configure here.

@AlvaroBrey
AlvaroBrey marked this pull request as ready for review August 27, 2026 14:09
@AlvaroBrey
AlvaroBrey requested a review from a team as a code owner August 27, 2026 14:09
@AlvaroBrey
AlvaroBrey removed the request for review from a team August 27, 2026 14:09

@tonidero tonidero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you!!

Comment thread .circleci/config.yml
Comment thread gradle.properties
Comment thread gradle/libs.versions.toml
AlvaroBrey and others added 2 commits August 27, 2026 16:59
The lane requires prepare-tests, whose only job is warming the shared
Gradle cache, and save_cache is first-write-wins per key. So this save
could never claim the key: it uploaded nothing and logged that the cache
already existed.

Leaves all three sample lanes restore-only and symmetric, rather than one
of them writing because it happens to build in the root project.
@AlvaroBrey
AlvaroBrey merged commit fc50f9c into 11.0-dev Aug 27, 2026
37 checks passed
@AlvaroBrey
AlvaroBrey deleted the build/agp9-bump branch August 27, 2026 15:48
AlvaroBrey added a commit that referenced this pull request Aug 28, 2026
> [!NOTE]
> This targets branch `11.0-dev` which will become the epic branch for
the upcoming major release. Once this first PR is merged, I'll open a
continuous one from that one to `main` until we are ready to release it
altogether.

- Moves the SDK's build toolchain to AGP 9:

| Dependency | From | To |
|---|---|---|
| AGP | 8.13.2 | **9.2.1** |
| Gradle | 8.14.5 | **9.4.1** |
| Kotlin | 2.0.21 | **2.2.21** |
| Poko | 0.17.2 | **0.20.2** |
| Paparazzi | 2.0.0-alpha02 | **2.0.0-alpha05** |

- No public API change and no `api*.txt` diff.
- **Why this becomes a major**: this transitively raises the miminum
kotlin version for consumers to 2.1.0. up from 1.8.0.
- Kotlin lands on **2.2.21, the minimum required by AGP 9**,
- AGP is capped at **9.2.1** rather than latest: AGP 9.3.x requires
Gradle 9.5+, and Gradle 9.5+ embeds a Kotlin that a 2.2 compiler cannot
read, bumping consumers to an even higher minimum Kotlin version.
- Nothing downstream needs AGP 9 or Gradle 9, just the Kotlin bump. The
published AAR metadata is unchanged (`minCompileSdk=1`,
`minAndroidGradlePluginVersion=1.0.0`).

Companion docs PR: RevenueCat/docs#1942
Companion PHC PR:
RevenueCat/purchases-hybrid-common#1844

### Checklist
- [ ] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-ios` and
hybrids

<details><summary>Agent description</summary>

### Motivation

AGP 8 is increasingly behind, and AGP 9 removes a number of DSL surfaces
we still used. The
prerequisite work already landed separately in #3931, #3961 and #3963.

AGP 9 has a runtime dependency on Kotlin Gradle Plugin **2.2.10 or
higher**, so a Kotlin bump is not
optional here. Everything above that floor is a choice, and this PR
deliberately takes the minimum.

### Why not Kotlin 2.3.x

An earlier revision of this branch used Kotlin 2.3.21. Measuring the
cost changed the decision:

- **It breaks every React Native consumer.** We publish `kotlin-stdlib`
at `compile` scope and Gradle
resolves consumers onto it. A compiler reads metadata at most one minor
ahead. Kotlin 2.3.x
publishes metadata 2.3.0, and React Native's own Gradle plugin pins the
compiler: 2.0.21 through
RN 0.79, 2.1.20 through RN 0.86, with only RN `main` on 2.2.0.
Reproduced on a real RN 0.86 app:
`metadata version is 2.3.0 / compiler version 2.1.0 can read versions up
to 2.2.0`. Every RN app
would have needed an explicit `kotlin-gradle-plugin` version in
`android/build.gradle`.
- **It forces the K2 migration.** Kotlin 2.3.21 rejects `languageVersion
1.8` outright. 2.2.x still
accepts it, so the frontend migration stays out of a version bump where
it does not belong.
- **It does not avoid a second bump anyway.** Kotlin 2.4 removes
`languageVersion 1.8` regardless.

At Kotlin 2.2.21 the published metadata is 2.2.0, which RN 0.81+ reads
untouched.

### Why AGP 9.2.1 and Gradle 9.4.1

This is the non-obvious constraint, and CI found it rather than local
builds.

`:codegen` is a `java-gradle-plugin` module, so it compiles against the
Kotlin that **Gradle itself
embeds**. Embedded Kotlin by Gradle release:

| Gradle | Embedded Kotlin | Readable by a 2.2 compiler (max 2.3.0) |
|---|---|---|
| 9.3.1 | 2.2.21 | yes |
| 9.4.1 | 2.3.0 | yes, exactly at the limit |
| 9.5.1 | 2.3.20 | no |
| 9.7.0 | 2.4.0 | no |

Gradle 9.7.0 therefore failed `prepare-tests` with
`metadata version is 2.4.0, but the compiler version 2.2.0 can read
versions up to 2.3.0`.

AGP 9.3.x requires Gradle 9.5.0 or higher (`Minimum supported Gradle
version is 9.5.0`), which is
exactly the range we cannot read. **AGP 9.2.1 is the newest release that
runs on Gradle 9.4.1**;
9.1.1 and 9.0.1 were also confirmed to work, 9.3.1 was confirmed not to.

So the AGP ceiling here is a consequence of the Kotlin floor, not an
independent preference. When the
deferred Kotlin 2.3 bump happens, AGP and Gradle can move up with it.

### Also in this PR

- Standalone sample apps (`CustomEntitlementComputationSample`,
`MagicWeather`, `MagicWeatherCompose`)
move to Kotlin 2.2.21 and gain the `compose-compiler` plugin alias. They
consume our published
  artifacts, so they need a compiler that can read our metadata.
- Unit tests are re-enabled on the release variant via
`HasUnitTestBuilder.enableUnitTest`, because
AGP 9 only creates unit tests for the `testBuildType` variant and the
published variant is release.
- The `androidDependencies` cache-warm CI step is dropped; AGP 9 removed
that task.
- `migrations/v11-MIGRATION.md` documents the new Kotlin floor,
mirroring the v9 guide.
- README `Requirements` now says Kotlin 2.1.0+.

### Deferred on purpose

- **Kotlin 2.3 and the K1 to K2 migration**, until React Native ships
the KGP 2.2 pin already on its
`main` branch. Doing it sooner buys nothing and costs every RN app a
migration step.
- **AGP 9 built-in Kotlin** (previously #3965). Not breaking and not
required; the
`android.builtInKotlin=false` / `android.newDsl=false` opt-out only has
to go at AGP 10.

### Testing

- `assembleDebug` across all modules; release AARs for `:purchases` and
`:ui:revenuecatui`.
- `:purchases` unit tests, `:ui:debugview:verifyPaparazziDefaultsDebug`,
`detektAll`.
- `scripts/api-check.sh` with **zero** `api*.txt` diff.
- Published contract checked directly: POM carries
`kotlin-stdlib:2.2.21` at `compile` scope, the
release AAR still reports `minCompileSdk=1` and
`minAndroidGradlePluginVersion=1.0.0`, and our own
  classes carry metadata `mv = {1, 8, 0}`.
- Consumer floor measured against the real release AAR plus the stdlib
we publish: Kotlin 2.1.21
compiles; 2.0.21 and 1.9.24 fail. That is what the 2.1.0 claim in the
README rests on.

### Downstream

The Kotlin floor is what propagates, not AGP. Nothing downstream needs
AGP 9 or Gradle 9.

The only direct consumer is purchases-hybrid-common, which re-exposes us
at compile scope via
`api(libs.purchases)`. RevenueCat/purchases-hybrid-common#1844 raises
its Kotlin accordingly, and the
hybrid SDKs pick the floor up from there rather than from this repo.

Documentation follows separately:

- RevenueCat/docs#1942 adds the Kotlin minimum to the Android, Flutter
and React Native installation
pages, and adds the 9.x-to-10.x and 10.x-to-11.x migration guides the
site was missing.
- RevenueCat/purchases-flutter#1876 states the Kotlin minimum in that
repo's README. Flutter applies
the version from the app's `android/settings.gradle`, so the requirement
lands on the consuming app
  and the Flutter version itself does not move.
- RevenueCat/react-native-purchases#1925 raises the React Native floor
to 0.80.0, the first release
  whose Gradle plugin pins a Kotlin that can read our metadata.

</details>





<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Major toolchain and minimum Kotlin version changes affect all
consumers and CI, though public API and published AAR AGP/minSdk
metadata are intentionally unchanged.
> 
> **Overview**
> This PR moves the SDK build to **AGP 9.2.1**, **Gradle 9.4.1**, and
**Kotlin 2.2.21**, and bumps related tooling (Poko, Paparazzi, baseline
profile plugin). **`gradle.properties`** opts out of AGP 9’s built-in
Kotlin and new DSL for now.
> 
> The **consumer-facing change** is a higher Kotlin floor: README and
**`migrations/v11-MIGRATION.md`** document **Kotlin 2.1.0+** (up from
1.8.0), driven by published stdlib metadata—not a public API change.
> 
> **Build-logic** re-enables unit tests on library variants under AGP 9
via **`enableUnitTest`**, and keeps **`aarMetadata.minCompileSdk = 1`**
so published AAR requirements don’t tighten.
> 
> Compose sample apps switch to the **Kotlin Compose compiler plugin**
and drop **`composeOptions.kotlinCompilerExtensionVersion`**. Several
modules replace deprecated **`kotlinOptions`** with
**`kotlin.compilerOptions.jvmTarget`**.
> 
> **CircleCI** drops the removed **`androidDependencies`** warmup and
uses Gradle cache restore on standalone sample builds instead.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1b6b4df. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
AlvaroBrey added a commit that referenced this pull request Aug 28, 2026
- Removes the `android.builtInKotlin=false` and `android.newDsl=false`
opt-out that #3964 added, from the root build and from both standalone
sample builds, and takes `kotlin-android` out of 11 build scripts and
the 3 convention plugins.
- `ui/debugview/api.txt` loses
`InternalDebugRevenueCatScreenViewModelFactory`. Metalava now analyses
the release variant, and that class lives in `src/debug/kotlin`. The
release AAR contains zero occurrences of it, so it never shipped and the
entry was wrong
- Fixes `:dokkaGenerate`, which is already broken on `11.0-dev`.
Built-in Kotlin registers Dokka source sets per Android variant instead
of per Kotlin source set, and registers them lazily, so both
`:purchases` and `:ui:revenuecatui` now select the documented variant
through `configureEach`.
- Bumps `androidx.navigation` 2.5.3 to 2.9.8. safeargs 2.5.3 detects AGP
through the old DSL and hard fails, so the navigation bump is a
prerequisite for the new DSL rather than an independent upgrade.

### Checklist
- [ ] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-ios` and
hybrids

<details><summary>Agent description</summary>

### Motivation

AGP 9 enables built-in Kotlin and rejects the
`org.jetbrains.kotlin.android` plugin outright:

```
The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0.
```

Opting out needs two flags together, `android.builtInKotlin=false` and
`android.newDsl=false`, because the kotlin-android plugin casts the
android extension to `BaseExtension`, which the new DSL drops. #3964
took that opt-out so the version bump stayed reviewable. AGP 10 removes
the opt-out, so the migration has to happen regardless; doing it
separately keeps the two diffs legible.

### Description

Four things blocked the migration, each hidden behind the previous.

**The old DSL extension types are gone.**
`com.android.build.gradle.BaseExtension` and
`com.android.build.gradle.LibraryExtension` do not exist once the new
DSL is active. The convention plugins now configure
`com.android.build.api.dsl.ApplicationExtension` and
`com.android.build.api.dsl.LibraryExtension`, and set `compileSdk`
directly rather than calling `compileSdkVersion()`.
`KotlinAndroidProjectExtension` stays as is: AGP's built-in Kotlin still
registers it.

**safeargs 2.5.3 cannot see AGP 9.** It fails with `safeargs plugin must
be used with android plugin`, because its detection goes through the old
DSL. The 2.5.3 jar references `com/android/build/gradle/BaseExtension`,
`AppExtension` and `gradle/api/BaseVariant`; the 2.9.8 jar references
only `com.android.build.api.variant.*`. `examples/purchase-tester` is
the only module applying the plugin, and it gets its own commit.

**Dokka source sets changed shape.** With the kotlin-android plugin,
Dokka saw the Kotlin source sets and the config addressed `main`,
`defaults` and `customEntitlementComputation`. Built-in Kotlin instead
registers one source set per Android variant, and registers them after
the `dokka { }` block runs, so `named("defaultsRelease")` throws even
though the name is valid later. Both modules now select through
`matching { }.configureEach`. `:purchases` keeps `reportUndocumented`,
`skipDeprecated`, the Android external documentation link, both source
links and the `paywalls.components` suppression, all moved onto the
selected variant. The explicit `customEntitlementComputation`
suppression is load-bearing, not redundant: Dokka defaults `suppress` to
`!isPublishable` and `customEntitlementComputationRelease` is published,
so it would otherwise be documented.

**`kotlin-test` stops carrying JUnit.** `:feature:galaxy` names
`kotlin-test-junit` instead. Verified rather than inherited: with plain
`kotlin-test`, `org.junit.Test` is unresolved across all 25 test
methods.

**Not visible in the diff:** `./gradlew :dokkaGenerate` fails on the
base commit with `Dokka cannot generate documentation for Android
projects with multiple enabled variants that have common source roots`.
The `docs-deploy` job that runs it is gated on release tags, so #3964
never exercised it. This PR is what makes it pass again, which is a
bigger deal than the diff makes it look.

**Worth noting separately:** AGP 9.2.1 declares
`kotlin-gradle-plugin:2.2.10`, so built-in Kotlin takes its compiler
from buildscript classpath resolution rather than from our catalog.
Confirmed the published contract from #3964 is intact rather than
assumed:

| check | result |
| --- | --- |
| resolved KGP | `2.2.21`, not AGP's bundled `2.2.10` |
| `:purchases` POM | `kotlin-stdlib:2.2.21` at `compile` scope |
| release AAR metadata | `mv=[1,8,0]`, so `languageVersion` 1.8 survives
|

### Regression gates

- `./gradlew :dokkaGenerate` for the Dokka rework. Generates 1367 pages
with `com.revenuecat.purchases.paywalls.components` still suppressed.
- `:feature:galaxy:testDefaultsDebugUnitTest` for the
`kotlin-test-junit` swap. Needs `--rerun-tasks`: a cached run passes
even with the dependency reverted, which is how this could slip through.
- `:examples:purchase-tester:assembleDebug` for the navigation bump.

**Rejected:**

- Wiring the Dokka variant selector to `ANDROID_VARIANT_TO_PUBLISH`
instead of hardcoding `"defaultsRelease"` twice. `fastlane/Fastfile:295`
rewrites that property per publish, so a `customEntitlementComputation`
publish would point Dokka at exactly the variant `:purchases`
suppresses.
- Hoisting the shared Dokka config into `ConfigureDokka.kt`. Dokka's
types are not on build-logic's compile classpath, so it needs the
reflection pattern from `ConfigureMetalava.kt`, and sharing
`reportUndocumented.set(true)` would newly turn it on for the four
public-library modules that have no `dokka { }` block.
- Moving the five hand-rolled app modules onto
`revenuecat-android-application`, which would collapse 5 of the 6
per-module edits. Each keeps 2 to 3 overrides, and both `testpurchases*`
modules deliberately pin old `compileSdk`/`targetSdk` to test
compatibility. Worth a separate PR, not this one.

**Limitations:**

- `examples/MagicWeather` and `test-apps/sdksizetesting` still apply
`kotlin-android`. They are separate builds pinned to AGP 8.13.2, so they
keep the plugin correctly and are untouched.
- `purchases/build.gradle.kts:120` has a dead `androidComponents {
onVariants }` block guarding a `"free"` product flavor that does not
exist in the repo. Pre-existing and unrelated, left alone.

</details>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Wide Gradle/AGP convention changes affect every Android module’s
compile and doc generation; navigation and Dokka behavior are
prerequisites for CI release docs, with limited runtime SDK impact.
> 
> **Overview**
> Completes the AGP 9 migration by removing the
**`android.builtInKotlin=false`** / **`android.newDsl=false`** opt-outs
and dropping **`org.jetbrains.kotlin.android`** across convention
plugins, app modules, samples, and the version catalog.
> 
> Build-logic now configures **`ApplicationExtension`** /
**`LibraryExtension`** from the new DSL (e.g. **`compileSdk`** instead
of **`BaseExtension`**). **`:purchases`** and **`:ui:revenuecatui`**
Dokka blocks use **`matching { }.configureEach`** so documentation
targets **`defaultsRelease`** and suppresses
**`customEntitlementComputation`** variants after AGP registers
per-variant source sets lazily.
> 
> **`androidx.navigation`** is bumped **2.5.3 → 2.9.8** so Safe Args
works with the new DSL. **`:feature:galaxy`** switches unit tests to
**`kotlin-test-junit`**. **`ui/debugview/api.txt`** drops
**`InternalDebugRevenueCatScreenViewModelFactory`** (debug-only; not in
the release API Metalava analyzes).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e45c589. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
AlvaroBrey added a commit that referenced this pull request Aug 31, 2026
> [!NOTE]
> This targets branch `11.0-dev` which will become the epic branch for
the upcoming major release. Once this first PR is merged, I'll open a
continuous one from that one to `main` until we are ready to release it
altogether.

- Moves the SDK's build toolchain to AGP 9:

| Dependency | From | To |
|---|---|---|
| AGP | 8.13.2 | **9.2.1** |
| Gradle | 8.14.5 | **9.4.1** |
| Kotlin | 2.0.21 | **2.2.21** |
| Poko | 0.17.2 | **0.20.2** |
| Paparazzi | 2.0.0-alpha02 | **2.0.0-alpha05** |

- No public API change and no `api*.txt` diff.
- **Why this becomes a major**: this transitively raises the miminum
kotlin version for consumers to 2.1.0. up from 1.8.0.
- Kotlin lands on **2.2.21, the minimum required by AGP 9**,
- AGP is capped at **9.2.1** rather than latest: AGP 9.3.x requires
Gradle 9.5+, and Gradle 9.5+ embeds a Kotlin that a 2.2 compiler cannot
read, bumping consumers to an even higher minimum Kotlin version.
- Nothing downstream needs AGP 9 or Gradle 9, just the Kotlin bump. The
published AAR metadata is unchanged (`minCompileSdk=1`,
`minAndroidGradlePluginVersion=1.0.0`).

Companion docs PR: RevenueCat/docs#1942
Companion PHC PR:
RevenueCat/purchases-hybrid-common#1844

### Checklist
- [ ] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-ios` and
hybrids

<details><summary>Agent description</summary>

### Motivation

AGP 8 is increasingly behind, and AGP 9 removes a number of DSL surfaces
we still used. The
prerequisite work already landed separately in #3931, #3961 and #3963.

AGP 9 has a runtime dependency on Kotlin Gradle Plugin **2.2.10 or
higher**, so a Kotlin bump is not
optional here. Everything above that floor is a choice, and this PR
deliberately takes the minimum.

### Why not Kotlin 2.3.x

An earlier revision of this branch used Kotlin 2.3.21. Measuring the
cost changed the decision:

- **It breaks every React Native consumer.** We publish `kotlin-stdlib`
at `compile` scope and Gradle
resolves consumers onto it. A compiler reads metadata at most one minor
ahead. Kotlin 2.3.x
publishes metadata 2.3.0, and React Native's own Gradle plugin pins the
compiler: 2.0.21 through
RN 0.79, 2.1.20 through RN 0.86, with only RN `main` on 2.2.0.
Reproduced on a real RN 0.86 app:
`metadata version is 2.3.0 / compiler version 2.1.0 can read versions up
to 2.2.0`. Every RN app
would have needed an explicit `kotlin-gradle-plugin` version in
`android/build.gradle`.
- **It forces the K2 migration.** Kotlin 2.3.21 rejects `languageVersion
1.8` outright. 2.2.x still
accepts it, so the frontend migration stays out of a version bump where
it does not belong.
- **It does not avoid a second bump anyway.** Kotlin 2.4 removes
`languageVersion 1.8` regardless.

At Kotlin 2.2.21 the published metadata is 2.2.0, which RN 0.81+ reads
untouched.

### Why AGP 9.2.1 and Gradle 9.4.1

This is the non-obvious constraint, and CI found it rather than local
builds.

`:codegen` is a `java-gradle-plugin` module, so it compiles against the
Kotlin that **Gradle itself
embeds**. Embedded Kotlin by Gradle release:

| Gradle | Embedded Kotlin | Readable by a 2.2 compiler (max 2.3.0) |
|---|---|---|
| 9.3.1 | 2.2.21 | yes |
| 9.4.1 | 2.3.0 | yes, exactly at the limit |
| 9.5.1 | 2.3.20 | no |
| 9.7.0 | 2.4.0 | no |

Gradle 9.7.0 therefore failed `prepare-tests` with
`metadata version is 2.4.0, but the compiler version 2.2.0 can read
versions up to 2.3.0`.

AGP 9.3.x requires Gradle 9.5.0 or higher (`Minimum supported Gradle
version is 9.5.0`), which is
exactly the range we cannot read. **AGP 9.2.1 is the newest release that
runs on Gradle 9.4.1**;
9.1.1 and 9.0.1 were also confirmed to work, 9.3.1 was confirmed not to.

So the AGP ceiling here is a consequence of the Kotlin floor, not an
independent preference. When the
deferred Kotlin 2.3 bump happens, AGP and Gradle can move up with it.

### Also in this PR

- Standalone sample apps (`CustomEntitlementComputationSample`,
`MagicWeather`, `MagicWeatherCompose`)
move to Kotlin 2.2.21 and gain the `compose-compiler` plugin alias. They
consume our published
  artifacts, so they need a compiler that can read our metadata.
- Unit tests are re-enabled on the release variant via
`HasUnitTestBuilder.enableUnitTest`, because
AGP 9 only creates unit tests for the `testBuildType` variant and the
published variant is release.
- The `androidDependencies` cache-warm CI step is dropped; AGP 9 removed
that task.
- `migrations/v11-MIGRATION.md` documents the new Kotlin floor,
mirroring the v9 guide.
- README `Requirements` now says Kotlin 2.1.0+.

### Deferred on purpose

- **Kotlin 2.3 and the K1 to K2 migration**, until React Native ships
the KGP 2.2 pin already on its
`main` branch. Doing it sooner buys nothing and costs every RN app a
migration step.
- **AGP 9 built-in Kotlin** (previously #3965). Not breaking and not
required; the
`android.builtInKotlin=false` / `android.newDsl=false` opt-out only has
to go at AGP 10.

### Testing

- `assembleDebug` across all modules; release AARs for `:purchases` and
`:ui:revenuecatui`.
- `:purchases` unit tests, `:ui:debugview:verifyPaparazziDefaultsDebug`,
`detektAll`.
- `scripts/api-check.sh` with **zero** `api*.txt` diff.
- Published contract checked directly: POM carries
`kotlin-stdlib:2.2.21` at `compile` scope, the
release AAR still reports `minCompileSdk=1` and
`minAndroidGradlePluginVersion=1.0.0`, and our own
  classes carry metadata `mv = {1, 8, 0}`.
- Consumer floor measured against the real release AAR plus the stdlib
we publish: Kotlin 2.1.21
compiles; 2.0.21 and 1.9.24 fail. That is what the 2.1.0 claim in the
README rests on.

### Downstream

The Kotlin floor is what propagates, not AGP. Nothing downstream needs
AGP 9 or Gradle 9.

The only direct consumer is purchases-hybrid-common, which re-exposes us
at compile scope via
`api(libs.purchases)`. RevenueCat/purchases-hybrid-common#1844 raises
its Kotlin accordingly, and the
hybrid SDKs pick the floor up from there rather than from this repo.

Documentation follows separately:

- RevenueCat/docs#1942 adds the Kotlin minimum to the Android, Flutter
and React Native installation
pages, and adds the 9.x-to-10.x and 10.x-to-11.x migration guides the
site was missing.
- RevenueCat/purchases-flutter#1876 states the Kotlin minimum in that
repo's README. Flutter applies
the version from the app's `android/settings.gradle`, so the requirement
lands on the consuming app
  and the Flutter version itself does not move.
- RevenueCat/react-native-purchases#1925 raises the React Native floor
to 0.80.0, the first release
  whose Gradle plugin pins a Kotlin that can read our metadata.

</details>





<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Major toolchain and minimum Kotlin version changes affect all
consumers and CI, though public API and published AAR AGP/minSdk
metadata are intentionally unchanged.
> 
> **Overview**
> This PR moves the SDK build to **AGP 9.2.1**, **Gradle 9.4.1**, and
**Kotlin 2.2.21**, and bumps related tooling (Poko, Paparazzi, baseline
profile plugin). **`gradle.properties`** opts out of AGP 9’s built-in
Kotlin and new DSL for now.
> 
> The **consumer-facing change** is a higher Kotlin floor: README and
**`migrations/v11-MIGRATION.md`** document **Kotlin 2.1.0+** (up from
1.8.0), driven by published stdlib metadata—not a public API change.
> 
> **Build-logic** re-enables unit tests on library variants under AGP 9
via **`enableUnitTest`**, and keeps **`aarMetadata.minCompileSdk = 1`**
so published AAR requirements don’t tighten.
> 
> Compose sample apps switch to the **Kotlin Compose compiler plugin**
and drop **`composeOptions.kotlinCompilerExtensionVersion`**. Several
modules replace deprecated **`kotlinOptions`** with
**`kotlin.compilerOptions.jvmTarget`**.
> 
> **CircleCI** drops the removed **`androidDependencies`** warmup and
uses Gradle cache restore on standalone sample builds instead.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1b6b4df. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
AlvaroBrey added a commit that referenced this pull request Aug 31, 2026
- Removes the `android.builtInKotlin=false` and `android.newDsl=false`
opt-out that #3964 added, from the root build and from both standalone
sample builds, and takes `kotlin-android` out of 11 build scripts and
the 3 convention plugins.
- `ui/debugview/api.txt` loses
`InternalDebugRevenueCatScreenViewModelFactory`. Metalava now analyses
the release variant, and that class lives in `src/debug/kotlin`. The
release AAR contains zero occurrences of it, so it never shipped and the
entry was wrong
- Fixes `:dokkaGenerate`, which is already broken on `11.0-dev`.
Built-in Kotlin registers Dokka source sets per Android variant instead
of per Kotlin source set, and registers them lazily, so both
`:purchases` and `:ui:revenuecatui` now select the documented variant
through `configureEach`.
- Bumps `androidx.navigation` 2.5.3 to 2.9.8. safeargs 2.5.3 detects AGP
through the old DSL and hard fails, so the navigation bump is a
prerequisite for the new DSL rather than an independent upgrade.

### Checklist
- [ ] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-ios` and
hybrids

<details><summary>Agent description</summary>

### Motivation

AGP 9 enables built-in Kotlin and rejects the
`org.jetbrains.kotlin.android` plugin outright:

```
The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0.
```

Opting out needs two flags together, `android.builtInKotlin=false` and
`android.newDsl=false`, because the kotlin-android plugin casts the
android extension to `BaseExtension`, which the new DSL drops. #3964
took that opt-out so the version bump stayed reviewable. AGP 10 removes
the opt-out, so the migration has to happen regardless; doing it
separately keeps the two diffs legible.

### Description

Four things blocked the migration, each hidden behind the previous.

**The old DSL extension types are gone.**
`com.android.build.gradle.BaseExtension` and
`com.android.build.gradle.LibraryExtension` do not exist once the new
DSL is active. The convention plugins now configure
`com.android.build.api.dsl.ApplicationExtension` and
`com.android.build.api.dsl.LibraryExtension`, and set `compileSdk`
directly rather than calling `compileSdkVersion()`.
`KotlinAndroidProjectExtension` stays as is: AGP's built-in Kotlin still
registers it.

**safeargs 2.5.3 cannot see AGP 9.** It fails with `safeargs plugin must
be used with android plugin`, because its detection goes through the old
DSL. The 2.5.3 jar references `com/android/build/gradle/BaseExtension`,
`AppExtension` and `gradle/api/BaseVariant`; the 2.9.8 jar references
only `com.android.build.api.variant.*`. `examples/purchase-tester` is
the only module applying the plugin, and it gets its own commit.

**Dokka source sets changed shape.** With the kotlin-android plugin,
Dokka saw the Kotlin source sets and the config addressed `main`,
`defaults` and `customEntitlementComputation`. Built-in Kotlin instead
registers one source set per Android variant, and registers them after
the `dokka { }` block runs, so `named("defaultsRelease")` throws even
though the name is valid later. Both modules now select through
`matching { }.configureEach`. `:purchases` keeps `reportUndocumented`,
`skipDeprecated`, the Android external documentation link, both source
links and the `paywalls.components` suppression, all moved onto the
selected variant. The explicit `customEntitlementComputation`
suppression is load-bearing, not redundant: Dokka defaults `suppress` to
`!isPublishable` and `customEntitlementComputationRelease` is published,
so it would otherwise be documented.

**`kotlin-test` stops carrying JUnit.** `:feature:galaxy` names
`kotlin-test-junit` instead. Verified rather than inherited: with plain
`kotlin-test`, `org.junit.Test` is unresolved across all 25 test
methods.

**Not visible in the diff:** `./gradlew :dokkaGenerate` fails on the
base commit with `Dokka cannot generate documentation for Android
projects with multiple enabled variants that have common source roots`.
The `docs-deploy` job that runs it is gated on release tags, so #3964
never exercised it. This PR is what makes it pass again, which is a
bigger deal than the diff makes it look.

**Worth noting separately:** AGP 9.2.1 declares
`kotlin-gradle-plugin:2.2.10`, so built-in Kotlin takes its compiler
from buildscript classpath resolution rather than from our catalog.
Confirmed the published contract from #3964 is intact rather than
assumed:

| check | result |
| --- | --- |
| resolved KGP | `2.2.21`, not AGP's bundled `2.2.10` |
| `:purchases` POM | `kotlin-stdlib:2.2.21` at `compile` scope |
| release AAR metadata | `mv=[1,8,0]`, so `languageVersion` 1.8 survives
|

### Regression gates

- `./gradlew :dokkaGenerate` for the Dokka rework. Generates 1367 pages
with `com.revenuecat.purchases.paywalls.components` still suppressed.
- `:feature:galaxy:testDefaultsDebugUnitTest` for the
`kotlin-test-junit` swap. Needs `--rerun-tasks`: a cached run passes
even with the dependency reverted, which is how this could slip through.
- `:examples:purchase-tester:assembleDebug` for the navigation bump.

**Rejected:**

- Wiring the Dokka variant selector to `ANDROID_VARIANT_TO_PUBLISH`
instead of hardcoding `"defaultsRelease"` twice. `fastlane/Fastfile:295`
rewrites that property per publish, so a `customEntitlementComputation`
publish would point Dokka at exactly the variant `:purchases`
suppresses.
- Hoisting the shared Dokka config into `ConfigureDokka.kt`. Dokka's
types are not on build-logic's compile classpath, so it needs the
reflection pattern from `ConfigureMetalava.kt`, and sharing
`reportUndocumented.set(true)` would newly turn it on for the four
public-library modules that have no `dokka { }` block.
- Moving the five hand-rolled app modules onto
`revenuecat-android-application`, which would collapse 5 of the 6
per-module edits. Each keeps 2 to 3 overrides, and both `testpurchases*`
modules deliberately pin old `compileSdk`/`targetSdk` to test
compatibility. Worth a separate PR, not this one.

**Limitations:**

- `examples/MagicWeather` and `test-apps/sdksizetesting` still apply
`kotlin-android`. They are separate builds pinned to AGP 8.13.2, so they
keep the plugin correctly and are untouched.
- `purchases/build.gradle.kts:120` has a dead `androidComponents {
onVariants }` block guarding a `"free"` product flavor that does not
exist in the repo. Pre-existing and unrelated, left alone.

</details>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Wide Gradle/AGP convention changes affect every Android module’s
compile and doc generation; navigation and Dokka behavior are
prerequisites for CI release docs, with limited runtime SDK impact.
> 
> **Overview**
> Completes the AGP 9 migration by removing the
**`android.builtInKotlin=false`** / **`android.newDsl=false`** opt-outs
and dropping **`org.jetbrains.kotlin.android`** across convention
plugins, app modules, samples, and the version catalog.
> 
> Build-logic now configures **`ApplicationExtension`** /
**`LibraryExtension`** from the new DSL (e.g. **`compileSdk`** instead
of **`BaseExtension`**). **`:purchases`** and **`:ui:revenuecatui`**
Dokka blocks use **`matching { }.configureEach`** so documentation
targets **`defaultsRelease`** and suppresses
**`customEntitlementComputation`** variants after AGP registers
per-variant source sets lazily.
> 
> **`androidx.navigation`** is bumped **2.5.3 → 2.9.8** so Safe Args
works with the new DSL. **`:feature:galaxy`** switches unit tests to
**`kotlin-test-junit`**. **`ui/debugview/api.txt`** drops
**`InternalDebugRevenueCatScreenViewModelFactory`** (debug-only; not in
the release API Metalava analyzes).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e45c589. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
AlvaroBrey added a commit that referenced this pull request Sep 7, 2026
> [!NOTE]
> This targets branch `11.0-dev` which will become the epic branch for
the upcoming major release. Once this first PR is merged, I'll open a
continuous one from that one to `main` until we are ready to release it
altogether.

- Moves the SDK's build toolchain to AGP 9:

| Dependency | From | To |
|---|---|---|
| AGP | 8.13.2 | **9.2.1** |
| Gradle | 8.14.5 | **9.4.1** |
| Kotlin | 2.0.21 | **2.2.21** |
| Poko | 0.17.2 | **0.20.2** |
| Paparazzi | 2.0.0-alpha02 | **2.0.0-alpha05** |

- No public API change and no `api*.txt` diff.
- **Why this becomes a major**: this transitively raises the miminum
kotlin version for consumers to 2.1.0. up from 1.8.0.
- Kotlin lands on **2.2.21, the minimum required by AGP 9**,
- AGP is capped at **9.2.1** rather than latest: AGP 9.3.x requires
Gradle 9.5+, and Gradle 9.5+ embeds a Kotlin that a 2.2 compiler cannot
read, bumping consumers to an even higher minimum Kotlin version.
- Nothing downstream needs AGP 9 or Gradle 9, just the Kotlin bump. The
published AAR metadata is unchanged (`minCompileSdk=1`,
`minAndroidGradlePluginVersion=1.0.0`).

Companion docs PR: RevenueCat/docs#1942
Companion PHC PR:
RevenueCat/purchases-hybrid-common#1844

- [ ] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-ios` and
hybrids

<details><summary>Agent description</summary>

AGP 8 is increasingly behind, and AGP 9 removes a number of DSL surfaces
we still used. The
prerequisite work already landed separately in #3931, #3961 and #3963.

AGP 9 has a runtime dependency on Kotlin Gradle Plugin **2.2.10 or
higher**, so a Kotlin bump is not
optional here. Everything above that floor is a choice, and this PR
deliberately takes the minimum.

An earlier revision of this branch used Kotlin 2.3.21. Measuring the
cost changed the decision:

- **It breaks every React Native consumer.** We publish `kotlin-stdlib`
at `compile` scope and Gradle
resolves consumers onto it. A compiler reads metadata at most one minor
ahead. Kotlin 2.3.x
publishes metadata 2.3.0, and React Native's own Gradle plugin pins the
compiler: 2.0.21 through
RN 0.79, 2.1.20 through RN 0.86, with only RN `main` on 2.2.0.
Reproduced on a real RN 0.86 app:
`metadata version is 2.3.0 / compiler version 2.1.0 can read versions up
to 2.2.0`. Every RN app
would have needed an explicit `kotlin-gradle-plugin` version in
`android/build.gradle`.
- **It forces the K2 migration.** Kotlin 2.3.21 rejects `languageVersion
1.8` outright. 2.2.x still
accepts it, so the frontend migration stays out of a version bump where
it does not belong.
- **It does not avoid a second bump anyway.** Kotlin 2.4 removes
`languageVersion 1.8` regardless.

At Kotlin 2.2.21 the published metadata is 2.2.0, which RN 0.81+ reads
untouched.

This is the non-obvious constraint, and CI found it rather than local
builds.

`:codegen` is a `java-gradle-plugin` module, so it compiles against the
Kotlin that **Gradle itself
embeds**. Embedded Kotlin by Gradle release:

| Gradle | Embedded Kotlin | Readable by a 2.2 compiler (max 2.3.0) |
|---|---|---|
| 9.3.1 | 2.2.21 | yes |
| 9.4.1 | 2.3.0 | yes, exactly at the limit |
| 9.5.1 | 2.3.20 | no |
| 9.7.0 | 2.4.0 | no |

Gradle 9.7.0 therefore failed `prepare-tests` with
`metadata version is 2.4.0, but the compiler version 2.2.0 can read
versions up to 2.3.0`.

AGP 9.3.x requires Gradle 9.5.0 or higher (`Minimum supported Gradle
version is 9.5.0`), which is
exactly the range we cannot read. **AGP 9.2.1 is the newest release that
runs on Gradle 9.4.1**;
9.1.1 and 9.0.1 were also confirmed to work, 9.3.1 was confirmed not to.

So the AGP ceiling here is a consequence of the Kotlin floor, not an
independent preference. When the
deferred Kotlin 2.3 bump happens, AGP and Gradle can move up with it.

- Standalone sample apps (`CustomEntitlementComputationSample`,
`MagicWeather`, `MagicWeatherCompose`)
move to Kotlin 2.2.21 and gain the `compose-compiler` plugin alias. They
consume our published
  artifacts, so they need a compiler that can read our metadata.
- Unit tests are re-enabled on the release variant via
`HasUnitTestBuilder.enableUnitTest`, because
AGP 9 only creates unit tests for the `testBuildType` variant and the
published variant is release.
- The `androidDependencies` cache-warm CI step is dropped; AGP 9 removed
that task.
- `migrations/v11-MIGRATION.md` documents the new Kotlin floor,
mirroring the v9 guide.
- README `Requirements` now says Kotlin 2.1.0+.

- **Kotlin 2.3 and the K1 to K2 migration**, until React Native ships
the KGP 2.2 pin already on its
`main` branch. Doing it sooner buys nothing and costs every RN app a
migration step.
- **AGP 9 built-in Kotlin** (previously #3965). Not breaking and not
required; the
`android.builtInKotlin=false` / `android.newDsl=false` opt-out only has
to go at AGP 10.

- `assembleDebug` across all modules; release AARs for `:purchases` and
`:ui:revenuecatui`.
- `:purchases` unit tests, `:ui:debugview:verifyPaparazziDefaultsDebug`,
`detektAll`.
- `scripts/api-check.sh` with **zero** `api*.txt` diff.
- Published contract checked directly: POM carries
`kotlin-stdlib:2.2.21` at `compile` scope, the
release AAR still reports `minCompileSdk=1` and
`minAndroidGradlePluginVersion=1.0.0`, and our own
  classes carry metadata `mv = {1, 8, 0}`.
- Consumer floor measured against the real release AAR plus the stdlib
we publish: Kotlin 2.1.21
compiles; 2.0.21 and 1.9.24 fail. That is what the 2.1.0 claim in the
README rests on.

The Kotlin floor is what propagates, not AGP. Nothing downstream needs
AGP 9 or Gradle 9.

The only direct consumer is purchases-hybrid-common, which re-exposes us
at compile scope via
`api(libs.purchases)`. RevenueCat/purchases-hybrid-common#1844 raises
its Kotlin accordingly, and the
hybrid SDKs pick the floor up from there rather than from this repo.

Documentation follows separately:

- RevenueCat/docs#1942 adds the Kotlin minimum to the Android, Flutter
and React Native installation
pages, and adds the 9.x-to-10.x and 10.x-to-11.x migration guides the
site was missing.
- RevenueCat/purchases-flutter#1876 states the Kotlin minimum in that
repo's README. Flutter applies
the version from the app's `android/settings.gradle`, so the requirement
lands on the consuming app
  and the Flutter version itself does not move.
- RevenueCat/react-native-purchases#1925 raises the React Native floor
to 0.80.0, the first release
  whose Gradle plugin pins a Kotlin that can read our metadata.

</details>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Major toolchain and minimum Kotlin version changes affect all
consumers and CI, though public API and published AAR AGP/minSdk
metadata are intentionally unchanged.
>
> **Overview**
> This PR moves the SDK build to **AGP 9.2.1**, **Gradle 9.4.1**, and
**Kotlin 2.2.21**, and bumps related tooling (Poko, Paparazzi, baseline
profile plugin). **`gradle.properties`** opts out of AGP 9’s built-in
Kotlin and new DSL for now.
>
> The **consumer-facing change** is a higher Kotlin floor: README and
**`migrations/v11-MIGRATION.md`** document **Kotlin 2.1.0+** (up from
1.8.0), driven by published stdlib metadata—not a public API change.
>
> **Build-logic** re-enables unit tests on library variants under AGP 9
via **`enableUnitTest`**, and keeps **`aarMetadata.minCompileSdk = 1`**
so published AAR requirements don’t tighten.
>
> Compose sample apps switch to the **Kotlin Compose compiler plugin**
and drop **`composeOptions.kotlinCompilerExtensionVersion`**. Several
modules replace deprecated **`kotlinOptions`** with
**`kotlin.compilerOptions.jvmTarget`**.
>
> **CircleCI** drops the removed **`androidDependencies`** warmup and
uses Gradle cache restore on standalone sample builds instead.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1b6b4df. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
AlvaroBrey added a commit that referenced this pull request Sep 7, 2026
- Removes the `android.builtInKotlin=false` and `android.newDsl=false`
opt-out that #3964 added, from the root build and from both standalone
sample builds, and takes `kotlin-android` out of 11 build scripts and
the 3 convention plugins.
- `ui/debugview/api.txt` loses
`InternalDebugRevenueCatScreenViewModelFactory`. Metalava now analyses
the release variant, and that class lives in `src/debug/kotlin`. The
release AAR contains zero occurrences of it, so it never shipped and the
entry was wrong
- Fixes `:dokkaGenerate`, which is already broken on `11.0-dev`.
Built-in Kotlin registers Dokka source sets per Android variant instead
of per Kotlin source set, and registers them lazily, so both
`:purchases` and `:ui:revenuecatui` now select the documented variant
through `configureEach`.
- Bumps `androidx.navigation` 2.5.3 to 2.9.8. safeargs 2.5.3 detects AGP
through the old DSL and hard fails, so the navigation bump is a
prerequisite for the new DSL rather than an independent upgrade.

### Checklist
- [ ] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-ios` and
hybrids

<details><summary>Agent description</summary>

### Motivation

AGP 9 enables built-in Kotlin and rejects the
`org.jetbrains.kotlin.android` plugin outright:

```
The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0.
```

Opting out needs two flags together, `android.builtInKotlin=false` and
`android.newDsl=false`, because the kotlin-android plugin casts the
android extension to `BaseExtension`, which the new DSL drops. #3964
took that opt-out so the version bump stayed reviewable. AGP 10 removes
the opt-out, so the migration has to happen regardless; doing it
separately keeps the two diffs legible.

### Description

Four things blocked the migration, each hidden behind the previous.

**The old DSL extension types are gone.**
`com.android.build.gradle.BaseExtension` and
`com.android.build.gradle.LibraryExtension` do not exist once the new
DSL is active. The convention plugins now configure
`com.android.build.api.dsl.ApplicationExtension` and
`com.android.build.api.dsl.LibraryExtension`, and set `compileSdk`
directly rather than calling `compileSdkVersion()`.
`KotlinAndroidProjectExtension` stays as is: AGP's built-in Kotlin still
registers it.

**safeargs 2.5.3 cannot see AGP 9.** It fails with `safeargs plugin must
be used with android plugin`, because its detection goes through the old
DSL. The 2.5.3 jar references `com/android/build/gradle/BaseExtension`,
`AppExtension` and `gradle/api/BaseVariant`; the 2.9.8 jar references
only `com.android.build.api.variant.*`. `examples/purchase-tester` is
the only module applying the plugin, and it gets its own commit.

**Dokka source sets changed shape.** With the kotlin-android plugin,
Dokka saw the Kotlin source sets and the config addressed `main`,
`defaults` and `customEntitlementComputation`. Built-in Kotlin instead
registers one source set per Android variant, and registers them after
the `dokka { }` block runs, so `named("defaultsRelease")` throws even
though the name is valid later. Both modules now select through
`matching { }.configureEach`. `:purchases` keeps `reportUndocumented`,
`skipDeprecated`, the Android external documentation link, both source
links and the `paywalls.components` suppression, all moved onto the
selected variant. The explicit `customEntitlementComputation`
suppression is load-bearing, not redundant: Dokka defaults `suppress` to
`!isPublishable` and `customEntitlementComputationRelease` is published,
so it would otherwise be documented.

**`kotlin-test` stops carrying JUnit.** `:feature:galaxy` names
`kotlin-test-junit` instead. Verified rather than inherited: with plain
`kotlin-test`, `org.junit.Test` is unresolved across all 25 test
methods.

**Not visible in the diff:** `./gradlew :dokkaGenerate` fails on the
base commit with `Dokka cannot generate documentation for Android
projects with multiple enabled variants that have common source roots`.
The `docs-deploy` job that runs it is gated on release tags, so #3964
never exercised it. This PR is what makes it pass again, which is a
bigger deal than the diff makes it look.

**Worth noting separately:** AGP 9.2.1 declares
`kotlin-gradle-plugin:2.2.10`, so built-in Kotlin takes its compiler
from buildscript classpath resolution rather than from our catalog.
Confirmed the published contract from #3964 is intact rather than
assumed:

| check | result |
| --- | --- |
| resolved KGP | `2.2.21`, not AGP's bundled `2.2.10` |
| `:purchases` POM | `kotlin-stdlib:2.2.21` at `compile` scope |
| release AAR metadata | `mv=[1,8,0]`, so `languageVersion` 1.8 survives
|

### Regression gates

- `./gradlew :dokkaGenerate` for the Dokka rework. Generates 1367 pages
with `com.revenuecat.purchases.paywalls.components` still suppressed.
- `:feature:galaxy:testDefaultsDebugUnitTest` for the
`kotlin-test-junit` swap. Needs `--rerun-tasks`: a cached run passes
even with the dependency reverted, which is how this could slip through.
- `:examples:purchase-tester:assembleDebug` for the navigation bump.

**Rejected:**

- Wiring the Dokka variant selector to `ANDROID_VARIANT_TO_PUBLISH`
instead of hardcoding `"defaultsRelease"` twice. `fastlane/Fastfile:295`
rewrites that property per publish, so a `customEntitlementComputation`
publish would point Dokka at exactly the variant `:purchases`
suppresses.
- Hoisting the shared Dokka config into `ConfigureDokka.kt`. Dokka's
types are not on build-logic's compile classpath, so it needs the
reflection pattern from `ConfigureMetalava.kt`, and sharing
`reportUndocumented.set(true)` would newly turn it on for the four
public-library modules that have no `dokka { }` block.
- Moving the five hand-rolled app modules onto
`revenuecat-android-application`, which would collapse 5 of the 6
per-module edits. Each keeps 2 to 3 overrides, and both `testpurchases*`
modules deliberately pin old `compileSdk`/`targetSdk` to test
compatibility. Worth a separate PR, not this one.

**Limitations:**

- `examples/MagicWeather` and `test-apps/sdksizetesting` still apply
`kotlin-android`. They are separate builds pinned to AGP 8.13.2, so they
keep the plugin correctly and are untouched.
- `purchases/build.gradle.kts:120` has a dead `androidComponents {
onVariants }` block guarding a `"free"` product flavor that does not
exist in the repo. Pre-existing and unrelated, left alone.

</details>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Wide Gradle/AGP convention changes affect every Android module’s
compile and doc generation; navigation and Dokka behavior are
prerequisites for CI release docs, with limited runtime SDK impact.
> 
> **Overview**
> Completes the AGP 9 migration by removing the
**`android.builtInKotlin=false`** / **`android.newDsl=false`** opt-outs
and dropping **`org.jetbrains.kotlin.android`** across convention
plugins, app modules, samples, and the version catalog.
> 
> Build-logic now configures **`ApplicationExtension`** /
**`LibraryExtension`** from the new DSL (e.g. **`compileSdk`** instead
of **`BaseExtension`**). **`:purchases`** and **`:ui:revenuecatui`**
Dokka blocks use **`matching { }.configureEach`** so documentation
targets **`defaultsRelease`** and suppresses
**`customEntitlementComputation`** variants after AGP registers
per-variant source sets lazily.
> 
> **`androidx.navigation`** is bumped **2.5.3 → 2.9.8** so Safe Args
works with the new DSL. **`:feature:galaxy`** switches unit tests to
**`kotlin-test-junit`**. **`ui/debugview/api.txt`** drops
**`InternalDebugRevenueCatScreenViewModelFactory`** (debug-only; not in
the release API Metalava analyzes).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e45c589. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
AlvaroBrey added a commit that referenced this pull request Sep 7, 2026
> [!NOTE]
> This targets branch `11.0-dev` which will become the epic branch for
the upcoming major release. Once this first PR is merged, I'll open a
continuous one from that one to `main` until we are ready to release it
altogether.

- Moves the SDK's build toolchain to AGP 9:

| Dependency | From | To |
|---|---|---|
| AGP | 8.13.2 | **9.2.1** |
| Gradle | 8.14.5 | **9.4.1** |
| Kotlin | 2.0.21 | **2.2.21** |
| Poko | 0.17.2 | **0.20.2** |
| Paparazzi | 2.0.0-alpha02 | **2.0.0-alpha05** |

- No public API change and no `api*.txt` diff.
- **Why this becomes a major**: this transitively raises the miminum
kotlin version for consumers to 2.1.0. up from 1.8.0.
- Kotlin lands on **2.2.21, the minimum required by AGP 9**,
- AGP is capped at **9.2.1** rather than latest: AGP 9.3.x requires
Gradle 9.5+, and Gradle 9.5+ embeds a Kotlin that a 2.2 compiler cannot
read, bumping consumers to an even higher minimum Kotlin version.
- Nothing downstream needs AGP 9 or Gradle 9, just the Kotlin bump. The
published AAR metadata is unchanged (`minCompileSdk=1`,
`minAndroidGradlePluginVersion=1.0.0`).

Companion docs PR: RevenueCat/docs#1942
Companion PHC PR:
RevenueCat/purchases-hybrid-common#1844

- [ ] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-ios` and
hybrids

<details><summary>Agent description</summary>

AGP 8 is increasingly behind, and AGP 9 removes a number of DSL surfaces
we still used. The
prerequisite work already landed separately in #3931, #3961 and #3963.

AGP 9 has a runtime dependency on Kotlin Gradle Plugin **2.2.10 or
higher**, so a Kotlin bump is not
optional here. Everything above that floor is a choice, and this PR
deliberately takes the minimum.

An earlier revision of this branch used Kotlin 2.3.21. Measuring the
cost changed the decision:

- **It breaks every React Native consumer.** We publish `kotlin-stdlib`
at `compile` scope and Gradle
resolves consumers onto it. A compiler reads metadata at most one minor
ahead. Kotlin 2.3.x
publishes metadata 2.3.0, and React Native's own Gradle plugin pins the
compiler: 2.0.21 through
RN 0.79, 2.1.20 through RN 0.86, with only RN `main` on 2.2.0.
Reproduced on a real RN 0.86 app:
`metadata version is 2.3.0 / compiler version 2.1.0 can read versions up
to 2.2.0`. Every RN app
would have needed an explicit `kotlin-gradle-plugin` version in
`android/build.gradle`.
- **It forces the K2 migration.** Kotlin 2.3.21 rejects `languageVersion
1.8` outright. 2.2.x still
accepts it, so the frontend migration stays out of a version bump where
it does not belong.
- **It does not avoid a second bump anyway.** Kotlin 2.4 removes
`languageVersion 1.8` regardless.

At Kotlin 2.2.21 the published metadata is 2.2.0, which RN 0.81+ reads
untouched.

This is the non-obvious constraint, and CI found it rather than local
builds.

`:codegen` is a `java-gradle-plugin` module, so it compiles against the
Kotlin that **Gradle itself
embeds**. Embedded Kotlin by Gradle release:

| Gradle | Embedded Kotlin | Readable by a 2.2 compiler (max 2.3.0) |
|---|---|---|
| 9.3.1 | 2.2.21 | yes |
| 9.4.1 | 2.3.0 | yes, exactly at the limit |
| 9.5.1 | 2.3.20 | no |
| 9.7.0 | 2.4.0 | no |

Gradle 9.7.0 therefore failed `prepare-tests` with
`metadata version is 2.4.0, but the compiler version 2.2.0 can read
versions up to 2.3.0`.

AGP 9.3.x requires Gradle 9.5.0 or higher (`Minimum supported Gradle
version is 9.5.0`), which is
exactly the range we cannot read. **AGP 9.2.1 is the newest release that
runs on Gradle 9.4.1**;
9.1.1 and 9.0.1 were also confirmed to work, 9.3.1 was confirmed not to.

So the AGP ceiling here is a consequence of the Kotlin floor, not an
independent preference. When the
deferred Kotlin 2.3 bump happens, AGP and Gradle can move up with it.

- Standalone sample apps (`CustomEntitlementComputationSample`,
`MagicWeather`, `MagicWeatherCompose`)
move to Kotlin 2.2.21 and gain the `compose-compiler` plugin alias. They
consume our published
  artifacts, so they need a compiler that can read our metadata.
- Unit tests are re-enabled on the release variant via
`HasUnitTestBuilder.enableUnitTest`, because
AGP 9 only creates unit tests for the `testBuildType` variant and the
published variant is release.
- The `androidDependencies` cache-warm CI step is dropped; AGP 9 removed
that task.
- `migrations/v11-MIGRATION.md` documents the new Kotlin floor,
mirroring the v9 guide.
- README `Requirements` now says Kotlin 2.1.0+.

- **Kotlin 2.3 and the K1 to K2 migration**, until React Native ships
the KGP 2.2 pin already on its
`main` branch. Doing it sooner buys nothing and costs every RN app a
migration step.
- **AGP 9 built-in Kotlin** (previously #3965). Not breaking and not
required; the
`android.builtInKotlin=false` / `android.newDsl=false` opt-out only has
to go at AGP 10.

- `assembleDebug` across all modules; release AARs for `:purchases` and
`:ui:revenuecatui`.
- `:purchases` unit tests, `:ui:debugview:verifyPaparazziDefaultsDebug`,
`detektAll`.
- `scripts/api-check.sh` with **zero** `api*.txt` diff.
- Published contract checked directly: POM carries
`kotlin-stdlib:2.2.21` at `compile` scope, the
release AAR still reports `minCompileSdk=1` and
`minAndroidGradlePluginVersion=1.0.0`, and our own
  classes carry metadata `mv = {1, 8, 0}`.
- Consumer floor measured against the real release AAR plus the stdlib
we publish: Kotlin 2.1.21
compiles; 2.0.21 and 1.9.24 fail. That is what the 2.1.0 claim in the
README rests on.

The Kotlin floor is what propagates, not AGP. Nothing downstream needs
AGP 9 or Gradle 9.

The only direct consumer is purchases-hybrid-common, which re-exposes us
at compile scope via
`api(libs.purchases)`. RevenueCat/purchases-hybrid-common#1844 raises
its Kotlin accordingly, and the
hybrid SDKs pick the floor up from there rather than from this repo.

Documentation follows separately:

- RevenueCat/docs#1942 adds the Kotlin minimum to the Android, Flutter
and React Native installation
pages, and adds the 9.x-to-10.x and 10.x-to-11.x migration guides the
site was missing.
- RevenueCat/purchases-flutter#1876 states the Kotlin minimum in that
repo's README. Flutter applies
the version from the app's `android/settings.gradle`, so the requirement
lands on the consuming app
  and the Flutter version itself does not move.
- RevenueCat/react-native-purchases#1925 raises the React Native floor
to 0.80.0, the first release
  whose Gradle plugin pins a Kotlin that can read our metadata.

</details>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Major toolchain and minimum Kotlin version changes affect all
consumers and CI, though public API and published AAR AGP/minSdk
metadata are intentionally unchanged.
>
> **Overview**
> This PR moves the SDK build to **AGP 9.2.1**, **Gradle 9.4.1**, and
**Kotlin 2.2.21**, and bumps related tooling (Poko, Paparazzi, baseline
profile plugin). **`gradle.properties`** opts out of AGP 9’s built-in
Kotlin and new DSL for now.
>
> The **consumer-facing change** is a higher Kotlin floor: README and
**`migrations/v11-MIGRATION.md`** document **Kotlin 2.1.0+** (up from
1.8.0), driven by published stdlib metadata—not a public API change.
>
> **Build-logic** re-enables unit tests on library variants under AGP 9
via **`enableUnitTest`**, and keeps **`aarMetadata.minCompileSdk = 1`**
so published AAR requirements don’t tighten.
>
> Compose sample apps switch to the **Kotlin Compose compiler plugin**
and drop **`composeOptions.kotlinCompilerExtensionVersion`**. Several
modules replace deprecated **`kotlinOptions`** with
**`kotlin.compilerOptions.jvmTarget`**.
>
> **CircleCI** drops the removed **`androidDependencies`** warmup and
uses Gradle cache restore on standalone sample builds instead.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1b6b4df. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
AlvaroBrey added a commit that referenced this pull request Sep 7, 2026
- Removes the `android.builtInKotlin=false` and `android.newDsl=false`
opt-out that #3964 added, from the root build and from both standalone
sample builds, and takes `kotlin-android` out of 11 build scripts and
the 3 convention plugins.
- `ui/debugview/api.txt` loses
`InternalDebugRevenueCatScreenViewModelFactory`. Metalava now analyses
the release variant, and that class lives in `src/debug/kotlin`. The
release AAR contains zero occurrences of it, so it never shipped and the
entry was wrong
- Fixes `:dokkaGenerate`, which is already broken on `11.0-dev`.
Built-in Kotlin registers Dokka source sets per Android variant instead
of per Kotlin source set, and registers them lazily, so both
`:purchases` and `:ui:revenuecatui` now select the documented variant
through `configureEach`.
- Bumps `androidx.navigation` 2.5.3 to 2.9.8. safeargs 2.5.3 detects AGP
through the old DSL and hard fails, so the navigation bump is a
prerequisite for the new DSL rather than an independent upgrade.

### Checklist
- [ ] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-ios` and
hybrids

<details><summary>Agent description</summary>

### Motivation

AGP 9 enables built-in Kotlin and rejects the
`org.jetbrains.kotlin.android` plugin outright:

```
The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0.
```

Opting out needs two flags together, `android.builtInKotlin=false` and
`android.newDsl=false`, because the kotlin-android plugin casts the
android extension to `BaseExtension`, which the new DSL drops. #3964
took that opt-out so the version bump stayed reviewable. AGP 10 removes
the opt-out, so the migration has to happen regardless; doing it
separately keeps the two diffs legible.

### Description

Four things blocked the migration, each hidden behind the previous.

**The old DSL extension types are gone.**
`com.android.build.gradle.BaseExtension` and
`com.android.build.gradle.LibraryExtension` do not exist once the new
DSL is active. The convention plugins now configure
`com.android.build.api.dsl.ApplicationExtension` and
`com.android.build.api.dsl.LibraryExtension`, and set `compileSdk`
directly rather than calling `compileSdkVersion()`.
`KotlinAndroidProjectExtension` stays as is: AGP's built-in Kotlin still
registers it.

**safeargs 2.5.3 cannot see AGP 9.** It fails with `safeargs plugin must
be used with android plugin`, because its detection goes through the old
DSL. The 2.5.3 jar references `com/android/build/gradle/BaseExtension`,
`AppExtension` and `gradle/api/BaseVariant`; the 2.9.8 jar references
only `com.android.build.api.variant.*`. `examples/purchase-tester` is
the only module applying the plugin, and it gets its own commit.

**Dokka source sets changed shape.** With the kotlin-android plugin,
Dokka saw the Kotlin source sets and the config addressed `main`,
`defaults` and `customEntitlementComputation`. Built-in Kotlin instead
registers one source set per Android variant, and registers them after
the `dokka { }` block runs, so `named("defaultsRelease")` throws even
though the name is valid later. Both modules now select through
`matching { }.configureEach`. `:purchases` keeps `reportUndocumented`,
`skipDeprecated`, the Android external documentation link, both source
links and the `paywalls.components` suppression, all moved onto the
selected variant. The explicit `customEntitlementComputation`
suppression is load-bearing, not redundant: Dokka defaults `suppress` to
`!isPublishable` and `customEntitlementComputationRelease` is published,
so it would otherwise be documented.

**`kotlin-test` stops carrying JUnit.** `:feature:galaxy` names
`kotlin-test-junit` instead. Verified rather than inherited: with plain
`kotlin-test`, `org.junit.Test` is unresolved across all 25 test
methods.

**Not visible in the diff:** `./gradlew :dokkaGenerate` fails on the
base commit with `Dokka cannot generate documentation for Android
projects with multiple enabled variants that have common source roots`.
The `docs-deploy` job that runs it is gated on release tags, so #3964
never exercised it. This PR is what makes it pass again, which is a
bigger deal than the diff makes it look.

**Worth noting separately:** AGP 9.2.1 declares
`kotlin-gradle-plugin:2.2.10`, so built-in Kotlin takes its compiler
from buildscript classpath resolution rather than from our catalog.
Confirmed the published contract from #3964 is intact rather than
assumed:

| check | result |
| --- | --- |
| resolved KGP | `2.2.21`, not AGP's bundled `2.2.10` |
| `:purchases` POM | `kotlin-stdlib:2.2.21` at `compile` scope |
| release AAR metadata | `mv=[1,8,0]`, so `languageVersion` 1.8 survives
|

### Regression gates

- `./gradlew :dokkaGenerate` for the Dokka rework. Generates 1367 pages
with `com.revenuecat.purchases.paywalls.components` still suppressed.
- `:feature:galaxy:testDefaultsDebugUnitTest` for the
`kotlin-test-junit` swap. Needs `--rerun-tasks`: a cached run passes
even with the dependency reverted, which is how this could slip through.
- `:examples:purchase-tester:assembleDebug` for the navigation bump.

**Rejected:**

- Wiring the Dokka variant selector to `ANDROID_VARIANT_TO_PUBLISH`
instead of hardcoding `"defaultsRelease"` twice. `fastlane/Fastfile:295`
rewrites that property per publish, so a `customEntitlementComputation`
publish would point Dokka at exactly the variant `:purchases`
suppresses.
- Hoisting the shared Dokka config into `ConfigureDokka.kt`. Dokka's
types are not on build-logic's compile classpath, so it needs the
reflection pattern from `ConfigureMetalava.kt`, and sharing
`reportUndocumented.set(true)` would newly turn it on for the four
public-library modules that have no `dokka { }` block.
- Moving the five hand-rolled app modules onto
`revenuecat-android-application`, which would collapse 5 of the 6
per-module edits. Each keeps 2 to 3 overrides, and both `testpurchases*`
modules deliberately pin old `compileSdk`/`targetSdk` to test
compatibility. Worth a separate PR, not this one.

**Limitations:**

- `examples/MagicWeather` and `test-apps/sdksizetesting` still apply
`kotlin-android`. They are separate builds pinned to AGP 8.13.2, so they
keep the plugin correctly and are untouched.
- `purchases/build.gradle.kts:120` has a dead `androidComponents {
onVariants }` block guarding a `"free"` product flavor that does not
exist in the repo. Pre-existing and unrelated, left alone.

</details>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Wide Gradle/AGP convention changes affect every Android module’s
compile and doc generation; navigation and Dokka behavior are
prerequisites for CI release docs, with limited runtime SDK impact.
> 
> **Overview**
> Completes the AGP 9 migration by removing the
**`android.builtInKotlin=false`** / **`android.newDsl=false`** opt-outs
and dropping **`org.jetbrains.kotlin.android`** across convention
plugins, app modules, samples, and the version catalog.
> 
> Build-logic now configures **`ApplicationExtension`** /
**`LibraryExtension`** from the new DSL (e.g. **`compileSdk`** instead
of **`BaseExtension`**). **`:purchases`** and **`:ui:revenuecatui`**
Dokka blocks use **`matching { }.configureEach`** so documentation
targets **`defaultsRelease`** and suppresses
**`customEntitlementComputation`** variants after AGP registers
per-variant source sets lazily.
> 
> **`androidx.navigation`** is bumped **2.5.3 → 2.9.8** so Safe Args
works with the new DSL. **`:feature:galaxy`** switches unit tests to
**`kotlin-test-junit`**. **`ui/debugview/api.txt`** drops
**`InternalDebugRevenueCatScreenViewModelFactory`** (debug-only; not in
the release API Metalava analyzes).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e45c589. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:breaking Changes that are breaking pr:other

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants