* Reduce fixture UI test time in CI
* Check fixture UI conditions before waits
* Reuse the app across carousel close checks
* Run fixture UI tests on release branches
* feat(ads): cache customer info from verified reward polls instead of refetching
* fix(ads): cache reward poll customer info under the user it was polled for
* refactor(ads): require customerInfo on Outcome.Verified
* refactor(ads): require customerInfo on RewardVerificationPollStatus.Verified
* refactor(ads): require customerInfo on RewardVerificationStatusResponse.init
* docs(ads): describe pollRewardVerification as updating state from the response
* fix(ads): poll reward verification for the user captured before polling
* fix(ads): reword malformed customer info log now that the poll no longer refetches
* fix(ads): treat a null subscriber in the reward poll as no customer info
* refactor(ads): pass appUserID through the poll run instead of storing it in the poller
* refactor(ads): drop async from rewardVerificationResult
* test(ads): assert the null subscriber guard skips the malformed customer info warning
* test(ads): assert the polled appUserID stays fixed across attempts
* docs(ads): fix the fetchRewardVerificationStatus reference in the poller doc comment
* Restrict GitHub write tokens to the jobs that need them
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use the danger-bot context for jobs that use the Danger token
Co-authored-by: Cursor <cursoragent@cursor.com>
* Give snapshot workflows the write token to open snapshot PRs
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: post a merge gate status that tightens once the full suite is approved
Adds a ci/all-tests-gate commit status driven from CircleCI. A no-requires
job posts success at pipeline start, approving the full-suite hold flips
it to pending, and all-tasks-passed posts success again once every full
job is green. Release pipelines post it from all-tests-succeeded so
release PRs are not blocked once the context is required.
The reduced suite needs no gate of its own: its jobs are already required
individually.
* test: find the merge gate by its instance name
all-tasks-passed is now an instance of the parameterized post-gate-status
job, so the workflow entry is keyed by the job name and carries name:.
* ci: never post the default gate success once the full suite is requested
full-suite-not-requested waits for the reduced suite and checks the
hold first: approved means halt without posting, so the gate goes from
unreported straight to pending; anything but on_hold fails the job
rather than posting success. all-tasks-passed reaches the reduced suite
through full-suite-started, so it keeps a single list, and the test
guards the list that gates normal PRs.
* ci: retry the gate status post
A failed POST leaves the gate at its previous value: green if the pending
post fails, stuck if the final success post fails. Retrying transient
errors narrows both. --retry-all-errors is not used: the cimg/base curl
predates it.
* ci: mark the merge gate pending when the bot approves the full suite
The bare @RCGitBot please test comment is the usual way to request the
full suite. Posting pending from the action right after a successful
approval closes the window on that path; a re-comment on an already
approved hold posts nothing. Manual approvals in the CircleCI UI keep
the window of one container start.
* ci: pin the gate's ordering in a test and trim the fan-in list
Asserts the state of each gate instance and the requires edges that
keep pending after the default success and the final success last.
Drops the two reduced-suite jobs all-tasks-passed still listed directly,
and lets the hold check log a curl failure instead of a bare exit code.
* fix(paywalls): stop nested Fill stacks from hanging on iOS 17
On iOS 17, SwiftUI places a frame's content three times per placement
whenever an ancestor proposes an unbounded size, as the paywall's root
ScrollView or ViewThatFits does. Each nested Fill stack multiplies that,
so a paywall about a dozen stacks deep keeps the main thread busy for
minutes and never appears.
Fill sizing now goes through a single-child Layout on iOS 17 that sizes
like the frame did and aligns by size, placing its content once.
* refactor(paywalls): trim FillLayout to its essentials
Drops the negative-proposal clamp, which SwiftUI never needs, and folds
the alignment fractions inline. Behavior and test coverage are unchanged.
* refactor(paywalls): move Layout conformances into extensions
Follows the style guide: stored properties in the declaration, protocol
conformance in an extension.
---------
Co-authored-by: Nevyn Bengtsson <nevyn.jpg@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(paywalls): close open sheet after in-app purchase
A purchase made from inside a V2 paywall sheet only requested a dismissal of
the whole paywall, which does nothing when the paywall is embedded (e.g. the
Flutter PaywallView in a persistent tab), so the sheet stayed open. Close it
when the purchase completes.
ONC-46
* fix(paywalls): close the sheet after every purchase, not just the first
hasPurchasedInSession stays true after the first purchase in an embedded paywall,
so key the sheet close on the session purchase result instead.
* Read PurchaseHandler directly in the sheet modifier instead of a new environment value
* Restore nil-default sessionPurchaseResult environment value
Reading PurchaseHandler as an environment object crashes hosts that render RootView without one (layout fixtures, previews).
* Test that the sheet closes after every purchase
* Add workflow prewarming performance benchmark
* Add performance baseline recording and comparison
* Move performance baseline recording and checks into XCTest
* Declare performance tolerance in tests and exclude legacy project checks
* Simplify memory benchmark and run it on each PR
* Use published workflow for performance benchmark
* Add PR job to record and push performance baselines
* Update performance baselines
* Run baseline automation tests on PRs and use Fastlane with Xcode 27
* Update performance baselines
* Require performance benchmark in PR merge gate
---------
Co-authored-by: RCGitBot <dev+RCGitBot@revenuecat.com>
* test(workflows): add Maestro tests for workflow branch routing
Covers the default route (Testimonials 1 to paywall) and an es-ES locale audience routing to Testimonials 3.
* test(workflows): trim branch test comments
* Warn CocoaPods users about Swift Package Manager migration
* Avoid redundant Xcode warning for CocoaPods migration phase
* Say "the CocoaPods trunk" in the migration warning
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use rev.cat short link in the migration warning
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Switching between tabs with web views and nested billing tabs could show the previous tab’s web content and hide purchase controls. Now we set explicit identities so the index no longer identifies the content
* other: move PurchaseTester to Projects
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* other(ci): add a PurchaseTester smoke build with Tuist
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* other: move rc-maestro to Projects
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* other: rename rc-maestro to AutomatedSDKTestsApp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* other: move PurchaseTester to Projects
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* other(ci): add a PurchaseTester smoke build with Tuist
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* other(samplecat): add a Checkpoints tab with 4 samples
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* other: delete CheckpointTester
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* other: point agents to SampleCat for integration examples
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* other(samplecat): use result.hasEntitlement in checkpoint callbacks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* other(ci): add a SampleCat smoke build with Tuist (#8163)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(ads): dispatch checkpoint ad presentation by format
Ad units are format-locked, so the SDK needs to know which AdMob loader a
checkpoint ad step's unit belongs to. The backend now requires ad_format
on the step; core surfaces it on ResolvedAdStep, RevenueCatUI threads it
through AdPresentationParams, and the AdMob adapter replaces its three
per-format presenters with one AdMobPresenter that dispatches on it and
fails fast on formats it does not implement.
* refactor: presenter composition
* fix: use renamed InviteOnlyCheckpointsApi SPI
* test(ads): assert on shown and failed ad executions
The ad outcome classes are gone, so the format-aware presenter reports
shown for every format and the tests match the execution cases.
* test(ads): match failed ad executions and the ad format param
Claude-Session: https://claude.ai/code/session_01Gj5NLpaCp45dfxDVR5rScP
* fix(ads): keep the ad params initializer internal-only
The adapter tests are the only external caller, so the init is SPI
rather than public, as the no_new_public_inits rule requires. Also
passes the ad format in the missing-presenter behavior test.
Claude-Session: https://claude.ai/code/session_01Gj5NLpaCp45dfxDVR5rScP
* fix(ads): mark AdMob presenter inits as SPI for lint
Claude-Session: https://claude.ai/code/session_01Gj5NLpaCp45dfxDVR5rScP
* test(api): cover AdPresentationParams.adFormat in the RevenueCatUI API tester
* feat(ads): AdMob rewarded checkpoint presenter
* feat(ads): AdMob rewarded interstitial checkpoint presenter
* refactor: common base for both rewarded presenter
* fix: drop weak self in rewarded presentation task
* fix: use renamed InviteOnlyCheckpointsApi SPI
* fix(ads): report rewarded outcomes as shown
AdPresentationResult no longer carries reward cases: a verified reward
reaches the app through the checkpoint result, and a reward that failed
verification is logged while the checkpoint still passes.
* refactor(ads): carry the verification result in the rewarded outcome
* fix: linter
* refactor: use the internal helper
* refactor(ads): drop the verification result from the rewarded outcome
* ci: run actionlint from the shared workflow
Replaces `docker://rhysd/actionlint:latest` with the reusable workflow in `sdk-github-workflows`, which runs a checksum-pinned release binary. GitHub rejects `docker://` references under a selected-actions policy.
* ci: repin shared actionlint workflow
* ci: pin shared actionlint workflow to v10