51 Commits
Author SHA1 Message Date
Dave DeLong 6ed515865a Simplify HTTP path types (#7987) 2026-10-23 11:11:52 -06:00
Antonio PallaresandCursor eac1656a40 Add the hosted checkout session status endpoint (#7885)
* Decode the in-app web checkout open method

Marks a web purchase button as opening its checkout in the in-app bottom
sheet. Nothing reads it yet: routing lands with the button wiring.

Where the method reaches Browser.navigateTo there is no sheet to present,
so it takes the in-app browser rather than opening nothing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Log the in-app browser fallback for the checkout open method

Co-authored-by: Cursor <cursoragent@cursor.com>

* Model the checkout web view lifecycle as a single state

The three booleans tracking the web view's progress could combine into
states that cannot happen, and `hasFinished` guarded only the callback:
a navigation still in flight after the return URL could set `.failed`
and blank the page while the host was dismissing the sheet.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Hold the checkout status until a host is listening

Detents without `presentationContentInteraction`, iOS 16.4, turn a drag
on the page into a sheet resize, so 16.0 to 16.3 gets the full-height
sheet rather than an unscrollable medium one.

The sheet assigns `onFinished` on appearance, and loading can start
before that, so a page reaching a return URL first would drop it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim comments in the checkout sheet presentation

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add the hosted checkout entry point the paywall can call

Composes the external purchase preparation and the hosted checkout call
behind an @_spi(Internal) entry point on Purchases, so RevenueCatUI can
start a checkout with a single call.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Decode the hosted web checkout purchase button method

Co-authored-by: Cursor <cursoragent@cursor.com>

* Give the checkout sheet its own object ID in the Xcode project

Co-authored-by: Cursor <cursoragent@cursor.com>

* Resolve the hosted checkout open method in the model

Co-authored-by: Cursor <cursoragent@cursor.com>

* Leave the open method default to the caller

Co-authored-by: Cursor <cursoragent@cursor.com>

* Take the hosted checkout method without an open method

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add the checkout web view host

Owns the WKWebView that presents a payment provider's checkout page, detects the
return URL that ends the flow, and reports success or cancellation to its host.

Navigations are not restricted: the checkout URL comes from RevenueCat's backend
and the page it opens is free to reach the provider's own domains and any bank's
3DS challenge.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Take both return URLs from the checkout start response

The backend now returns success_url and cancel_url alongside checkout_url, so
the outcome is told by which one the page navigates to rather than by a query
parameter the SDK knows the name of.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Compare the root return path consistently

URL.path strips trailing slashes for every path but the root, so the length
guard was the only thing keeping https://host/ and https://host from matching.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the unused reload from the checkout view model

Nothing calls it until something presents a retry, so leave it to whichever
change needs it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim comments in the checkout web view

Co-authored-by: Cursor <cursoragent@cursor.com>

* Model the checkout web view lifecycle as a single state

The three booleans tracking the web view's progress could combine into
states that cannot happen, and `hasFinished` guarded only the callback:
a navigation still in flight after the return URL could set `.failed`
and blank the page while the host was dismissing the sheet.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Leave the Test Store out of the hosted checkout

A test_ key stops at the eligibility gate and reports external purchases
as unavailable, so the caller buys through StoreKit rather than opening a
checkout the Test Store does not support yet.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore Package.resolved

Co-authored-by: Cursor <cursoragent@cursor.com>

* Follow the external purchase outcomes the manager reports now

The manager tells payment authorization apart from eligibility and stops
a preparation asked for while another is under way, neither of which
existed when the checkout was written. It also reuses the instance the
link-out flow uses, so a single preparation runs at a time however the
customer is taken to the checkout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add the checkout web view host

Owns the WKWebView that presents a payment provider's checkout page, detects the
return URL that ends the flow, and reports success or cancellation to its host.

Navigations are not restricted: the checkout URL comes from RevenueCat's backend
and the page it opens is free to reach the provider's own domains and any bank's
3DS challenge.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Take both return URLs from the checkout start response

The backend now returns success_url and cancel_url alongside checkout_url, so
the outcome is told by which one the page navigates to rather than by a query
parameter the SDK knows the name of.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Compare the root return path consistently

URL.path strips trailing slashes for every path but the root, so the length
guard was the only thing keeping https://host/ and https://host from matching.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the unused reload from the checkout view model

Nothing calls it until something presents a retry, so leave it to whichever
change needs it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim comments in the checkout web view

Co-authored-by: Cursor <cursoragent@cursor.com>

* Model the checkout web view lifecycle as a single state

The three booleans tracking the web view's progress could combine into
states that cannot happen, and `hasFinished` guarded only the callback:
a navigation still in flight after the return URL could set `.failed`
and blank the page while the host was dismissing the sheet.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Open the checkout the purchase button asks for in a sheet

A purchase button configured for the in-app checkout now runs Apple's
flow, creates the session and presents it, rather than buying through
StoreKit. The page reporting success or a cancellation takes the sheet
down and settles the paywall; a customer who closes the sheet without
the page saying anything is left for the backend to settle, which is
not wired up yet.

A device that cannot take part in Apple's programme still buys through
StoreKit, as an SDK that does not know this purchase method would.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Let the checkout page reach the bottom edge

Stopping above the home indicator leaves a strip of the host's
background under a page that fills its sheet. The web view insets its
own content by the safe area, so nothing the page puts at the bottom
goes out of reach.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add the checkout web view host

Owns the WKWebView that presents a payment provider's checkout page, detects the
return URL that ends the flow, and reports success or cancellation to its host.

Navigations are not restricted: the checkout URL comes from RevenueCat's backend
and the page it opens is free to reach the provider's own domains and any bank's
3DS challenge.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Take both return URLs from the checkout start response

The backend now returns success_url and cancel_url alongside checkout_url, so
the outcome is told by which one the page navigates to rather than by a query
parameter the SDK knows the name of.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Compare the root return path consistently

URL.path strips trailing slashes for every path but the root, so the length
guard was the only thing keeping https://host/ and https://host from matching.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the unused reload from the checkout view model

Nothing calls it until something presents a retry, so leave it to whichever
change needs it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim comments in the checkout web view

Co-authored-by: Cursor <cursoragent@cursor.com>

* Model the checkout web view lifecycle as a single state

The three booleans tracking the web view's progress could combine into
states that cannot happen, and `hasFinished` guarded only the callback:
a navigation still in flight after the return URL could set `.failed`
and blank the page while the host was dismissing the sheet.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Let the checkout page reach the bottom edge

Stopping above the home indicator leaves a strip of the host's
background under a page that fills its sheet. The web view insets its
own content by the safe area, so nothing the page puts at the bottom
goes out of reach.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Read what the checkout returned off the view model

The status was pushed into SwiftUI state in the same breath as the
dismissal that reads it, and the view model handed it over and forgot
it, leaving nothing to fall back on. A page that returned before the
sheet was on screen also asked to be dismissed from inside its own
presentation, which SwiftUI is in no position to honour.

The status now stays on the view model, the sheet keeps what it
presented so it can ask at any time, and a checkout that returned
early waits a hop before asking to come down.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add the checkout web view host

Owns the WKWebView that presents a payment provider's checkout page, detects the
return URL that ends the flow, and reports success or cancellation to its host.

Navigations are not restricted: the checkout URL comes from RevenueCat's backend
and the page it opens is free to reach the provider's own domains and any bank's
3DS challenge.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Take both return URLs from the checkout start response

The backend now returns success_url and cancel_url alongside checkout_url, so
the outcome is told by which one the page navigates to rather than by a query
parameter the SDK knows the name of.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Compare the root return path consistently

URL.path strips trailing slashes for every path but the root, so the length
guard was the only thing keeping https://host/ and https://host from matching.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the unused reload from the checkout view model

Nothing calls it until something presents a retry, so leave it to whichever
change needs it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim comments in the checkout web view

Co-authored-by: Cursor <cursoragent@cursor.com>

* Model the checkout web view lifecycle as a single state

The three booleans tracking the web view's progress could combine into
states that cannot happen, and `hasFinished` guarded only the callback:
a navigation still in flight after the return URL could set `.failed`
and blank the page while the host was dismissing the sheet.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Let the checkout page reach the bottom edge

Stopping above the home indicator leaves a strip of the host's
background under a page that fills its sheet. The web view insets its
own content by the safe area, so nothing the page puts at the bottom
goes out of reach.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim comments in the hosted checkout manager

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim the remaining hosted checkout result comments

Co-authored-by: Cursor <cursoragent@cursor.com>

* Send the paywall session without a paywall identifier

khepri#24938 made `paywall_id` optional precisely so that iOS could
send the session identifier, which is the key the paywall events are
joined on. Dropping the whole paywall object for a paywall that has no
identifier of its own threw that session away.

Also trims the last of the result comments.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim the hosted checkout session comments

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the remaining hosted checkout session comments

Co-authored-by: Cursor <cursoragent@cursor.com>

* Silence missing docs for the whole session file

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add the checkout web view host

Owns the WKWebView that presents a payment provider's checkout page, detects the
return URL that ends the flow, and reports success or cancellation to its host.

Navigations are not restricted: the checkout URL comes from RevenueCat's backend
and the page it opens is free to reach the provider's own domains and any bank's
3DS challenge.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Take both return URLs from the checkout start response

The backend now returns success_url and cancel_url alongside checkout_url, so
the outcome is told by which one the page navigates to rather than by a query
parameter the SDK knows the name of.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Compare the root return path consistently

URL.path strips trailing slashes for every path but the root, so the length
guard was the only thing keeping https://host/ and https://host from matching.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the unused reload from the checkout view model

Nothing calls it until something presents a retry, so leave it to whichever
change needs it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim comments in the checkout web view

Co-authored-by: Cursor <cursoragent@cursor.com>

* Model the checkout web view lifecycle as a single state

The three booleans tracking the web view's progress could combine into
states that cannot happen, and `hasFinished` guarded only the callback:
a navigation still in flight after the return URL could set `.failed`
and blank the page while the host was dismissing the sheet.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Let the checkout page reach the bottom edge

Stopping above the home indicator leaves a strip of the host's
background under a page that fills its sheet. The web view insets its
own content by the safe area, so nothing the page puts at the bottom
goes out of reach.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep the checkout sheet's callbacks on the main actor

The status the sheet reports is read off the view model, which is main
actor isolated, so the methods doing the reading have to say they run
there. Swift 5.10 does not infer it from the ViewModifier conformance
the way newer compilers do, and the iOS 16 job builds with Xcode 15.4.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Present the in-app web checkout outside Apple's external purchase programme

An ineligible customer, or an app that has not opted into the programme, now
gets the checkout presented with no external purchase token behind it, the same
way a web purchase in the browser is opened today, instead of being routed to a
StoreKit purchase.

A Test Store API key still leaves the caller to buy through StoreKit, since no
checkout session can be created for it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Present the in-app web checkout with a Test Store API key

The backend creates a sandbox checkout session for a test_ key, so there is no
reason to leave the caller to buy through StoreKit instead of presenting the
checkout, which is what a web purchase in the browser already does.

Apple's programme has no App Store to apply to behind such a key, so the
checkout is presented with no notice shown and no token minted.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep the console quiet for apps outside Apple's programme

The external purchase side already reports why no external purchase was
prepared, and says nothing at all where the app takes no part in the programme,
so the checkout has nothing to add.

Also covers the window before the notice comes up, where the one checkout at a
time rule now decides between a checkout that opens and one that is dropped.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say what the outcome does rather than what it used to do

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say that Apple's custom link does not apply rather than that nothing can be bought

An ineligible customer is not a customer who cannot buy outside the App Store:
the custom link may simply not apply where they are, and the purchase goes ahead
without it. Dropped to debug, since that is the normal case anywhere the link
does not reach.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop a note on an outcome no caller acts on

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop a note on an outcome no caller acts on

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove two conveniences on the preparation result that no caller uses

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore the test dependencies dropped from Package.resolved

Co-authored-by: Cursor <cursoragent@cursor.com>

* Run Apple's external purchase flow with a Test Store key

Eligibility, the notice and the token follow the app's entitlement and the
customer's storefront, none of which has anything to do with the key the SDK was
configured with, and the purchase behind such a token is a sandbox one either
way. Skipping the flow left a Test Store key, the shortest path a developer has
to trying the checkout out, as the one key that could not exercise it.

Reverts the carve-out added in #7643.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Route the hosted web checkout method to the checkout it asks for

The merge left the placeholder branch that sent hosted_web_checkout to StoreKit
alongside the one that opens the checkout, and the checkout no longer reports an
external purchase being unavailable: it presents itself in that case too.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Tell the customer when the in-app checkout is refused for a product they own

The backend answers 409/7772 for a product the customer already has, which
the SDK decoded as an unknown error and reported as a plain failure: the
sheet never opened and the tap looked like it did nothing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Word the already-owned alert as Apple does, by product category

Only the title carries over: Apple's own alert also names the renewal date
and offers to manage the subscription, both of which describe a purchase
made on the App Store rather than this one.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Present the in-app web checkout outside Apple's external purchase programme

An ineligible customer, or an app that has not opted into the programme, now
gets the checkout presented with no external purchase token behind it, the same
way a web purchase in the browser is opened today, instead of being routed to a
StoreKit purchase.

A Test Store API key still leaves the caller to buy through StoreKit, since no
checkout session can be created for it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep the console quiet for apps outside Apple's programme

The external purchase side already reports why no external purchase was
prepared, and says nothing at all where the app takes no part in the programme,
so the checkout has nothing to add.

Also covers the window before the notice comes up, where the one checkout at a
time rule now decides between a checkout that opens and one that is dropped.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say what the outcome does rather than what it used to do

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say that Apple's custom link does not apply rather than that nothing can be bought

An ineligible customer is not a customer who cannot buy outside the App Store:
the custom link may simply not apply where they are, and the purchase goes ahead
without it. Dropped to debug, since that is the normal case anywhere the link
does not reach.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop a note on an outcome no caller acts on

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove two conveniences on the preparation result that no caller uses

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore the test dependencies dropped from Package.resolved

Co-authored-by: Cursor <cursoragent@cursor.com>

* Ask for the checkout through the purchase handler

The handler is the paywall's way to the SDK, so reaching for the singleton
meant checking whether it was configured at all. It never had to: an
unconfigured paywall is never rendered, and the handler already stands in for
a purchase that cannot be made.

Drops the StoreKit fallback that only that check could reach.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Let the mapping speak for the outcome it produces

The init lists what turns into nothing, so the case did not need to repeat it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Present the in-app web checkout outside Apple's external purchase programme

An ineligible customer, or an app that has not opted into the programme, now
gets the checkout presented with no external purchase token behind it, the same
way a web purchase in the browser is opened today, instead of being routed to a
StoreKit purchase.

A Test Store API key still leaves the caller to buy through StoreKit, since no
checkout session can be created for it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep the console quiet for apps outside Apple's programme

The external purchase side already reports why no external purchase was
prepared, and says nothing at all where the app takes no part in the programme,
so the checkout has nothing to add.

Also covers the window before the notice comes up, where the one checkout at a
time rule now decides between a checkout that opens and one that is dropped.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say what the outcome does rather than what it used to do

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say that Apple's custom link does not apply rather than that nothing can be bought

An ineligible customer is not a customer who cannot buy outside the App Store:
the custom link may simply not apply where they are, and the purchase goes ahead
without it. Dropped to debug, since that is the normal case anywhere the link
does not reach.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop a note on an outcome no caller acts on

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove two conveniences on the preparation result that no caller uses

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore the test dependencies dropped from Package.resolved

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf: add checkout request lane and generalize backend lanes

Checkout tap-path requests now bypass the shared backend serialization
stack via a dedicated lane, using the same generalized lane factory as
remote config.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address review feedback on backend lanes

Restore misplaced comments, add BackendLanesTests, and use plain Sendable
on BackendLanes now that the justification matches the type.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Clarify timeoutManager sharing comment wording

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim comments around the already-owned alert

Co-authored-by: Cursor <cursoragent@cursor.com>

* State where the hosted checkout is presented from

Presenting the sheet is main actor work, and the await in front of it said the
opposite: nothing suspends there, which `pod lib lint` counts as a warning and
so refuses the podspec.

Annotating the caller rather than dropping the await alone keeps this from
depending on whether a given compiler infers a View's methods to be isolated,
which is the same disagreement that put the sheet's callbacks on the main actor.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Present the in-app web checkout outside Apple's external purchase programme

An ineligible customer, or an app that has not opted into the programme, now
gets the checkout presented with no external purchase token behind it, the same
way a web purchase in the browser is opened today, instead of being routed to a
StoreKit purchase.

A Test Store API key still leaves the caller to buy through StoreKit, since no
checkout session can be created for it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep the console quiet for apps outside Apple's programme

The external purchase side already reports why no external purchase was
prepared, and says nothing at all where the app takes no part in the programme,
so the checkout has nothing to add.

Also covers the window before the notice comes up, where the one checkout at a
time rule now decides between a checkout that opens and one that is dropped.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say what the outcome does rather than what it used to do

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say that Apple's custom link does not apply rather than that nothing can be bought

An ineligible customer is not a customer who cannot buy outside the App Store:
the custom link may simply not apply where they are, and the purchase goes ahead
without it. Dropped to debug, since that is the normal case anywhere the link
does not reach.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop a note on an outcome no caller acts on

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove two conveniences on the preparation result that no caller uses

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore the test dependencies dropped from Package.resolved

Co-authored-by: Cursor <cursoragent@cursor.com>

* Test production checkout lane wiring

Build Backend through the convenience initializer in the parallel
checkout test so removing .checkout from dedicatedLanes fails the suite.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restrict RequestLane to canonical constants

Replace the synthesized memberwise initializer with a private init so
lane values cannot be built ad hoc and miss dedicatedConfigurations keys.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Clarify apiSourceFailover is shared across lanes

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove RequestLane private init doc comment

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add checkout lane tests for shared lane and token refresh dedupe.

Pin that both checkout endpoints share one lane in a single backend, and that concurrent 401s across lanes trigger exactly one token refresh when HTTPClients share a TokenManager.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Clear HTTP client caches on every backend lane.

Backend now retains its BackendLanes and delegates cache clearing to all lane configurations, with a test that asserts each lane's HTTPClient is cleared.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Cover external purchase token in checkout lane parallel test.

Extend the production wiring test so both checkout tap-path requests complete while offerings hangs on the default lane.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Derive Backend default config from lanes.

Remove the redundant backendConfig parameter from Backend's required initializer so config always matches lanes[.default].

Co-authored-by: Cursor <cursoragent@cursor.com>

* Warn when a backend lane falls back to the default configuration.

Log a warning when a BackendLanes instance has dedicated lanes but the requested lane is missing, so miswired APIs are visible in production without noisy warnings for single-lane bundles.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Move test-only Backend and BackendLanes initializers to UnitTests.

Single-lane construction helpers now live in the test target so production code only exposes the multi-lane factory path.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Register BackendTestInitializers in StoreKitUnitTests.

MockBackend is compiled into both UnitTests and StoreKitUnitTests, so the
test-only Backend initializers helper must be in both targets' Sources phases.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Ask the backend what became of a checkout session

The checkout page returning to its success URL says the customer finished paying,
not that the purchase has landed, so the session is polled until the backend
settles it one way or the other.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Settle a checkout on the customer and the reason it ended

The session belongs to the customer who started it, so the poll asks about that
one rather than about whoever is current when an attempt goes out: a customer
change mid-poll would otherwise turn a landed purchase into no answer at all.

A confirmed purchase is fetched before it is reported, so a caller acting on the
outcome finds the entitlement on the customer instead of the state from before
the checkout. A fetch that does not land leaves the purchase standing.

The failure the backend gives now travels with the outcome, which tells a
declined payment from a checkout that never got going.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Let the view showing the checkout hand its links to the browser

The button set onOpenExternalURL on the view model it was about to present, and
WebCheckoutView assigns that same closure when it appears. Nothing could reach
the button's: the page only starts loading on that appearance, a line after.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Present the in-app web checkout outside Apple's external purchase programme

An ineligible customer, or an app that has not opted into the programme, now
gets the checkout presented with no external purchase token behind it, the same
way a web purchase in the browser is opened today, instead of being routed to a
StoreKit purchase.

A Test Store API key still leaves the caller to buy through StoreKit, since no
checkout session can be created for it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep the console quiet for apps outside Apple's programme

The external purchase side already reports why no external purchase was
prepared, and says nothing at all where the app takes no part in the programme,
so the checkout has nothing to add.

Also covers the window before the notice comes up, where the one checkout at a
time rule now decides between a checkout that opens and one that is dropped.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say what the outcome does rather than what it used to do

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say that Apple's custom link does not apply rather than that nothing can be bought

An ineligible customer is not a customer who cannot buy outside the App Store:
the custom link may simply not apply where they are, and the purchase goes ahead
without it. Dropped to debug, since that is the normal case anywhere the link
does not reach.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop a note on an outcome no caller acts on

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove two conveniences on the preparation result that no caller uses

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore the test dependencies dropped from Package.resolved

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the note on who hands the checkout's links to the browser

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore the test dependencies dropped from Package.resolved

Co-authored-by: Cursor <cursoragent@cursor.com>

* Call what the paywall does with a checkout tap an action

Start read as a verb or an event, so HostedCheckout.Start(.failed) looked like it
began something. The cases are things the paywall does: present, or nothing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Name the already-owned mapping test's type after the rename

Co-authored-by: Cursor <cursoragent@cursor.com>

* Ask the purchase interceptor before starting a hosted checkout

Apps that gate purchases with onPurchaseInitiated (e.g. behind sign in) could be bypassed by the hosted checkout, and Apple's flow may mint a token that must be reported, so the interceptor now runs first.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the token note from HostedCheckout.start's docs

Co-authored-by: Cursor <cursoragent@cursor.com>

* Track hosted checkouts as purchase initiated

The event was only used for its data to attribute the checkout session, so these purchases never showed up as initiated in paywall analytics, unlike the StoreKit and performPurchase paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Move the checkout poller to its own PR

Keeps this PR to the network layer, so it can be reviewed on its own. The poller, HostedCheckoutManager.pollCheckout and Purchases.pollHostedCheckout follow in a stacked PR.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Share the purchase tap checks between the in-app and hosted checkout paths

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim packageForPurchaseTap's doc comment

Co-authored-by: Cursor <cursoragent@cursor.com>

* Present the in-app web checkout outside Apple's external purchase programme

An ineligible customer, or an app that has not opted into the programme, now
gets the checkout presented with no external purchase token behind it, the same
way a web purchase in the browser is opened today, instead of being routed to a
StoreKit purchase.

A Test Store API key still leaves the caller to buy through StoreKit, since no
checkout session can be created for it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep the console quiet for apps outside Apple's programme

The external purchase side already reports why no external purchase was
prepared, and says nothing at all where the app takes no part in the programme,
so the checkout has nothing to add.

Also covers the window before the notice comes up, where the one checkout at a
time rule now decides between a checkout that opens and one that is dropped.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say what the outcome does rather than what it used to do

Co-authored-by: Cursor <cursoragent@cursor.com>

* Say that Apple's custom link does not apply rather than that nothing can be bought

An ineligible customer is not a customer who cannot buy outside the App Store:
the custom link may simply not apply where they are, and the purchase goes ahead
without it. Dropped to debug, since that is the normal case anywhere the link
does not reach.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop a note on an outcome no caller acts on

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove two conveniences on the preparation result that no caller uses

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore the test dependencies dropped from Package.resolved

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add hosted checkout status snapshots and fix the email escaping expectation

* Read the hosted checkout status at the top level of the response

The backend now returns the operation's fields directly instead of nesting them under `operation`.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Read an unrecognized hosted checkout status as pending

The backend only ever sends started, in_progress, succeeded or failed, so an unknown case was a status it never returns.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Stop reading in_progress as a hosted checkout status

The backend no longer sends it. Should it still arrive, it reads as pending like any status the SDK does not recognize.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Trim hosted checkout status comments

Co-authored-by: Cursor <cursoragent@cursor.com>

* Move the hosted checkout query escaping next to the path escaping

Co-authored-by: Cursor <cursoragent@cursor.com>

* Read an in-progress hosted checkout session as still under way

purchases-js polls on it too, and the SDK was warning about it as unrecognized.

* Add request snapshots for the in-progress hosted checkout status test

* Say where hosted checkout failure codes come from

* Join the hosted checkout status cache key with a hyphen, like other operations

* Say what the hosted checkout status endpoint gives in plainer words

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-02 17:08:30 +02:00
Peter Porfy a2733a1bc8 feat(ads): send ad unit id to backend during reward polling (#7872)
* feat(ads): send ad unit id to backend during reward polling

* refactor: built in url encoding

* refactor: change ad unit position
2026-10-01 15:53:46 +02:00
Rick 6026b4ff1f Enable response signature verification for web purchase redemption (#7874) 2026-09-30 16:50:20 +02:00
Dave DeLong bd418527c6 Remove assertion (#7665)
The non-IAM path is used when logging, so even when IAM is enabled, this can still cause issues. Attempting to spend without enabling IAM will still fail.
2026-09-08 13:08:12 -06:00
f14a56dbdf Add the networking layer for starting a hosted checkout (#7627)
* Add the networking layer for starting a hosted checkout

POST /rcbilling/v1/hosted-checkout creates the checkout session with the
payment provider and returns the page to present for it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Send the Apple external purchase token id when starting a checkout

The endpoint ties the checkout session to the registered token, so the
token id is also part of the request's dedup key: two taps carrying
different tokens must never share one session.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Document what PostHostedCheckoutOperation is for

Co-authored-by: Cursor <cursoragent@cursor.com>

* Retry the hosted checkout request

Retries only ever fire on a 429, which the server rejects before creating
a session, so there is nothing to strand. Giving up instead costs the
customer the tap they are waiting on.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Name a case where no external purchase token applies

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the comments on the hosted checkout path cases

Co-authored-by: Cursor <cursoragent@cursor.com>

* Generating new test snapshots (#7633)

* Generating new test snapshots (#7634)

* Generating new test snapshots (#7635)

* Generating new test snapshots (#7636)

* Generating new test snapshots (#7637)

* Generating new test snapshots (#7638)

* Generating new test snapshots (#7639)

* Generating new test snapshots (#7640)

* Generating new test snapshots (#7641)

* Cover the created status of a new checkout session

The endpoint answers 201, which is the status the flow actually reads.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Spell ID and URL the way the rest of the SDK does

The decoder converts from snake case, which yields Id and Url, so the
names have to be mapped rather than inferred.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Send the presented offering context when starting a checkout

A purchase made through the in-app sheet has to carry the same
attribution a StoreKit purchase from that paywall would, so placement,
targeting rule, workflow, step and paywall travel with it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add snapshots for the presented offering context request

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2026-09-07 16:42:50 +02:00
c6ccc5e8d8 Add the networking layer for registering external purchase tokens (#7596)
* Add the networking layer for registering external purchase tokens

Registers a StoreKit external purchase token with the backend so the
resulting identifier can be handed to the checkout page. Nothing calls
this yet.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Cover the created status of a first token registration

The backend answers 201 the first time a token is stored and 200 when
the same token is re-submitted.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Don't send an ETag when registering an external purchase token

ETag responses are cached by URL alone, and every registration posts a
different body to the same URL, so a cached response could be replayed for
a token it doesn't belong to.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Read only the identifier from the external purchase token response

The checkout page only needs the registration identifier, so the remaining
response fields have no consumer in the SDK.

Also drop the hand-written request body encoding, which the compiler
synthesizes identically, and the signature content, which this endpoint
doesn't sign.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Generating new test snapshots (#7606)

* Generating new test snapshots (#7607)

* Generating new test snapshots (#7608)

* Generating new test snapshots (#7611)

* Generating new test snapshots (#7610)

* Generating new test snapshots (#7609)

* Generating new test snapshots (#7612)

* Generating new test snapshots (#7614)

* Generating new test snapshots (#7615)

* Document what PostExternalPurchaseTokenOperation is for

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the comment on ExternalPurchaseTokenResponse

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2026-09-07 13:26:27 +02:00
Dave DeLong a1781cb28e (Internal) Spend virtual currencies (#7507) 2026-08-27 16:14:19 -06:00
Dave DeLongandÁlvaro Brey 8349a4afd1 Implement IAM Login Operations (#7439)
* ci: announce modifications in the SDK API feed (#7451)

An attribute-only modification is neither an addition nor a break, so it reached the feed as a headline and a link, with the headline claiming new API.

* Implement the IAM network operations

# Conflicts:
#	RevenueCat.xcodeproj/project.pbxproj

# Conflicts:
#	RevenueCat.xcodeproj/project.pbxproj

* target membership

# Conflicts:
#	RevenueCat.xcodeproj/project.pbxproj

* fix test

* address cursor feedback

* fix imports

* revert fetch policy

* missed one

---------

Co-authored-by: Álvaro Brey <alvaro.brey@revenuecat.com>
2026-08-21 14:04:22 -06:00
Dave DeLongandÁlvaro Brey 3eb01c32bc HTTP Paths can change their absolute path based on IAM enablement (#7422)
* ci: announce modifications in the SDK API feed (#7451)

An attribute-only modification is neither an addition nor a break, so it reached the feed as a headline and a link, with the headline claiming new API.

* HTTP Paths can change their absolute path based on IAM enablement

* missed one

* fix failures

* fix crashing test

* address feedback

* unit tests

* slightly more graceful

* fix signing test

* fix other test

* log on invalid path

* throwAssertion is unavailable on watchOS

* fix backend integration tests

* fix import

---------

Co-authored-by: Álvaro Brey <alvaro.brey@revenuecat.com>
2026-08-21 14:04:21 -06:00
e7384b8282 Re-tier HTTP request timeouts around per-host memory (#7179)
* Re-tier HTTP request timeouts around per-source memory

Replaces the single global "recent timeout" slot in `HTTPRequestTimeoutManager`
with a per-host map (`host -> lastTimeoutDate`, 10-minute per-entry expiry,
pruned on access) and re-tiers the request timeouts:

- Main-source, endpoint with no fallback-URL support: 15s base / 5s reduced
- Main-source, endpoint with fallback-URL support: 5s base / 2s reduced
- Fallback-host and proxied requests: flat 30s (never consult the memory)

A main-source request to host H uses the reduced tier iff H has a non-expired
timeout entry. Timeouts are now recorded for any non-fallback request (dropping
the previous fallback-support-only condition), keyed by attempt host; a
successful main-source response clears only that host's entry. Renames the
`timeoutOnMainBackendForFallbackSupportedEndpoint` result to `mainSourceTimedOut`.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Define flat timeout tier as a Timeout constant

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add exhaustive multi-host tests for timeout manager

Co-authored-by: Cursor <cursoragent@cursor.com>

* Honor developer networkTimeout via internal NetworkTimeout enum

Introduce an internal NetworkTimeout enum (.default/.custom) threaded from
Configuration down to HTTPRequestTimeoutManager. When a developer sets
networkTimeout, it replaces the built-in base/flat tiers while the reduced
fail-fast tiers stay fixed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add NetworkTimeout.swift to RevenueCat.xcodeproj

The file was only picked up by the SPM/Tuist directory globs, so the
hand-maintained Xcode project build failed to resolve the type.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Gate re-tiered no-fallback timeouts behind usesRemoteConfigAPISources

The aggressive no-fallback fail-fast timeouts and their per-host timeout
recording now apply to main-API requests only when the usesRemoteConfigAPISources
dangerous setting is enabled. With it disabled (the default) the legacy network
timeout is preserved. The gating is a per-call timeout(...) parameter so
blob-source downloads can opt into the re-tiered timeouts independently.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep the legacy flat timeout when re-tiered timeouts are off, and share one manager

The fallback-host and proxied tiers returned the new 30s flat timeout regardless of
`usesRemoteConfigAPISources`, halving the legacy 60s (or custom) network timeout for
proxy users and every fallback-host request. They now honor `reTieredTimeoutsEnabled`
like the no-fallback tier does, so only the fallback-supported tiers behave the same
either way.

`Backend` also built one timeout manager per HTTPClient, so the main and remote-config
lanes kept independent per-host memories despite talking to the same hosts. They now
share one, and `HTTPClient` requires both the manager and the network timeout so this
wiring can't be dropped silently.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Clear a host's fail-fast memory as soon as it answers

The memory exists to remember how quickly a host responds, so a non-error
response is evidence the host recovered even when decoding or verifying it
fails afterwards. Deciding off the raw status code instead of the parsed result
keeps those failures from leaving a responsive host stuck on the reduced
timeout tier.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Name the requestTimeoutManager in the fail-fast comment

Co-authored-by: Cursor <cursoragent@cursor.com>

* Tighten comments on the Backend-shared failover and timeout manager

Name the instances the comments describe so it's clear what is shared.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Record API source timeouts before health checks

* Test shared timeout state across backend lanes

* Normalize URL timeout classification across platforms

* Remove unused RemoteConfigAPI test hook

* Skip HTTP stubs lane tests on watchOS

* Treat the remote config fallback path as a fallback host request

`FallbackPath` bakes the fallback host into its `serverHostURL`, but `fallbackUrlIndex` is
nil on those requests, so `isFallbackURLRequest` was false and they were classified as
main-source: with API sources enabled they drew the aggressive 15s/5s no-fallback tiers
instead of the flat fallback timeout, their timeouts armed the per-host fail-fast memory
for the fallback host, and their successes cleared the main source's entry.

Add `isFallbackHostPath` so a path can declare that it targets a fallback host on its own,
and use it alongside the index when picking the tier and updating the memory.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Rick van der Linden <rick.vanderlinden@revenuecat.com>
2026-08-03 14:51:50 +02:00
c4d824d68c feat(remote-config): source API base host from remote-config sources (#7123)
* feat(remote-config): wire manager behind feature flag

* fix(remote-config): only mark refresh fresh after cache write

* feat(remote-config): source API base host from remote-config sources

Resolve the API base host from the shared RemoteConfigSourceProvider (with
the embedded defaults as a floor) instead of the hardcoded apiBaseURL, so
requests can fail over across API hosts. proxyURL and an overridden
apiBaseURL still pin the host and bypass API sources, and per-endpoint
fallback URLs are unchanged.

The provider is created once in the composition root (gated on
remoteConfigEnabled) and injected explicitly into Backend/HTTPClient; nil
preserves the existing SystemInfo.apiBaseURL behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(remote-config): derive nullable API source provider and manager from remoteConfigEnabled

Create the RemoteConfigSourceProvider (and its shared on-disk cache) up front,
gated on remoteConfigEnabled, and build the RemoteConfigManager from the
provider's nullability after Backend exists so it can reuse backend.remoteConfigAPI.
Keeps a single shared source provider + disk cache across the networking layer
and the manager, and avoids touching the disk cache directory when remote config
is disabled.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(remote-config): drop redundant remote-config wiring comment

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(remote-config): drop redundant remote-config manager comment

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(remote-config): simplify APISourceProviding doc comment

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(remote-config): always wire API source provider, gate only disk cache

Create the RemoteConfigSourceProvider unconditionally and inject it into
Backend/HTTPClient so main-API host resolution runs even when remote config
is disabled (embedded default sources as the floor). Only the on-disk cache
and the real RemoteConfigManager stay gated on remoteConfigEnabled; the
provider's topic store is now optional (nil means defaults-only).

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(remote-config): rename APISourceProviding to APISourceProviderType

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(remote-config): resolve web billing host from API sources

The web billing paths are hosted on the same api.revenuecat.com host as the
main API, so they should resolve their base host from the API source provider
too (with failover) instead of the static serverHostURL.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(remote-config): drop redundant web billing API-sources comments

The comments restated what the code and surrounding context already convey.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(remote-config): assert ETag retry stays on the API source host

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(remote-config): drop APISourceProviderType, use RemoteConfigSourceProviderType directly

The networking layer depended on the narrow APISourceProviderType protocol, an extra
abstraction over RemoteConfigSourceProviderType that added little. Fold currentAPISource()
into RemoteConfigSourceProviderType and have HTTPClient/Backend depend on it directly.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(remote-config): wire apiSourceProvider into dedicated remote-config lane

The main-into-branch merge left `HTTPClient.dedicatedRemoteConfig` (and its
call site) without the new `apiSourceProvider` argument, breaking compilation.
Thread the provider through so the dedicated `/config` lane also resolves its
base host from remote-config API sources, matching the main lane.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: gate remote-config API sources behind usesRemoteConfigAPISources

Resolve main-API request hosts from the remote-config API sources only when
the `usesRemoteConfigAPISources` dangerous setting is enabled. When disabled
(the default), requests keep targeting the static `SystemInfo.apiBaseURL`.

Enable the setting in the API-source `HTTPClient` tests and add a test
asserting an injected provider is ignored while the setting is off.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(test): pass apiSourceProvider in BackendRemoteConfigLaneTests HTTPClient init

The lane test constructed HTTPClient without the now-required apiSourceProvider
argument, breaking compilation of the unit test target after merging main.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Rick van der Linden <rick.vanderlinden@revenuecat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 08:56:01 +02:00
Rick b82223e5a1 feat(remote-config): add static fallback config endpoint (#7182)
* feat(remote-config): add static fallback config endpoint

* refactor(remote-config): clarify static fallback path name

* test(remote-config): cover static fallback signature failures

* fix(remote-config): use static fallback only without cache

* test(remote-config): cover static fallback external blobs

* refactor(remote-config): align static fallback naming

* refactor(remote-config): scope static fallback path

* fix(remote-config): mark fallback no content fresh

* fix(remote-config): align fallback accept headers

* refactor(remote-config): rename fallback endpoint types

* fix(remote-config): enable etags for fallback config

* fix(remote-config): skip fallback when proxy is configured

* fix(remote-config): rely on etag cache for fallback

* refactor(remote-config): reuse fallback retry eligibility

* fix(remote-config): disable after fallback client errors
2026-07-10 14:17:31 +02:00
Facundo Menzella de6bccae28 other(networking): remove unused workflows OperationQueue and HTTP path cases (#7145)
Follow-up to the dead-endpoint-subsystem deletion: the dedicated
workflows OperationQueue (allocated on every Backend init, capped at
4 concurrent operations) was only ever consumed by the now-deleted
WorkflowsAPI, and HTTPRequestPath's .getWorkflow/.getWorkflows cases
had no remaining production caller. Confirmed via grep before removal.
2026-07-08 13:08:08 +02:00
RickandRevenueCat Git Bot f4c06c1f4a feat(remote-config): add rc container compression support (#7107)
* feat(remote-config): add rc container compression support

* fix(remote-config): use rc element encoding header

* test(remote-config): align rc container warning logs

* Update baseline swiftinterface files (#7108)

* fix(remote-config): remove unused accept encoding header case

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2026-07-01 11:58:03 +02:00
Rick 00251c6e02 feat(remote-config): switch config request to v1 and send app user id (#7080)
* fix(remote-config): use v1 config endpoint

* feat(remote-config): include app user id in request

* feat(remote-config): move domain into config path

* fix(remote-config): always encode prefetched blobs

* chore(remote-config): remove redundant coding key comment

* fix(remote-config): update nonce path test
2026-06-29 13:25:55 +02:00
Rick 48e61c41e9 refactor(remote-config): make rc container validation domain-specific (#7074)
* feat(remote-config): make rc container validation domain-specific

* refactor(remote-config): share config element validation

* refactor(remote-config): decode remote config container in http client

* fix(remote-config): align rebased container tests

* fix(remote-config): fix remote config signature verification

* docs(remote-config): clarify rc container validation comments
2026-06-29 12:19:58 +02:00
Rick cf77437cc0 feat(remote-config): verify RC container signatures (#7046)
* feat(remote-config): use opaque manifest token models

* fix(remote-config): simplify manifest token model

* feat(remote-config): verify RC container signatures

* refactor(remote-config): isolate signature payload context

* refactor(remote-config): clarify fetch and signing context

* fix(remote-config): log signature payload failures

* fix(remote-config): correct checksum mismatch verification tests

* fix(remote-config): clarify checksum mismatch test behavior

* fix(remote-config): expect enforced checksum verification failure

* fix(remote-config): address signing review feedback

* fix(remote-config): verify no-content response context

* fix(remote-config): expect no-content verification failure code

* refactor(remote-config): group signature verified paths
2026-06-25 17:03:58 +02:00
Rick 6330937692 feat(remote-config): support RC Container format in network stack and use for remote-config request (#7037)
* feat(remote-config): request RC Container format

* fix(remote-config): handle no content responses

* chore(remote-config): allow snapshot todo

* fix(remote-config): disable response verification

* chore(remote-config): document disabled signing

* refactor(remote-config): parse RC containers in HTTP stack
2026-06-19 14:38:38 +02:00
Rosie Watson 50a1b541cb WFL-303 | Add fallback URL for workflows (#7008)
* chore: add fallback url for workflows

* test: add test

* chore: address comments about type param
2026-06-16 08:06:51 +01:00
Rick 7ee15c73a2 feat(remote-config): add network scaffolding for GET /v2/config endpoint (#6854)
* feat(remote-config): add network scaffolding for GET /v2/config endpoint

Adds RemoteConfigResponse model, GetRemoteConfigOperation, RemoteConfigAPI,
and wires everything into Backend. Unknown topic keys in the manifest are
silently dropped for forward compatibility. Mirrors Android PR #3435.

* fix(remote-config): use lowercase "default" variant key in test fixtures

Aligns with Android convention (DEFAULT_ENTRY_ID = "default") established
in purchases-android PR #3437.

* test(remote-config): add snapshot files for all CI OS versions

BackendGetRemoteConfigTests was only recording snapshots for iOS 26.
CI also runs tests on iOS 14/15/16/17/18, tvOS 18, macOS, and watchOS,
each of which requires its own snapshot file.

Derived from existing snapshot patterns in BackendGetRewardVerificationStatusTests.

* fix(remote-config): log warning when unknown topic key is received

Follows the codebase convention of logging unknown backend values
(e.g. WorkflowsResponse unknown_workflow_trigger_type) rather than
silently dropping them.

* refactor(remote-config): rename wireKey to rawValue, add WIP comment for signature verification

Aligns Topic with Swift's RawRepresentable convention and matches the
Android SDK's comment indicating signature verification will be enabled
once the remote config endpoint is finalized.

* refactor(remote-config): enable etag and simplify HTTPRequestPath cases
2026-06-02 17:19:05 +02:00
Facundo MenzellaandClaude Sonnet 4.6 a3ea621584 feat(networking): add getWorkflows list endpoint (#6853)
* feat(networking): add getWorkflows list endpoint

Port of https://github.com/RevenueCat/purchases-android/pull/3509

Adds WorkflowSummary and WorkflowsListResponse models, GetWorkflowsListOperation,
and WorkflowsAPI.getWorkflows() to support listing a subscriber's workflows via
GET /v1/subscribers/{userId}/workflows. Includes request deduplication and
background-aware delay, matching the existing getWorkflow(by id) pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add WorkflowsListCallback and GetWorkflowsListOperation to Xcode project

Register the two new source files in the root RevenueCat.xcodeproj so
xcodebuild (used by CI) can find them. SPM discovers them automatically
but the tracked root project needs explicit file references and build
file entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix snapshots: remove ?type=paywall query param from getWorkflows endpoint

The getWorkflows endpoint does not send a type filter query parameter.
The snapshots were created from an earlier design and needed to be
updated to match the actual request URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix WorkflowSummary decoding with JSONDecoder.default convertFromSnakeCase

JSONDecoder.default uses .convertFromSnakeCase, which converts JSON keys to
camelCase before matching against CodingKey raw values. The explicit CodingKeys
had snake_case raw values (e.g. "display_name"), causing a mismatch since the
decoder would convert display_name to displayName before the lookup.

Removing the explicit CodingKeys lets Swift synthesize them with camelCase raw
values, which correctly match the converted keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(networking): add type filter parameter and deserialization robustness tests

- Add optional `type: String?` parameter to `getWorkflows` path, operation,
  API method, and mock, matching Android PR #3509 which supports
  `?type=<value>` query filtering on the list endpoint
- Include `type` in the operation deduplication cache key so calls with
  different types are not coalesced
- Add `testGetWorkflowsWithTypePassesQueryParameter` with snapshots for all
  OS variants
- Add `WorkflowSummaryDecodingTests` covering explicit-null `offering_id`,
  absent `prefetch` default, and unknown-field tolerance for both
  `WorkflowSummary` and `WorkflowsListResponse`
- Remove unnecessary `import Foundation` from `WorkflowsListCallback.swift`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Address review: enable signature verification for getWorkflows, fix cache key separator and indentation

- Move .getWorkflows to supportsSignatureVerification=true, matching .getWorkflow and Android PR (#3509)
- Add "\n" separator between appUserID and type in individualizedCacheKeyPart to prevent collision
- Fix indentation of factory closure closing brace and label to match GetWorkflowOperation pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:31:12 +02:00
Pol MiroandAntonio Pallares 7d7d584634 Rename internal SSV symbols, URL and metric to RewardVerification (#6667)
* Rename internal AdMobSSV symbols, URL and metric to RewardVerification

Renames the internal AdMobSSV* types and SPI introduced in #6641 to
RewardVerification* so the wording stays adapter-agnostic. Also renames
the backend URL path and the client-side metric to match, so the SPI is
provider-agnostic end to end.

Renames:
- Sources/Ads/AdMobSSV/ -> Sources/Ads/RewardVerification/
- AdMobSSVPollStatus -> RewardVerificationPollStatus
- RewardVerificationPollStatus.validated -> .verified
- AdMobSSVStatusResponse -> RewardVerificationStatusResponse
- RewardVerificationStatusResponse.Status.validated -> .verified
  (the decoder still accepts the legacy "validated" wire value during
  the rollout window; covered by a new decoder unit test)
- AdMobSSVStatusCallback -> RewardVerificationStatusCallback
- GetAdMobSSVStatusOperation -> GetRewardVerificationStatusOperation
- AdsAPI.getAdMobSSVStatus -> getRewardVerificationStatus
- Purchases.pollAdMobSSVStatus -> pollRewardVerificationStatus
- HTTPRequest.Path.adMobSSVStatus -> .rewardVerificationStatus
- URL path: /ads/admob/ssv/{tx} -> /ads/reward_verifications/{tx}
- Metric name: get_admob_ssv_status -> get_reward_verification_status
- BackendErrorStrings.unknown_admob_ssv_status -> .unknown_reward_verification_status
- Test files, test methods and snapshot artifacts renamed to match.
- Xcode project file references updated in lockstep.

* Drop legacy `validated` wire-value compat in RewardVerification decoder

The `RewardVerificationStatusResponse` decoder was accepting both `"verified"`
and the legacy `"validated"` status from the backend as a backwards-compat
hedge during rollout. The endpoint has not been shipped and the SDK has no
public entry point that polls it yet, so there is no compatibility window to
preserve — the decoder can require `"verified"` outright.

While here, simplify the decoder to look up known cases via
`Status(rawValue:)` instead of a string-literal switch, so adding a future
case can't silently drift from the enum.

Removes the corresponding `testDecodesLegacyValidatedWireValueAsVerified`
test. Existing coverage for `verified`, `pending`, `failed`, and the
`unknown` fallback (with the `Logger.warn` assertion) is unchanged.

* Update Sources/Purchasing/Purchases/Purchases.swift

Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>

* test: assert warning is logged when decoding literal "unknown" status

Address review feedback: cover the case where the backend sends the
literal "unknown" wire value, which still falls through the
unmapped-status branch and should emit a warning.

---------

Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>
2026-04-23 13:57:32 +02:00
23d012abe2 Add workflow-based paywall resolution (#6675)
* Add workflows network layer for multipage paywalls

- Add `getWorkflows` and `getWorkflow` endpoint paths
- Add `WorkflowsListResponse` and `PublishedWorkflow` response models
- Add `WorkflowDetailProcessor` to handle `inline`/`use_cdn` response actions
- Add `WorkflowCdnFetcher` protocol and `DirectWorkflowCdnFetcher` implementation
- Add `GetWorkflowsOperation` and `GetWorkflowOperation` cacheable network operations
- Add `WorkflowsAPI` facade and wire into `Backend`
- Add unit tests for all new components

iOS equivalent of https://github.com/RevenueCat/purchases-android/pull/3300

Made-with: Cursor

* fix xcodeproj

* reset Package.resolved

* Update WorkflowStep models to match actual backend response

- Add WorkflowTrigger struct (name, type, action_id, component_id)
- Add triggers, outputs, and metadata fields to WorkflowStep
- Add metadata field to PublishedWorkflow
- Remove value field from WorkflowTriggerAction (backend uses step_id only)

Made-with: Cursor

* Update WorkflowResponseTests to match updated models

- Replace value/resolvedTargetStepId assertions with stepId
- Remove testDecodeWorkflowTriggerActionValueTakesPrecedence (value field removed)
- Add testDecodeWorkflowTrigger for new WorkflowTrigger struct
- Add testDecodePublishedWorkflowWithMetadata
- Update testDecodeWorkflowStepDefaults to cover triggers, outputs, metadata
- Add testDecodeWorkflowStepMatchingActualBackendResponse with real backend payload

Made-with: Cursor

* Disable file_length lint rule in HTTPRequestPath.swift

Made-with: Cursor

* Fix GetWorkflowOperation to compute result once before distributing to callbacks

CDN fetch and JSON decoding were running once per deduplicated callback.
Compute the Result<WorkflowFetchResult, BackendError> once outside the
performOnAllItemsAndRemoveFromCache loop, matching the pattern used by
GetOfferingsOperation and other operations in the codebase.

Made-with: Cursor

* Fix CDN fetcher: use URLSession instead of Data(contentsOf:), classify errors correctly

- Replace Data(contentsOf:) with URLSession.shared.dataTask + DispatchSemaphore
  in DirectWorkflowCdnFetcher; gets URLSession timeout, HTTP status validation,
  and proper network stack semantics
- Add WorkflowDetailProcessingError.cdnFetchFailed typed error so CDN I/O
  failures are distinguishable from envelope parsing failures
- Catch cdnFetchFailed in GetWorkflowOperation and map to NetworkError.networkError
  instead of NetworkError.decoding, fixing misleading error classification
- Update WorkflowDetailProcessorTests to assert the typed error is thrown

Made-with: Cursor

* Replace semaphore CDN fetch with async/await using withCheckedThrowingContinuation

- Make WorkflowCdnFetcher.fetchCompiledWorkflowData async throws; use
  withCheckedThrowingContinuation to bridge URLSession.dataTask into async,
  avoiding any thread-blocking
- Make WorkflowDetailProcessor.process async throws to propagate async
- Bridge into async in GetWorkflowOperation via Task {}; completion() is
  called inside the Task after CDN fetch and decoding complete
- Update WorkflowDetailProcessorTests to async throws with await

Made-with: Cursor

* Make CDN fetcher and processor completion-handler based for consistency

Avoids Task{} in GetWorkflowOperation and keeps all operations calling
completion() synchronously from within the HTTP callback, matching every
other operation in the codebase.

- WorkflowCdnFetcher.fetchCompiledWorkflowData now takes a completion handler;
  DirectWorkflowCdnFetcher uses URLSession.dataTask (non-blocking, no semaphore)
- WorkflowDetailProcessor.process now takes a completion handler; inline action
  completes synchronously, use_cdn fans out to the fetcher callback
- GetWorkflowOperation splits into getWorkflow/handleResponse/backendResult/
  distribute helpers to stay within line-length limits
- WorkflowDetailProcessorTests updated to use waitUntilValue pattern

Made-with: Cursor

* Fix ambiguous cache key delimiter in GetWorkflowOperation

Space-separated appUserID+workflowId could collide (e.g. user 'a b' + workflow 'c'
== user 'a' + workflow 'b c'). Use newline as delimiter, matching the precedent
set by GetWebBillingProductsOperation.

Made-with: Cursor

* PR comments

* remove cdn fetcher

* fix response in BackendGetWorkflowsTests.swift

* fix WorkflowResponseTests

* fix error

* [skip ci] Generating new test snapshots (#6584)

* [skip ci] Generating new test snapshots (#6585)

* [skip ci] Generating new test snapshots (#6586)

* [skip ci] Generating new test snapshots (#6587)

* [skip ci] Generating new test snapshots (#6588)

* [skip ci] Generating new test snapshots (#6589)

* [skip ci] Generating new test snapshots (#6590)

* Test CDN mock is not re-assignable per test

* [skip ci] Generating new test snapshots (#6597)

* [skip ci] Generating new test snapshots (#6598)

* [skip ci] Generating new test snapshots (#6599)

* [skip ci] Generating new test snapshots (#6600)

* [skip ci] Generating new test snapshots (#6601)

* [skip ci] Generating new test snapshots (#6602)

* [skip ci] Generating new test snapshots (#6603)

* getWorkflow is signed

* add type parameter

* add response verification for CDN response

* remove workflows list

* add value to WorkflowTriggerAction

* step id

* hash and filerepo

* missingCdnHash

* linter and project

* fix project

* revert Package.resolved

* @unchecked Sendable

* use hash in generateOrGetCachedFileURL

* fix compilation

* skip responseVerificationMode in cdn

* change basePath of caches

* Add workflow-based paywall resolution (iOS parity with Android #3350)

Implements the workflows endpoint so paywalls can be resolved from a
workflow document rather than the offerings cache, gated behind the
ENABLE_WORKFLOWS_ENDPOINT compile flag. Validates the step→screen→
offeringId chain and maps the workflow screen into Offering.PaywallComponents
using the existing V2 rendering path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add tests for workflow networking layer and WorkflowScreenMapper

BackendGetWorkflowTests covers HTTP call, request caching, per-user and
per-workflow-ID deduplication, error propagation, empty appUserID, and
full response decoding. WorkflowScreenMapperTests verifies all
PaywallComponentsData fields and the UIConfig are mapped correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* remove reserialization

* Use explicit type in GetWorkflowOperation.createFactory for greppability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Use explicit type in GetWorkflowOperation.createFactory for greppability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix lint errors and refactor workflow offering resolution into PaywallViewConfiguration

- Add swiftlint:disable comments for _revision identifier name in WorkflowsResponse.swift
- Add missing_docs disable in WorkflowsResponse.swift (all @_spi(Internal) types)
- Add file_length disable in Offering.swift
- Fix identifier name 'd' → 'doubleValue' in WorkflowDetailProcessorTests.swift
- Move workflow offering resolution from PaywallView into PaywallViewConfiguration.resolveOfferingOrThrow()
- Add cachedInitialOffering() to PaywallViewConfiguration.Content
- Add PaywallViewConfigurationTests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix swiftlint identifier_name violation in WorkflowDetailProcessorTests

Rename short variable `d` to `doubleValue` in two `if case .double(let d)`
patterns to satisfy the identifier_name rule requiring names >= 3 characters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Inject PaywallPurchasesType into offering resolution; remove dead code

- Add offerings(), cachedOfferings, and workflow() to PaywallPurchasesType
  so PaywallViewConfiguration.Content no longer calls Purchases.shared directly
- Thread purchases through cachedInitialOffering/resolveOffering/resolveOfferingOrThrow
  and all private helpers; callers pass purchaseHandler.purchasesInstance
- Expose purchasesInstance on PurchaseHandler for call sites
- Add stubs to NotConfiguredPurchases, LoadingPaywallPurchases, and MockPurchases
  (MockPurchases gets offeringsBlock/workflowBlock for test injection)
- Remove dead WorkflowDetailProcessingError.missingInlineData case
- Replace manual JSONSerialization parseEnvelope with Data.asJSONDictionary()
- Collapse double #if nesting into single compound condition
- Remove WHAT doc comment from WorkflowDetailProcessor
- Refactor resolveOffering() to delegate to resolveOfferingOrThrow()
- Parallelize workflow + offerings fetches with async let

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Move offering resolution from Content to PurchaseHandler

Resolving an offering requires a purchases instance, so it belongs on
PurchaseHandler rather than as a method on the Content enum that takes
purchases as a parameter. purchasesInstance accessor removed.

Also fix MockPurchases.map() to forward offeringsBlock, workflowBlock,
and cachedOfferings to the mapped instance, and simplify
PaywallViewConfigurationTests to use MockPurchases directly instead of
configuring a real Purchases singleton.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add clarifying comment to workflow(forOfferingIdentifier:) explaining parameter naming

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix CI failures: remove duplicate switch cases and add @_spi(Internal) to test imports

- Remove duplicate .getWorkflow cases in HTTPRequestPath.swift switches
  (auto-merge artifact that caused duplicate_conditions lint violations)
- Add @_spi(Internal) to @testable imports in three unit test files so they
  can access WorkflowFetchResult, PublishedWorkflow, and WorkflowStep which
  are now @_spi(Internal) public

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix @_spi access in WorkflowDetailProcessorTests

WorkflowResponseTests already had @_spi(Internal) @testable import RevenueCat;
WorkflowDetailProcessorTests was missing the @_spi annotation, causing
'id', 'steps', and 'initialStepId' to be inaccessible on PublishedWorkflow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix MockWorkflowsAPI.swift missing from StoreKitUnitTests and UnitTests targets

The PBXBuildFile entries for DB7EA77F2F9764F700BCC082 and DB7EA7802F9764F700BCC082
were referenced in the StoreKitUnitTests and UnitTests source phases but never
defined, causing MockWorkflowsAPI to be missing at compile time and
MockBackend.swift to fail with 'cannot find MockWorkflowsAPI in scope'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add offering_identifier field to WorkflowScreen for parity with Android

Android PR #3350 split offering_id (internal ID) from offering_identifier
(human-readable identifier used for lookup). iOS was only decoding
offering_id and using it for the offering lookup, which would fail if the
workflow response sends the public identifier in offering_identifier.

- Add offeringIdentifier (from offering_identifier) to WorkflowScreen
- Update PurchaseHandler to prefer offeringIdentifier, falling back to offeringId
- Fix typo: workflow.screelns -> workflow.screens
- Add decode tests for both offering fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix WorkflowResponseTests: add missing padding field to stack component fixtures

Stack component decoding requires a padding field; test JSON fixtures were
missing it causing keyNotFound decode errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix WorkflowResponseTests: add missing margin field to stack component fixtures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Replace ENABLE_WORKFLOWS_ENDPOINT compile flag with runtime argument check

The -EnableWorkflowsEndpoint launch argument replaces the compile-time flag,
allowing workflow resolution to be toggled without rebuilding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add Workflow presentation mode to PaywallsTester

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Remove offeringId shim, allow no-paywall offerings to open as workflows

- Drop WorkflowScreen.offeringId / offering_id CodingKey (Android already
  uses offering_identifier exclusively — aligns both platforms)
- Simplify PurchaseHandler to use screen.offeringIdentifier directly
- No-paywall offerings in PaywallsTester now render as tappable rows
  (workflow mode) instead of plain read-only text, with a context menu
  restricted to the Workflow option

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Rename WorkflowFetchResult to WorkflowDataResult, extract ProcessInfo extension

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix project

---------

Co-authored-by: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 10:01:14 +00:00
Pol MiroandRevenueCat Git Bot abad547968 AdMob SSV: add @_spi(Internal) poll endpoint on Purchases (#6641)
* Add AdMob SSV poll endpoint and `pollAdMobSSVStatus` SPI

* Trim verbose AdMob SSV transport comments.

Align AdsAPI and cache-key inline documentation with existing repository comment density while preserving behavior.

* Trim pollAdMobSSVStatus SPI documentation.

Reduce prescriptive guidance in the internal API comment while keeping the key cancellation behavior note.

* [skip ci] Generating new test snapshots (#6648)

* [skip ci] Generating new test snapshots (#6647)

* [skip ci] Generating new test snapshots (#6646)

* [skip ci] Generating new test snapshots (#6645)

* [skip ci] Generating new test snapshots (#6644)

* [skip ci] Generating new test snapshots (#6642)

* [skip ci] Generating new test snapshots (#6643)

* Trigger CI for PR 6641 checks.

Add a non-skip commit so required CircleCI statuses are reported on the current head.

* Validate non-empty client transaction ID for AdMob SSV polling.

Guard against empty client transaction IDs in the SPI and backend operation and add focused backend tests for the new error path.

* [skip ci] Generating new test snapshots (#6652)

* [skip ci] Generating new test snapshots (#6653)

* Trigger CI for PR 6641 checks.

Add a non-skip commit so required CircleCI statuses are reported on the current head.

* Enable signature verification for AdMob SSV status path.

Update HTTP request path tests to cover AdMob SSV signature and nonce behavior.

* fix: address review — move AdMob SPI extension to dedicated file

* fix: address review — fail fast when AdMob status mock is unstubbed

* fix: address review — dedupe client transaction validation in backend operation

* fix: address review — use enum for AdMob SSV poll status

* fix: address review — allow extracted Purchases extensions to access backend

* fix: address review — log unknown AdMob SSV statuses

* fix: address review — add Purchases AdMob SSV mapping tests

* fix: address review — map missing client transaction ID to configuration error

* fix: address review — keep backend private by co-locating AdMob extension

* fix: address review — remove unused raw-value conformance

* fix: address review — assert warning log for unknown AdMob status

* fix: address review — surface unknown AdMob SSV status to adapters

* fix: address review — enable eTag support for AdMob SSV status path

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2026-04-22 15:55:13 +02:00
0b65086ec2 Add workflows network layer for multipage paywalls (#6557)
* Add workflows network layer for multipage paywalls

- Add `getWorkflows` and `getWorkflow` endpoint paths
- Add `WorkflowsListResponse` and `PublishedWorkflow` response models
- Add `WorkflowDetailProcessor` to handle `inline`/`use_cdn` response actions
- Add `WorkflowCdnFetcher` protocol and `DirectWorkflowCdnFetcher` implementation
- Add `GetWorkflowsOperation` and `GetWorkflowOperation` cacheable network operations
- Add `WorkflowsAPI` facade and wire into `Backend`
- Add unit tests for all new components

iOS equivalent of https://github.com/RevenueCat/purchases-android/pull/3300

Made-with: Cursor

* fix xcodeproj

* reset Package.resolved

* Update WorkflowStep models to match actual backend response

- Add WorkflowTrigger struct (name, type, action_id, component_id)
- Add triggers, outputs, and metadata fields to WorkflowStep
- Add metadata field to PublishedWorkflow
- Remove value field from WorkflowTriggerAction (backend uses step_id only)

Made-with: Cursor

* Update WorkflowResponseTests to match updated models

- Replace value/resolvedTargetStepId assertions with stepId
- Remove testDecodeWorkflowTriggerActionValueTakesPrecedence (value field removed)
- Add testDecodeWorkflowTrigger for new WorkflowTrigger struct
- Add testDecodePublishedWorkflowWithMetadata
- Update testDecodeWorkflowStepDefaults to cover triggers, outputs, metadata
- Add testDecodeWorkflowStepMatchingActualBackendResponse with real backend payload

Made-with: Cursor

* Disable file_length lint rule in HTTPRequestPath.swift

Made-with: Cursor

* Fix GetWorkflowOperation to compute result once before distributing to callbacks

CDN fetch and JSON decoding were running once per deduplicated callback.
Compute the Result<WorkflowFetchResult, BackendError> once outside the
performOnAllItemsAndRemoveFromCache loop, matching the pattern used by
GetOfferingsOperation and other operations in the codebase.

Made-with: Cursor

* Fix CDN fetcher: use URLSession instead of Data(contentsOf:), classify errors correctly

- Replace Data(contentsOf:) with URLSession.shared.dataTask + DispatchSemaphore
  in DirectWorkflowCdnFetcher; gets URLSession timeout, HTTP status validation,
  and proper network stack semantics
- Add WorkflowDetailProcessingError.cdnFetchFailed typed error so CDN I/O
  failures are distinguishable from envelope parsing failures
- Catch cdnFetchFailed in GetWorkflowOperation and map to NetworkError.networkError
  instead of NetworkError.decoding, fixing misleading error classification
- Update WorkflowDetailProcessorTests to assert the typed error is thrown

Made-with: Cursor

* Replace semaphore CDN fetch with async/await using withCheckedThrowingContinuation

- Make WorkflowCdnFetcher.fetchCompiledWorkflowData async throws; use
  withCheckedThrowingContinuation to bridge URLSession.dataTask into async,
  avoiding any thread-blocking
- Make WorkflowDetailProcessor.process async throws to propagate async
- Bridge into async in GetWorkflowOperation via Task {}; completion() is
  called inside the Task after CDN fetch and decoding complete
- Update WorkflowDetailProcessorTests to async throws with await

Made-with: Cursor

* Make CDN fetcher and processor completion-handler based for consistency

Avoids Task{} in GetWorkflowOperation and keeps all operations calling
completion() synchronously from within the HTTP callback, matching every
other operation in the codebase.

- WorkflowCdnFetcher.fetchCompiledWorkflowData now takes a completion handler;
  DirectWorkflowCdnFetcher uses URLSession.dataTask (non-blocking, no semaphore)
- WorkflowDetailProcessor.process now takes a completion handler; inline action
  completes synchronously, use_cdn fans out to the fetcher callback
- GetWorkflowOperation splits into getWorkflow/handleResponse/backendResult/
  distribute helpers to stay within line-length limits
- WorkflowDetailProcessorTests updated to use waitUntilValue pattern

Made-with: Cursor

* Fix ambiguous cache key delimiter in GetWorkflowOperation

Space-separated appUserID+workflowId could collide (e.g. user 'a b' + workflow 'c'
== user 'a' + workflow 'b c'). Use newline as delimiter, matching the precedent
set by GetWebBillingProductsOperation.

Made-with: Cursor

* PR comments

* remove cdn fetcher

* fix response in BackendGetWorkflowsTests.swift

* fix WorkflowResponseTests

* fix error

* [skip ci] Generating new test snapshots (#6584)

* [skip ci] Generating new test snapshots (#6585)

* [skip ci] Generating new test snapshots (#6586)

* [skip ci] Generating new test snapshots (#6587)

* [skip ci] Generating new test snapshots (#6588)

* [skip ci] Generating new test snapshots (#6589)

* [skip ci] Generating new test snapshots (#6590)

* Test CDN mock is not re-assignable per test

* [skip ci] Generating new test snapshots (#6597)

* [skip ci] Generating new test snapshots (#6598)

* [skip ci] Generating new test snapshots (#6599)

* [skip ci] Generating new test snapshots (#6600)

* [skip ci] Generating new test snapshots (#6601)

* [skip ci] Generating new test snapshots (#6602)

* [skip ci] Generating new test snapshots (#6603)

* getWorkflow is signed

* add type parameter

* add response verification for CDN response

* remove workflows list

* add value to WorkflowTriggerAction

* step id

* hash and filerepo

* missingCdnHash

* linter and project

* fix project

* revert Package.resolved

* @unchecked Sendable

* use hash in generateOrGetCachedFileURL

* fix compilation

* skip responseVerificationMode in cdn

* change basePath of caches

* remove reserialization

* Use explicit type in GetWorkflowOperation.createFactory for greppability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix swiftlint identifier_name violation in WorkflowDetailProcessorTests

Rename short variable `d` to `doubleValue` in two `if case .double(let d)`
patterns to satisfy the identifier_name rule requiring names >= 3 characters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* remove default FileRepositoryType

* fix project

* fix test

* test preserves camel case

* Generating new test snapshots for `feat/workflows-network-layer` - ios-15 (#6654)

* Generating new test snapshots for `feat/workflows-network-layer` - ios-14 (#6656)

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Facundo Menzella <facumenzella@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Facundo Menzella <facumenzella@users.noreply.github.com>
2026-04-21 12:32:01 +02:00
Will Taylor 9356db1d42 [CEC Mode]: introduce isPurchaseAllowedByRestoreBehavior() (#6192) 2026-02-06 11:54:42 -06:00
Rick b49a054843 Add client side timeout logic for endpoints that support fallback URLs (#5760)
* Implemented initial HTTPRequestTimeoutManager + added first couple of tests

* Cleaned up the code and added a bunch more unit tests based on the Android test suite

* Updated doc format

* Add new files to workspace

* Added HTTPClient tests testing interaction with timeout manager

* Swiftlint fixes

* Updated comment

* Add missing file to workspace

* Fix after rebase

* Added extra HTTPClient <-> HTTPRequestTimeoutManager tests asserting on the recorded events

* Implemented PR feedback

* Fixed compile issue

* Fix compile issue

* Implemented PR feedback

* Removed deleted file reference from workspace

* Fixed test after changing reset timeout

* Make sure the default HTTPRequistTimeoutManager is using the default value from the HTTPClient

* Add a sophisticated test testing the full timeout flow against an unroutable IP for a real world scenario

* Updated timeout case name

* Fixed case where .timeoutOnMainBackendForFallbackSupportedEndpoint was also recorded for non timeouts and added tests to prevent regression

* Fixed only stubbing the HTTP response based on path, since that could be the same across hosts, resulting in flaky tests
2025-11-26 11:16:04 +01:00
Rosie Watson 8b229a2adc CC-582 | Allow for support ticket creation (#5779)
* feat: add contact support button and new sheet for ticket creation

* chore: use localisation

* feat: send support ticket details to backend

* feat: only show the support ticket creation button when config is set up

* feat: only show to correct customer type

* test: add base tests

* chore: remove unrequired var

* chore: use translations

* docs: update link to tuist

* refactor: move to cc api

* refactor: api calls

* test: update setup

* chore: better email validation

* chore: improved focus state

* chore: add dismiss

* chore: hide keyboard on dismiss

* chore: fix indentation

* feat: add charcter count

* chore: add rest of cc config

* test: clean up unit tests

* refactor: pull email validation out into struct

* chore: refine email checks

* refactor: pull out max characters into own var
2025-11-19 08:26:43 +00:00
Antonio Pallares eaee0c3592 Fix signature verification fallback urls (#5756)
* Fix signature verification for fallback URLs

* Add tests for signing with fallback path

* Lint

* Add log
2025-11-06 22:43:27 +01:00
Rick 72cf38a0fe Added an internal API for overriding the API base URL (#5739)
* Added an internal API for overriding the API base URL

* Updated and added tests for API baseURL configuration option

* Revert change that removed serverHostURL from HTTPRequestPath since it broke other types implementing this protocol

* swiftlint

* Removed the apiBaseURL property from Purchases since it's only used for internal testing anyways

* Remove unused @_spi(Internal)

* Updated comment on api base URL vs proxy URL

* Fixed linting issue
2025-11-04 13:35:20 +01:00
Pol Miro 2658f2b8fa Refactor: Rename PaywallEventsManager to EventsManager and separate ad events (#5718)
* Rename paywallsEventsManager to eventsManager

* Rename PaywallEventsManagerType to EventsManagerType

* Rename PaywallEventsManager to EventsManager

* Rename PaywallEventStore to FeatureEventStore

* Rename InternalAPI.postPaywallEvents to postFeatureEvents

* Rename StoredEventSerializer to StoredFeatureEventSerializer

* Rename createStoredEvent to createFeatureStoredEvent

* Rename StoredEvent to StoredFeatureEvent

* Rename HTTPRequest.PaywallPath to HTTPRequest.FeatureEventsPath

* Rearrange events files

* Rename event related files

* Extract shared events endpoint configuration to EventsHTTPRequestPath

Both FeatureEventsPath and AdPath share the same /v1/events endpoint
configuration, differing only in domain (api-paywalls.revenuecat.com vs
a.revenue.cat) and name. Extract common logic to EventsHTTPRequestPath
protocol in Sources/Events/Networking.

* Move Sources/FeatureEvents to Sources/Events/FeatureEvents

* Rename EventsRequest to FeatureEventsRequest

- Rename EventsRequest.swift to FeatureEventsRequest.swift
- Move file to Sources/Events/FeatureEvents/Networking/
- Update all references in source and test files
- Update Xcode project file references

This makes it clear that this request type is specifically for the
feature events endpoint, not a generic events request.

* Remove ad events from feature events system

Ad events should not be sent through the feature events system as they
require their own separate infrastructure with a different API endpoint.

Changes:
- Remove FeatureEvent conformance from AdEvent
- Remove .ads case from Feature enum
- Remove ad events handling from FeatureEventsRequest
- Add no-op track(adEvent:) method to EventsManager with comment
- Update AdTracker to call the new track(adEvent:) method

Ad events tracking now becomes a no-op until proper implementation with:
- StoredAdEvent structure
- AdEventStore for persistence
- InternalAPI.postAdEvents() using HTTPRequest.AdPath.postEvents

* Rename PostEventsOperation to PostFeatureEventsOperation

- Rename PostEventsOperation.swift to PostFeatureEventsOperation.swift
- Move file to Sources/Events/FeatureEvents/Networking/
- Update class name and remove misleading "generic" comment
- Update references in InternalAPI.swift
- Update Xcode project file references

This makes it clear that this operation is specifically for posting
feature events, not a generic operation for various event types.
2025-10-30 14:52:35 +01:00
Antonio Pallares 3ace587482 [SDK-4153] Use static fallback urls (#5709)
* change fallback URL logic to support a different path from the main server

* Adapt unit tests to changes in fallback urls logic

* lint

* Add unit test for fallback urls

* fix switched logic
2025-10-29 18:43:19 +01:00
Pol MiroandClaude 470ce93f4f Add non paid revenue reporting infra (#5640)
* Add RC Ads event tracking infrastructure

Implements event tracking for RevenueCat Ads following the Paywalls Events pattern.

- Adds three event types: ad displayed, ad opened, and ad revenue
- Uses actor-based concurrency with file-based event storage
- Posts events to e.revenue.cat/v1/events endpoint
- Marks track(adEvent:) method as @_spi(Internal) for internal use
- Includes comprehensive unit tests with snapshot testing
- Available on iOS 15.0+, tvOS 15.0+, macOS 12.0+, watchOS 8.0+

Event schema follows khepri RC Ads input events specification with
app_session_id field matching backend expectations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Avoid mapping in EventsRequest+Ad

* Consolidate ad events into PaywallEventsManager

Eliminates duplication between AdEventsManager and PaywallEventsManager by:
- Making PaywallEventsManager handle all event types (ads, paywalls, customerCenter)
- Adding feature-aware routing in flushEvents() to send events to correct domains
  - Ad events → https://e.revenue.cat/v1/events
  - Other events → https://api-paywalls.revenuecat.com/v1/events
- Removing AdEventsManager, AdEventStore, and their tests (~500 lines)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove AdsStrings and use PaywallsStrings instead

AdsStrings only had 2 strings in use, both of which already exist in PaywallsStrings. Consolidates logging strings to eliminate duplication.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Namespace Precision enum inside AdEvent.RevenueData

Moves Precision from a top-level public enum to AdEvent.RevenueData.Precision to avoid polluting the global namespace and reduce autocomplete noise.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Move Precision conformances to end of file

Follows the pattern of other types by declaring protocol conformances at the end of the file rather than inline with the type definition.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove redundant sessionIdentifier from AdImpressionData

The sessionIdentifier field in AdImpressionData was redundant since
StoredEvent already tracks appSessionID. This change:

- Removes sessionIdentifier from AdImpressionData struct
- Uses storedEvent.appSessionID directly when building requests
- Renames sessionId to appSessionId to match backend field app_session_id
- Updates documentation examples to remove sessionIdentifier parameter
- Adds guard statement for optional appSessionID
- Updates all test fixtures and regenerates snapshots
- Removes iOS26 snapshot references from project

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove unneeded SessionId

* Add proper assertions to PurchasesAdEventsTests

Enhanced test coverage for public ad tracking APIs:
- Verify events are tracked via mockPaywallEventsManager
- Assert correct event types (displayed, opened, revenue)
- Validate all impression data fields
- Verify revenue-specific fields (revenueMicros, currency, precision)

All 3 tests now pass with proper verification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add ENABLE_AD_EVENTS_TRACKING compiler flag

* Simplify track ad events APIs

* Fix ENABLE_AD_EVENTS_TRACKING scope and refactor Precision to struct

Compiler flag fixes:
- Move ENABLE_AD_EVENTS_TRACKING #endif to wrap all ad event code including internal enum
- Add compiler flag guards to all three test files

Precision API refactor:
- Change AdRevenue.Precision from enum to struct following Apple's API patterns (like Message.Reason)
- Use internal init(rawValue:) to prevent custom precision values
- Convert enum cases to static properties maintaining same public API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Refactor mediatorName from String to MediatorName struct

Replace string-based mediator names with a type-safe struct following Apple's API patterns:
- Add MediatorName struct with public init(rawValue:) to support custom values
- Include predefined static properties for .adMob and .appLovin
- Update AdEventData protocol to require MediatorName type
- Update all event types (AdDisplayed, AdOpened, AdRevenue) to use MediatorName
- Use .rawValue when serializing to network requests
- Update all tests and documentation examples to use .appLovin

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove RawRepresentable conformance from Precision and update test snapshots

- Remove RawRepresentable conformance from AdRevenue.Precision to allow internal init
- Update test assertions to use .appLovin instead of string comparison
- Update all snapshot test files to use "AppLovin" instead of "MAX"
- All ad event tests now passing (11/11)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix PaywallDataDecodingTests locale check for region overrides

The test was failing when the system locale included region overrides
(e.g., "en_US@rg=eszzzz" instead of just "en_US").

Changes:
- Use beginWith("en_US") instead of exact equality check
- Compare against enConfig directly instead of fetching via Locale.current

This makes the test more robust while maintaining the same behavior validation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Rename timestampMs to timestamp but keep serialized timestamp_ms

* Use different serverHostURL

* Simplify and paralelize postEvents

* Adjust usage of ENABLE_AD_EVENTS_TRACKING flag

* Move track ad event methods to AdTracker

* Remove PaywallEventsManager implementation

* Improving naming overlapping naming AdEvent->AdEventRequest

* Simplify compiler flag usage

* Use Experimental

* Remove duplicate with PostPaywallEventsOperation and PostAdEventsOperation

* Remove postAdEvents methods assuming reuse of event storage

* Make Precision rawValue internal

* Remove runtime checks

* Trigger build

* Move Public Types MARK comment after internal protocol

* Add documentation for AdDisplayed, AdOpened, and AdRevenue properties

* Remove redundant smoke tests

The "DoesNotCrash" tests were completely redundant with the
comprehensive "StoresEvent" tests. The comprehensive tests already:
1. Verify methods don't crash (implicitly)
2. Verify events are stored
3. Verify event data is correct

Removed 3 redundant tests, keeping only the 3 comprehensive tests.

* Fix bug not tracking initialized events

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-29 11:56:48 +01:00
Antonio Pallares 2bd289a993 Add GetWebBillingProductsOperation (#5419)
* Add validation logic for Test Store API keys under the TEST_STORE compiler flag

* add unit test for test store api key validation

* Move API key validation to `configure` and store validation result in `SystemInfo`

* start new flow for purchase of `TestStoreProduct`s

* WIP: implement alert for purchase of a Test Store product

* Add file to Xcode workspace

* WIP: implement fake purchase alert in all platforms and OSs

* Enforce only one test purchases at once. Use constants for test purchase alert strings

* Place new test store purchase APIs under the TEST_STORE compiler flag

* fix lint

* fix unit tests after change of internal APIs

* PR comments

* temporarily return hardcoded `CustomerInfo` on confirmed test purchase

* tests: adapt unit tests to new `TestStorePurchaseHandler` dependency of `PurchasesOrchestrator`

* fix lint

* Fix backend integration tests compilation

* Remove deprecation warning

* remove unnecessary OSs checks

Since the minimum supported versions of the SDK are iOS 13 and tvOS 13

* add `GetWebProductsOperation`

* avoid duplicating decoding objects from WebOfferingProductsResponse and WebProductsResponse

* add missing tests snapshots

* Add `WebProductsResponse.ProductType` enum

* Fix tests

* add web products decoding tests

* Rename WebProducts to WebBillingProducts

* (more) rename WebProducts to WebBillingProducts

* (even more) rename WebProducts to WebBillingProducts

* Move `getWebBillingProducts` outside of `OfferingsAPI` into a new `WebBillingAPI` object

* improve test

* remove unnecessary `sorted()` of `Set`

* fix lint

* fix tests

Since the GetWebBilling request path contains an unordered set of productIds, which the tests do not support, we're only doing snapshot tests with 1 product ID

* fix more tests
2025-07-29 18:31:15 +02:00
Antonio Pallares b7c98a0783 Rename internal WebProducts APIs to WebOfferingProducts (#5416)
* Rename existing WebProducts to WebOfferingProducts

* fix lint

* fix wrong file reference
2025-07-23 13:56:20 +02:00
Will Taylor 01a352d9e9 Virtual Currency Support (#5108) 2025-07-07 12:11:27 -05:00
Pol Piella Abadia 39ad5519fd [DX-457] Re-apply Health SDK logging on app launch (#5360)
* Revert "Temporary revert of automatic health reporting in the SDK (#5353)"

This reverts commit dd7086fd98.

* Do not run health check when running integration tests
2025-07-07 17:00:54 +01:00
Pol Piella Abadia dd7086fd98 Temporary revert of automatic health reporting in the SDK (#5353)
* Revert "fix `Purchases` temporary leak when running SDK health check (#5350)"

This reverts commit 26de54308c.

* Revert "[DX-457] Log the SDK configuration report on every `#DEBUG` run (#5317)"

This reverts commit 21f226de00.
2025-07-03 13:22:47 +01:00
Pol Piella Abadia 21f226de00 [DX-457] Log the SDK configuration report on every #DEBUG run (#5317)
* Creates a brand new SDK Manager to reuse logic across different classes

* Updates to use the new manager

* Adds a new method to log the health report to the console only for debug builds

* Run health check for debug builds

* Fixes sendable warnings that got introduced with new manager

* Fixes swiftlint violations

* Adds unit tests for the new SDK Health manager

* Creates a new authorization provider

* Injects the new authorization provider

* Tidies and cleans up unit tests

* Adds a new configuration value

* Updates configuration unit tests

* Makes sure that values are only set on DEBUG configuration

* Reverts state of default value

* Updates wording of output message

* Fixes the wrong wording of error message

* Logs the word RevenueCat along with report title

* Fixes wording issues in error messages

* Addresses PR comments

* Adds extra checks

* Renames configuration value

* Removes unnecessary `Task`

* Renames parameter

* Calls the new endpoint in the BE to check if the app is available or not

* Do not log or perform the report operations if it is not available

* Missing xcodeproj changes

* Adds unit test coverage

* Turns validation on by default

* Creates a new path for the report availability endpoint

* We need to bypass auth but make sure that requests are still signed

* Updates the name of the response field

* Updates unit tests target with new response structure

* Handles no credentials case more gracefully

* Adds new unit tests for the new No ASC credentials warning

* Updates the file's header comment

* Adds documentation

* Wraps calls in necessary compiler directives

* Hide all report-related content behind compiler directives

* Makes more health-related functionality DEBUG only

* Addresses PR comments

* Updates all unit test build failures

* Gets it back to compile on `CUSTOM_ENTITLEMENT_COMPUTATION`

* Removes configuration value

* Updates logging to only include issues

* Adds a mock method for the health report availability request
2025-07-02 14:00:48 +01:00
Toni RicoandRevenueCat Git Bot 46f2cd8c29 Change to use new endpoint to fetch web product info (#5135)
* Change to use new endpoint to fetch web product info

* [skip ci] Generating new test snapshots (#5142)

* [skip ci] Generating new test snapshots (#5141)

* [skip ci] Generating new test snapshots (#5140)

* [skip ci] Generating new test snapshots (#5139)

* [skip ci] Generating new test snapshots (#5138)

* [skip ci] Generating new test snapshots (#5137)

* [skip ci] Generating new test snapshots (#5136)

* Add small utility function to know if offerings has any web biling offering available

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2025-05-21 13:37:56 +02:00
Toni Rico c7edb46f31 Add endpoint for fetching web billing products (#5055)
* Add support for fetching web billing products

* Some fixes

* Escape product ids

* Fixes from review

* Support etags

* Change to Int64

* Add method to MockOfferingsAPI
2025-05-06 23:28:59 +02:00
Antonio Pallares e991e0708a Use fallback API hosts when receiving server down response (#4970)
* feat: implement fallback host in all URL requests

* cleanup fallback host API implementation

* Adapt existing tests to changes in `HTTPRequestPath`

* fix implementation

* add unit tests for fallback API host

* add extra unit test

* fix tests

* fix lint

* change the logic from fallback hosts to fallback paths

* fix test

* fix fallback endpoint for get product entitlement mapping

* fix backend integration tests not building after changes

* rewrite fallback host functionality knowing that only some specific requests support a fallback host

For those requests that support a fallback host, the path, headers and response structure remain the same

* fix text name
2025-04-25 12:00:13 +00:00
47f40f5abe [DX-345] Update the testSDKHealth to use the new /health_report endpoint (#4979)
* Initial app creation

* Initial screen

* Installs and configures the SDK

* Creates a full Swift 6 compliant ViewModel

* Adds a demo customer center button

* Fixes icon name

* Updates a bit of the logic to fetch offerings

* Updates initial layout by showing offerings

* Makes more progress with a VERY DRAFT and VERY WIP screen

* Creates a new network path and requests

* Creates a new health API and adds it to the BE class

* Creates a `checkOfferings` method

* Pushes local changes

* Moved viewmodels to separate files

* Prepared some basic UI structure

* Initial shell of the new method for testing

* Initial implementation of `HealthReportOperation` from the SDK

* Passes `appUserId` through to the internal API

* Adds a new path called `appUserID`

* Removes 'old' `appHealth` endpoint

* Creates a new model for the network response

* Report should be the response type

* Updates backend request

* Updates types and documentation for the new internal request

* Moves validation into its own file

* Replaces method for DebugViewModel

* Updates error messages

* Updates the `Status` section

* Adds logic to show failure reason

* Removes Sample App from PR

* Resets `Package.resolved`

* Availability checks

* Check if device is authorized to make payments

* Fixes all SwiftLint errors

* Updates `ProductStatus` to not have a raw value of String

* Minor style fixes

* Removes warning

* Adds more availability checks

* Fixes comments

Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>

* Simplifies logic

* Updates comment

* Updates the comments to denote that this method is not intended for production use

* Removes android-only invalid SDK version error

* Adds an action button that the user can use to open the dashboard

* Updates copy about non-actionable item

* Prefers `Your RevenueCat Configuration` to `The Dashboard`

* Removes `Sendable`-related warnings

* Fixes unit tests on CI/CD

* Fixes tests

* Adds unit test for offering configuration error

* Adds unit test for no packages in default offering

* Adds unit test for generic offering configuration

* Removes periods from the end of the error messages

* Adds unit tests for all new errors

* Fixes linter errors

* Wrap in a `DEBUG` compiler directive

* Includes all products in the healthy check status to be used in the Sample App

* Updates report to include product and offering information

* Moves logic over to the report rather than being exclusive to the Debug View

* Fixes CI/CD failures

* Updates the asset for the health check status

---------

Co-authored-by: Hidde van der Ploeg <hiddevanderploeg@me.com>
Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>
2025-04-24 13:46:21 +00:00
Facundo Menzella 6b5c430082 chore: Enable force_unwrapping for SwiftLint (#4820)
* enable force_unwrapping

* avoid force unwrap

* fixed all errors

* remove extra disable
2025-02-26 12:40:49 +01:00
98e4a33225 Support anonymous web purchase redemptions (#4439)
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: andy boedo <andresboedo@gmail.com>
Co-authored-by: Cesar de la Vega <cesarvegaro@gmail.com>
2024-11-25 16:14:32 +01:00
dd0ee262ea [Customer Center] Initial implementation (#3967)
Alpha of the Customer Center. In this branch, it's been disabled behind
a compilation flag.

---------

Co-authored-by: Will Taylor <wtaylor151@gmail.com>
Co-authored-by: James Borthwick <109382862+jamesrb1@users.noreply.github.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: JayShortway <29483617+JayShortway@users.noreply.github.com>
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
Co-authored-by: Toni Rico <antonio.rico.diez@revenuecat.com>
Co-authored-by: Cody Kerns <44073103+codykerns@users.noreply.github.com>
Co-authored-by: Andy Boedo <andresboedo@gmail.com>
2024-08-09 15:26:55 +02:00
Cesar de la Vega a956dbec71 Add name property to HTTPRequestPath (#3790)
This will be used for diagnostics
2024-04-10 13:06:05 +02:00
Cesar de la Vega 03d93bb061 Add HTTPRequest.DiagnosticsPath (#3791)
Create DiagnosticsHTTPRequestPath for Diagnostics

Depends on #3790 which adds the name
2024-04-10 13:00:48 +02:00
NachoSoto 5e968ad268 Paywalls: implemented PostPaywallEventsOperation (#3158)
This adds the operation + `PaywallEventsRequest` and associated tests.
Also added method to `InternalAPI` for posting events.
2023-09-14 18:28:11 -07:00