Commit Graph
90 Commits
Author SHA1 Message Date
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
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
c23c3cdf40 UI events for paywall component interactions (#6523)
* Initial setup for paywall control interaction events for v2 paywalls (PWENG-9)

* Use tabs container name for component_name and tab name for component_value (PWENG-9)

* Use tabs container name for tab and toggle control interaction (PWENG-9)

* Introduce paywall control interaction events for legacy paywalls (PWENG-9)

* Align documented code (PWENG-9)

* Fix tvOS 13 build by replacing TapGesture with onTapGesture in FooterView (PWENG-9)

* Fix RevenueCatUITests build by inlining JSON in ToPresentedOverridesTests (PWENG-9)

* Nest paywall control interaction type as PaywallEvent.ControlType (PWENG-9)

* Add componentURL to paywall control interaction events (PWENG-9)

* Add text control type for paywall markdown link analytics (PWENG-9)

* Only track carousel interactions for user drags (PWENG-9)

* Open footer links via OpenURLAction with Link on macOS (PWENG-9)

* Updated documentation for the tab control type (PWENG-9)

* Omit nil name/transition when encoding ButtonComponent (PWENG-9)

* Fix lint errors (PWENG-9)

* Track footer restore tap before restore interceptor (PWENG-9)

* Add CodingKeys for componentUrl in control interaction events (PWENG-9)

* Revert "Add CodingKeys for componentUrl in control interaction events (PWENG-9)"

This reverts commit 57a7525cca.

* Update the control type as a stand-alone enum (PWENG-9)

* Remove trivial unit tests (PWENG-9)

* Update baseline swiftinterface files for `monika/UI-events/paywall-control-interaction` (#6528)

* Remove trivial unit tests (PWENG-9)

* [skip ci] Update baseline swiftinterface files

---------

Co-authored-by: Monika Mateska <monika.mateska@revenuecat.com>
Co-authored-by: Monika Mateska <45938914+MonikaMateska@users.noreply.github.com>

* Use a fitting parameter name (PWENG-9)

* Track navigation metadata for control interactions

Add navigation metadata (originIndex, destinationIndex, originContextName, destinationContextName, defaultIndex) to paywall control interactions and propagate them through the codebase. Updated PurchaseHandler.trackControlInteraction signature and PaywallEvent.ControlInteractionData to include the new fields and include them in event serialization (FeatureEventsRequest, FeatureEvent map). Updated UI components to populate navigation info: Carousel now reports origin/destination/default page indices and page context names; Tabs now carries tab names/context mapping, initial/default index and reports origin/destination info on selection. Paywall component models now include optional `name` for StackComponent and Tab. Tests and swiftinterface API files were updated accordingly.

* Add PaywallEventTracker and control logger

Introduce PaywallEventTracker to centralize paywall event tracking and provide a lightweight ControlInteractionLogger environment key for views. Refactor PurchaseHandler to delegate event logic to PaywallEventTracker and wire the controlInteractionLogger into paywall views and components (replacing direct PurchaseHandler calls). Update event discriminator from "paywall_control_interaction" to "paywall_component_interacted" and adjust related networking and event mapping. Add unit/UI tests for PaywallEventTracker and ControlInteractionLogger and remove redundant control-interaction tests from PurchaseHandlerTests. Includes background event dispatcher wiring and minor view adjustments to use the new logger.

* Rename control interaction to component interaction

Replace "control"-centric interaction names with "component"-centric ones across the paywall UI and event pipeline. This updates types, methods, environment keys and loggers (e.g. ControlInteraction -> ComponentInteraction), event payload fields and networking mapping, and updates all unit/UITests to match. This standardizes terminology for component interactions (tabs, carousels, buttons, text links) and adjusts the event tracking surface accordingly — note this is a breaking rename for any external API/consumers referencing the old identifiers.

* Rename ControlInteraction to ComponentInteraction

Rename PaywallEvent control interaction API: replace case `controlInteraction` with `componentInteraction`, `ControlInteractionData` with `ComponentInteractionData`, and the `controlInteractionData` property with `componentInteractionData` across platform swiftinterface files (iOS, macOS, tvOS, visionOS, watchOS and their simulators). Updates equality, codable/sendable conformance and encoder/decoder signatures to match the new type name for consistency and clearer naming.

* Use shared PaywallEventTracker instance

Add a private PaywallEventTracker property to PaywallView and PaywallsV2View and pass its componentInteractionLogger into the environment instead of creating a new PaywallEventTracker inline. This ensures a single tracker instance is used for component interaction logging within each view, avoiding ephemeral tracker creation and keeping tracking state consistent.

* Use shared PaywallEventTracker & inject tracker

Add a singleton PaywallEventTracker.shared and wire it into UI and purchasing code. Replace local tracker instances in paywall views with PaywallEventTracker.shared.componentInteractionLogger, and update PurchaseHandler to accept an injectable PaywallEventTracker (defaulting to .shared). Update test helpers and mocks to construct and pass explicit eventTracker instances, adjust LoadingPaywallView test setup to provide a dedicated tracker, and add new source/test files to the Xcode project file. These changes allow deterministic event-tracking in tests and avoid duplicated tracker instances at runtime.

* Remove convenience trackComponentInteraction overload

Remove the parameter-based convenience overload of PaywallEventTracker.trackComponentInteraction and consolidate usage to the single method that accepts PaywallEvent.ComponentInteractionData. Update unit tests to construct ComponentInteractionData via its initializer and call trackComponentInteraction(data) accordingly. This removes duplicated API surface and centralizes event construction.

* Remove EventDispatcher from PurchaseHandler

Remove the EventDispatcher abstraction and related properties from PurchaseHandler: delete the EventDispatcher typealias, backgroundEventDispatcher and testEventDispatcher, and the eventDispatcher stored property and init parameter. Update mapping helpers to stop propagating an eventDispatcher. Update tests and test helpers to pass inline Task { await work() } closures where needed. Files changed: PurchaseHandler.swift, PurchaseHandler+TestData.swift, and related tests in Tests/RevenueCatUITests/Purchasing.

* add name to stack partial

* Update PaywallAPI.swift

* Add testEventDispatcher back to tests

Introduce PaywallEventTracker.testEventDispatcher that uses Task { } (non-detached) so test events inherit caller context and aren't deprioritized by Task.detached(priority: .background) on iOS 26+ CI. Update PurchaseHandler test data and unit tests to pass this test dispatcher when constructing the event tracker to ensure prompt event delivery in tests.

* Add plan-selection metadata to paywall component interaction events

* Add package-selection sheet tracking (PWENG-15)

* [skip ci] Update baseline swiftinterface files (#6564)

* Route paywall control analytics through ComponentInteractionData factories (PWENG-15)

* [skip ci] Update baseline swiftinterface files (#6565)

* Make PaywallEventTracker thread-safe with locked state (PWENG-15)

* Clarify unknown button action telemetry as diagnostics (PWENG-15)

* Isolate paywall analytics state per session in shared tracker (PWENG-15)

* Fix swiftlint issues (PWENG-15)

* Prune PaywallEventTracker session state (PWENG-15)

* Fix ) SPM RevenueCatUl Tests

* Move paywall test event dispatcher out of RevenueCatUI production code (PWENG-15)

* Add purchase button interaction tracking (#6575)

* Add purchase button interaction tracking

Add tracking for purchase button taps: introduce ComponentInteractionType.purchaseButton and a paywallPurchaseButtonAction factory that captures componentName, componentValue, componentURL, currentPackageIdentifier and currentProductIdentifier. Expose componentName and customWebCheckoutUrl on the PurchaseButton view model and log the interaction from PurchaseButtonComponentView before initiating the purchase. Extend PurchaseButtonComponent to include an optional name and make Method conform to CustomStringConvertible to provide stable string values for logging. Update previews to include the new name field where applicable.

Co-Authored-By: Monika Mateska <45938914+MonikaMateska@users.noreply.github.com>

* Use context-aware web checkout URL

Replace direct use of viewModel.customWebCheckoutUrl with viewModel.urlForWebCheckout(packageContext: packageContext)?.url when logging the purchase button interaction. This ensures the logged componentURL is derived using the current package context (if available) and pulls the URL from the computed web checkout result rather than the raw custom value.

Co-Authored-By: Monika Mateska <45938914+MonikaMateska@users.noreply.github.com>

* Add tests for purchase button interactions

Add comprehensive tests covering paywall purchase button behavior across components and tracking.

- Tests/RevenueCatUITests/Purchasing/ControlInteractionLoggerTests.swift: add factory tests for PaywallEvent.ComponentInteractionData.paywallPurchaseButtonAction (componentType, name, value, URL, current package/product identifiers, and fully-populated case).
- Tests/RevenueCatUITests/Purchasing/PaywallEventTrackerTests.swift: add tracker tests to ensure component interaction events include purchase button metadata and handle nil name/no-package cases.
- Tests/UnitTests/Paywalls/Components/PurchaseButtonComponentTests.swift: add decoding tests for name present/absent, include name:nil in expected models, add Method.description tests and ComponentInteractionType raw value test.

These tests validate decoding, description strings, raw values, and that tracking records the expected purchase button metadata.

Co-Authored-By: Monika Mateska <45938914+MonikaMateska@users.noreply.github.com>

* Include name in PurchaseButtonComponent eq/hash

Add `name` to the PurchaseButtonComponent Hashable and Equatable implementations. This ensures instances with different `name` values produce distinct hashes and are not considered equal, preventing incorrect comparisons or collection behavior.

Co-Authored-By: Monika Mateska <45938914+MonikaMateska@users.noreply.github.com>

* [skip ci] Update baseline swiftinterface files (#6577)

---------

Co-authored-by: Monika Mateska <45938914+MonikaMateska@users.noreply.github.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>

* Route paywall component interaction logger through PurchaseHandler (PWENG-15)

* Log V1 purchase button component interactions (PWENG-30)

* Consolidate ComponentInteractionData factory extensions (PWENG-15)

* Defer Paywalls V2 purchase button analytics until checkout is resolved (PWENG-30)

* Fix lint issue (PWENG-15)

* Use NSLock.withLock instead of custom perform (PWENG-15)

* Fix failing test (PWENG-15)

* Fix failing tests (PWENG-15)

* Log Paywalls V2 purchase button interaction before purchase gate (PWENG-15)

* Fallback tier selector interaction value to tier id (PWENG-15)

* Record paywall_close before session reset on UIKit dismiss (PWENG-15)

* Fix lint issues (PWENG-15)

* Expose tier selector interaction value for tests (PWENG-15)

* correct modifers

* Emit paywall close after exit offer dismiss (PWENG-15)

* Consolidate PaywallsV2 impression tracking and eligibility in addPaywallModifiers (PWENG-15)

* Derive tab toggle from selectedTabId to avoid sync race (PWENG-15)

* Fix lint issue (PWENG-15)

* Fix lint issue (PWENG-15)

* Clear tab package selection when user picks a root-only package (PWENG-15)

* Fix Swift 5.8 compile error in purchase button method description (PWENG-15)

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Alexander Repty <alexander.repty@mac.com>
Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
Co-authored-by: Alexander Repty <alex.repty@revenuecat.com>
2026-04-16 15:30:01 +02:00
13212e20f1 Track paywall source for Customer Center purchases (#5691)
* Track customer center purchases

* simplified logic

* add spiinternal

* fixed tests

* pod lib lint

* Add PaywallSource.swift to RevenueCatUI target

* Remove unnecessary `@_spi(Internal)` attributes

* Simplified Customer Center paywall source to `customer_center`

* Remove redundant `source` parameter from `track` method

* Update snapshot files

* Move PaywallSource to RevenueCat module as @_spi(Internal) enum

PaywallSource is now a String-backed enum in the core SDK module
(alongside PaywallViewMode), with PaywallEvent.Data.source typed as
PaywallSource? instead of String?. Public inits without source are
deprecated in favor of @_spi(Internal) inits that accept the typed
source parameter.

Made-with: Cursor

* Fix: pass `paywallSource` to `PaywallEvent` creation

* Cleanup unnecessary diff

* Revert changes to Examples/rc-maestro/rc-maestro/Resources/StoreKit/StoreKitConfiguration.storekit

* Remove redundant source parameter from trackPaywallImpression

The source is already embedded in PaywallEvent.Data by createEventData(),
so passing it separately was redundant. Callers no longer need to forward
paywallSource explicitly.

Made-with: Cursor

* Undo changes to RevenueCatUI/Purchasing/MockPurchases.swift

* Undo changes to RevenueCatUI/Purchasing/PurchaseHandler.swift

* Remove presentedOfferingSource from receipt posting

This top-level field will be added in a separate PR for the
non-paywall Customer Center purchase path. For now, source is
only sent inside the paywall object.

Made-with: Cursor

* Remove trailing newlines from snapshot JSON files

Restores the original file endings to avoid potential test
comparison issues when files are opened in Xcode.

Made-with: Cursor

* Simplification

* Remove trailing newlines from OfferingContext snapshot files

Restores the original file endings to match main and avoid
spurious last-line diffs in the PR.

Made-with: Cursor

* Change Paywall.source type from String? to PaywallSource?

Uses the strongly-typed enum directly instead of its raw value,
letting Codable handle the string encoding automatically.

Made-with: Cursor

* Revert cosmetic changes to PurchaseHandler+TestData.swift

Restores original formatting from main; the diff was purely
noise (reformatting + redundant default parameter).

Made-with: Cursor

* Change EventsRequest.PaywallEvent.source type from String? to PaywallSource?

Consistent with PostReceiptDataOperation.Paywall.source, uses the
strongly-typed enum directly and lets Codable handle serialization.

Made-with: Cursor

* Cleanup

* Revert cosmetic changes in AsyncPurchaseHandler init

Restores original trailing closure syntax to reduce PR noise.

Made-with: Cursor

* Add snapshot test for PaywallEvent source serialization

Verifies that the source field is correctly encoded as
"customer_center" in the EventsRequest.PaywallEvent payload.

Made-with: Cursor

* Update deprecation messages for PaywallEvent.Data inits

Replace implementation-specific deprecation messages with a generic
"This initializer will be removed in a future version." message.

Made-with: Cursor

* Fix compilation error, add source: nil test coverage, fix snapshot newline

- Add @_spi(Internal) import to PurchaseHandlerTests so the `source`
  parameter on the @_spi(Internal) PaywallEvent.Data init is visible.
- Restore testPostsReceiptDataWithPresentedPaywall to use source: nil
  (the common case) so we verify the source key is absent from JSON.
- Add testPostsReceiptDataWithPresentedPaywallAndSource for the
  source: .customerCenter case with its own snapshots.
- Fix missing trailing newline in testImpressionEventWithSource snapshot.

Made-with: Cursor

* Fix snapshot test trailing newlines causing test failures

Remove spurious trailing newlines from 11 snapshot JSON files.
The snapshot library produces output without a final newline, so
the extra byte caused every comparison to fail.

Made-with: Cursor

* Fix exhaustive switch on PaywallEvent in PurchaseHandlerTests

Add missing cases for exitOffer, purchaseInitiated, and purchaseError.

Made-with: Cursor

* Fix PaywallSource being dropped by withPurchaseInfo

withPurchaseInfo was creating a new PaywallEvent.Data without forwarding
self.source, so the source was silently lost for purchaseInitiated,
cancel, and purchaseError events. This meant the POST receipt paywall.source
field was always nil even when the paywall was presented from Customer Center.

Also remove the default nil for the source parameter in the internal init
to prevent similar issues in the future.

Made-with: Cursor

* fix: Add missing `source` parameter to PaywallEvent.Data init in tests

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

---------

Co-authored-by: Antonio Pallares <antonio.pallares@revenuecat.com>
Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:10:39 +00:00
Will Taylor 9356db1d42 [CEC Mode]: introduce isPurchaseAllowedByRestoreBehavior() (#6192) 2026-02-06 11:54:42 -06:00
b1df8c54e6 Add paywall_id to paywall events and POST /receipt requests (#6087)
* Small cleanup

* add `LocalTransactionDetailsStorage`

* Add models for local transaction metadata caching. Reset PurchasesOrchestrator

* Complete `Codable`, lossless implementation of `LocalTransactionMetadata`

* Rename `LocalTransactionMetadataCache` to `LocalTransactionMetadataStore`

* Comment

* lint

* Remove unnecessary import

* Remove unused method

* Replace `Storefront` with `storeCountry` in both `ProductRequestData` and `PurchasedTransactionData`

This removes the need of a `StorefrontEncodedWrapper` object

* Disambiguate `asDictionary()` method

Now that `SubscriberAttribute` is `Codable`, `Encodable`'s `asDictionary()` and `SubscriberAttribute`'s `asDictionary()` were clashing

* Undo `Codable` conformance of `PresentedOfferingContext` and `TargetingContext`

* lint

* `LocalTransactionMetadata` now refers to a single transaction. `LocalTransactionMetadataStore` now stores one file per transaction and does not use an in-memory cache

* Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review

* Revert "Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review"

This reverts commit d958601711.

* Small code cleanup

* Remove `EncodedAppleReceipt` from `LocalTransactionMetadata`

* Undo `Codable` conformance of `EncodedAppleReceipt`

Not needed for now

* Remove `self.appTransactionJWS` from `LocalTransactionMetadata`

* Adapt to API changes in `SynchronizedLargeItemCache`

* Plug in `LocalTransactionMetadataStore` and store + get stored `LocalTransactionMetadata`

* Removed unused parameter

* Remove `schemaVersion` property from `LocalTransactionMetadata`

* Add `MockLocalTransactionMetadataStore` for tests

* Remove `appUserID` from `PurchasedTransactionData`

* Update tests

* Add `transaction_id` field to POST receipt

* adapt tests

* Add transaction ID parameter to cache key for `PostReceiptDataOperation`'s factory

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

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

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

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

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

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

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

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

* Fix lint

* Only send transaction ID when there's associated metadata

* Fix tests compilation

* Add unit test

* Add tests

* Add tests for `LocalTransactionMetadataStore`

* Have `PurchasesOrchestrator` post transaction with syncing purchases in SK2 go through `TransactionPoster` to include the `LocalTransactionMetadata` logic there

* Correct values for `purchaseCompletedBy` field in `PostReceiptDataOperation`

* Fix finish transaction logic

* Add more tests and fix test compilation

* More tests

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

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

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

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

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

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

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

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

* Fix lint

* Remove TODO

* Remove redundancy

* Fix tests and add test

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

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

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

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

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

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

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

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

* Add `transactionId` to `LocalTransactionMetadata`

* Add `getAllStoredMetadata()` method to `LocalTransactionMetadataStore`

* Cleanup stored metadata file if it fails to be read or decode

* Fix lint

* Simplify parameters in `handlePostReceiptResult` method in `PurchasesOrchestrator`

The same subscriber attributes were already included in the `PurchasedTransactionData` object

* Fix recovering `PresentedOfferingContext` from `LocalTransactionMetadata`

* Add `EncodedAppleReceipt` to `LocalTransactionMetadata`

* Take `source: PurchaseSource` out from `PurchasedTransactionData`

* Rename `PurchaseSource` to `PostReceiptSource` and `ProductRequestData.InitiationSource` to `PostReceiptSource.InitiationSource`

* Pass receipt as parameter directly

* Fix lint

* Remove unused variable

* Add internal interface to sync cached local transaction metadata

* Adapt unit tests

* Trigger cached transaction metadata sync on `applicationWillEnterForeground`

* fix test with previous value in snapshot

* Parse paywall identifier field from GET /offerings responses and add field to Paywall events and POST /receipt operations

* Update snapshots

* Fix compilation of tests

* Make paywall id default to `nil` in `init`

* Update API tests

* Introduce `sdk_originated` parameter in POST /receipt

* Adapt tests to new `sdkOriginated` property

* TEMP: Remove sdkOriginated field (to split into 2 PRs)

* Revert "TEMP: Remove sdkOriginated field (to split into 2 PRs)"

This reverts commit 6d361fde30.

* Remove sdkOriginated field as we won't be adding it for now

This reverts commit e4989a3d33.

* Store transaction metadata when offering context or paywall is present

* Fix PaywallEvent initialization in tests

* Use variable

* Fix compilation of tests

* Improve tests

* Fix lint

* Cache CustomerInfo when receiving responses from cached transaction metadata syncs

* Add more tests

* Fix tests

* Move syncing of cached local metadata to `applicationDidBecomeActive`, which does happen on cold app launches

* Prevent concurrent cached transaction metadata syncs

* Add tests for applicationDidBecomeActive syncing cached transaction metadata

* Small improvements

* Perform sync in `applicationWillEnterForeground` + at `configure` time

* Fix tests after merge from main

* Fix and improved tests

* Fix tests

* test improvements

* Use `transactionId` in `PostData` as cacheKey only if it `containsAttributionData`

This means that POST /receipt for renewals are deduplicated, while purchases with attribution data are guaranteed to always be sent (regardless of whether there's an in-flight POST /receipt request for a renewal of that purchase)

* Adapt tests to new behavior + new tests

* Add `paywall_version` field to POST /receipt

* Update Sources/Networking/Operations/PostReceiptDataOperation.swift

Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>

* Do not clear `LocalTransactionMetadata` when receiving offline-computed CustomerInfo

* Add tests

* Fix test + add test

* Fix test

* Fix lint

* Fix test compilation

* Improve comment

* Fix codable `CodingKeys` to not use snake case in rawValues As the `JSONEncoder.default` uses `convertToSnakeCase` `keyEncodingStrategy`

This allows decoding using the `JSONDecoder.default (which uses `convertFromSnakeCase` `keyDecodingStrategy` to correctly decode the JSONs resulting from encoding with `JSONEncoder.default`.

* Add tests

* Improve comment

* Add `Codable` idempotency tests for `EncodedAppleReceipt`

* Add tests for encoding/decoding `LocalTransactionMetadata`

* Have `LocalTransactionMetadataStore` use a `.persistence` `DirectoryType` instead of a `.cache` one

* Add E2E tests for `LocalTransactionMetadataStore`

* Fix compilation

* Fix test

* Add some logs and reduce some flakiness of tests

* Remove test (will be added in a future PR after a small rewrite)

* Reduce test flakiness

* Change directory name

* Perform cached transaction metadata sync without delay on tests

* Remove unnecessary helper in tests

* Fix deadlock

* Inject `applicationSupportDirectory` to `LocalTransactionMetadataStore` for more reliable tests

* Update snapshots for getOfferings in integration tests

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
2026-01-27 08:42:39 +01:00
507c9d42f7 Add payload_version to POST /receipt (#6130)
* Small cleanup

* add `LocalTransactionDetailsStorage`

* Add models for local transaction metadata caching. Reset PurchasesOrchestrator

* Complete `Codable`, lossless implementation of `LocalTransactionMetadata`

* Rename `LocalTransactionMetadataCache` to `LocalTransactionMetadataStore`

* Comment

* lint

* Remove unnecessary import

* Remove unused method

* Replace `Storefront` with `storeCountry` in both `ProductRequestData` and `PurchasedTransactionData`

This removes the need of a `StorefrontEncodedWrapper` object

* Disambiguate `asDictionary()` method

Now that `SubscriberAttribute` is `Codable`, `Encodable`'s `asDictionary()` and `SubscriberAttribute`'s `asDictionary()` were clashing

* Undo `Codable` conformance of `PresentedOfferingContext` and `TargetingContext`

* lint

* `LocalTransactionMetadata` now refers to a single transaction. `LocalTransactionMetadataStore` now stores one file per transaction and does not use an in-memory cache

* Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review

* Revert "Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review"

This reverts commit d958601711.

* Small code cleanup

* Remove `EncodedAppleReceipt` from `LocalTransactionMetadata`

* Undo `Codable` conformance of `EncodedAppleReceipt`

Not needed for now

* Remove `self.appTransactionJWS` from `LocalTransactionMetadata`

* Adapt to API changes in `SynchronizedLargeItemCache`

* Plug in `LocalTransactionMetadataStore` and store + get stored `LocalTransactionMetadata`

* Removed unused parameter

* Remove `schemaVersion` property from `LocalTransactionMetadata`

* Add `MockLocalTransactionMetadataStore` for tests

* Remove `appUserID` from `PurchasedTransactionData`

* Update tests

* Add `transaction_id` field to POST receipt

* adapt tests

* Add transaction ID parameter to cache key for `PostReceiptDataOperation`'s factory

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

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

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

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

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

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

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

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

* Fix lint

* Only send transaction ID when there's associated metadata

* Fix tests compilation

* Add unit test

* Add tests

* Add tests for `LocalTransactionMetadataStore`

* Have `PurchasesOrchestrator` post transaction with syncing purchases in SK2 go through `TransactionPoster` to include the `LocalTransactionMetadata` logic there

* Correct values for `purchaseCompletedBy` field in `PostReceiptDataOperation`

* Fix finish transaction logic

* Add more tests and fix test compilation

* More tests

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

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

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

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

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

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

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

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

* Fix lint

* Remove TODO

* Remove redundancy

* Fix tests and add test

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

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

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

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

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

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

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

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

* Add `transactionId` to `LocalTransactionMetadata`

* Add `getAllStoredMetadata()` method to `LocalTransactionMetadataStore`

* Cleanup stored metadata file if it fails to be read or decode

* Fix lint

* Simplify parameters in `handlePostReceiptResult` method in `PurchasesOrchestrator`

The same subscriber attributes were already included in the `PurchasedTransactionData` object

* Fix recovering `PresentedOfferingContext` from `LocalTransactionMetadata`

* Add `EncodedAppleReceipt` to `LocalTransactionMetadata`

* Take `source: PurchaseSource` out from `PurchasedTransactionData`

* Rename `PurchaseSource` to `PostReceiptSource` and `ProductRequestData.InitiationSource` to `PostReceiptSource.InitiationSource`

* Pass receipt as parameter directly

* Fix lint

* Remove unused variable

* Add internal interface to sync cached local transaction metadata

* Adapt unit tests

* Trigger cached transaction metadata sync on `applicationWillEnterForeground`

* fix test with previous value in snapshot

* Fix compilation of tests

* Introduce `sdk_originated` parameter in POST /receipt

* Adapt tests to new `sdkOriginated` property

* TEMP: Remove sdkOriginated field (to split into 2 PRs)

* Revert "TEMP: Remove sdkOriginated field (to split into 2 PRs)"

This reverts commit 6d361fde30.

* Remove sdkOriginated field as we won't be adding it for now

This reverts commit e4989a3d33.

* Store transaction metadata when offering context or paywall is present

* Fix PaywallEvent initialization in tests

* Use variable

* Cache CustomerInfo when receiving responses from cached transaction metadata syncs

* Add more tests

* Fix tests

* Move syncing of cached local metadata to `applicationDidBecomeActive`, which does happen on cold app launches

* Prevent concurrent cached transaction metadata syncs

* Add tests for applicationDidBecomeActive syncing cached transaction metadata

* Small improvements

* Perform sync in `applicationWillEnterForeground` + at `configure` time

* Fix tests after merge from main

* Fix and improved tests

* Fix tests

* test improvements

* Use `transactionId` in `PostData` as cacheKey only if it `containsAttributionData`

This means that POST /receipt for renewals are deduplicated, while purchases with attribution data are guaranteed to always be sent (regardless of whether there's an in-flight POST /receipt request for a renewal of that purchase)

* Adapt tests to new behavior + new tests

* Add `paywall_version` field to POST /receipt

* Update Sources/Networking/Operations/PostReceiptDataOperation.swift

Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>

* Do not clear `LocalTransactionMetadata` when receiving offline-computed CustomerInfo

* Add tests

* Fix test + add test

* Fix test

* Fix lint

* Fix test compilation

* Improve comment

* Fix codable `CodingKeys` to not use snake case in rawValues As the `JSONEncoder.default` uses `convertToSnakeCase` `keyEncodingStrategy`

This allows decoding using the `JSONDecoder.default (which uses `convertFromSnakeCase` `keyDecodingStrategy` to correctly decode the JSONs resulting from encoding with `JSONEncoder.default`.

* Add tests

* Improve comment

* Add `Codable` idempotency tests for `EncodedAppleReceipt`

* Add tests for encoding/decoding `LocalTransactionMetadata`

* Have `LocalTransactionMetadataStore` use a `.persistence` `DirectoryType` instead of a `.cache` one

* Add E2E tests for `LocalTransactionMetadataStore`

* Fix test

* Add some logs and reduce some flakiness of tests

* Remove test (will be added in a future PR after a small rewrite)

* Reduce test flakiness

* Change directory name

* Perform cached transaction metadata sync without delay on tests

* Remove unnecessary helper in tests

* Fix deadlock

* Inject `applicationSupportDirectory` to `LocalTransactionMetadataStore` for more reliable tests

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
2026-01-26 20:45:51 +01:00
Antonio Pallares e43ac94d42 Fix CodingKeys to work correctly with snake_case key decoding strategies (#6134)
* Improve comment

* Fix codable `CodingKeys` to not use snake case in rawValues As the `JSONEncoder.default` uses `convertToSnakeCase` `keyEncodingStrategy`

This allows decoding using the `JSONDecoder.default (which uses `convertFromSnakeCase` `keyDecodingStrategy` to correctly decode the JSONs resulting from encoding with `JSONEncoder.default`.

* Add tests

* Improve comment
2026-01-26 12:50:00 +01:00
Antonio Pallares a0636e36df Improve accuracy of transactions origin Part 7: add sdk_originated to POST /receipt (#6091) 2026-01-23 21:35:19 +01:00
0e10bc8720 Improve accuracy of transactions origin Part 6: add transaction_id to POST /receipt (#6023)
* Small cleanup

* add `LocalTransactionDetailsStorage`

* Add models for local transaction metadata caching. Reset PurchasesOrchestrator

* Complete `Codable`, lossless implementation of `LocalTransactionMetadata`

* Rename `LocalTransactionMetadataCache` to `LocalTransactionMetadataStore`

* Comment

* lint

* Remove unnecessary import

* Remove unused method

* Replace `Storefront` with `storeCountry` in both `ProductRequestData` and `PurchasedTransactionData`

This removes the need of a `StorefrontEncodedWrapper` object

* Disambiguate `asDictionary()` method

Now that `SubscriberAttribute` is `Codable`, `Encodable`'s `asDictionary()` and `SubscriberAttribute`'s `asDictionary()` were clashing

* Undo `Codable` conformance of `PresentedOfferingContext` and `TargetingContext`

* lint

* `LocalTransactionMetadata` now refers to a single transaction. `LocalTransactionMetadataStore` now stores one file per transaction and does not use an in-memory cache

* Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review

* Revert "Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review"

This reverts commit d958601711.

* Small code cleanup

* Remove `EncodedAppleReceipt` from `LocalTransactionMetadata`

* Undo `Codable` conformance of `EncodedAppleReceipt`

Not needed for now

* Remove `self.appTransactionJWS` from `LocalTransactionMetadata`

* Adapt to API changes in `SynchronizedLargeItemCache`

* Plug in `LocalTransactionMetadataStore` and store + get stored `LocalTransactionMetadata`

* Removed unused parameter

* Remove `schemaVersion` property from `LocalTransactionMetadata`

* Add `MockLocalTransactionMetadataStore` for tests

* Remove `appUserID` from `PurchasedTransactionData`

* Update tests

* Add `transaction_id` field to POST receipt

* adapt tests

* Add transaction ID parameter to cache key for `PostReceiptDataOperation`'s factory

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

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

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

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

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

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

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

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

* Fix lint

* Only send transaction ID when there's associated metadata

* Fix tests compilation

* Add unit test

* Add tests

* Add tests for `LocalTransactionMetadataStore`

* Have `PurchasesOrchestrator` post transaction with syncing purchases in SK2 go through `TransactionPoster` to include the `LocalTransactionMetadata` logic there

* Correct values for `purchaseCompletedBy` field in `PostReceiptDataOperation`

* Fix finish transaction logic

* Add more tests and fix test compilation

* More tests

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

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

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

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

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

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

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

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

* Fix lint

* Remove TODO

* Remove redundancy

* Fix tests and add test

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

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

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

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

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

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

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

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

* Fix lint

* Fix recovering `PresentedOfferingContext` from `LocalTransactionMetadata`

* Take `source: PurchaseSource` out from `PurchasedTransactionData`

* Rename `PurchaseSource` to `PostReceiptSource` and `ProductRequestData.InitiationSource` to `PostReceiptSource.InitiationSource`

* Pass receipt as parameter directly

* Fix lint

* Remove unused variable

* fix test with previous value in snapshot

* Fix compilation of tests

* Introduce `sdk_originated` parameter in POST /receipt

* Adapt tests to new `sdkOriginated` property

* TEMP: Remove sdkOriginated field (to split into 2 PRs)

* Store transaction metadata when offering context or paywall is present

* Fix PaywallEvent initialization in tests

* Use variable

* Fix tests after merge from main

* Fix and improved tests

* Fix tests

* test improvements

* Use `transactionId` in `PostData` as cacheKey only if it `containsAttributionData`

This means that POST /receipt for renewals are deduplicated, while purchases with attribution data are guaranteed to always be sent (regardless of whether there's an in-flight POST /receipt request for a renewal of that purchase)

* Adapt tests to new behavior + new tests

* Update Sources/Networking/Operations/PostReceiptDataOperation.swift

Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>

* Do not clear `LocalTransactionMetadata` when receiving offline-computed CustomerInfo

* Add tests

* Fix test + add test

* Fix test

* Fix lint

* Fix tests

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
2026-01-23 21:00:33 +01:00
Antonio PallaresandRevenueCat Git Bot 1569b69e58 Improve accuracy of transactions origin Part 3: remove PurchaseSource from PurchasedTransactionData and rename it to PostReceiptSource (#6076)
* Small cleanup

* add `LocalTransactionDetailsStorage`

* Add models for local transaction metadata caching. Reset PurchasesOrchestrator

* Complete `Codable`, lossless implementation of `LocalTransactionMetadata`

* Rename `LocalTransactionMetadataCache` to `LocalTransactionMetadataStore`

* Comment

* lint

* Remove unnecessary import

* Remove unused method

* Replace `Storefront` with `storeCountry` in both `ProductRequestData` and `PurchasedTransactionData`

This removes the need of a `StorefrontEncodedWrapper` object

* Disambiguate `asDictionary()` method

Now that `SubscriberAttribute` is `Codable`, `Encodable`'s `asDictionary()` and `SubscriberAttribute`'s `asDictionary()` were clashing

* Undo `Codable` conformance of `PresentedOfferingContext` and `TargetingContext`

* lint

* `LocalTransactionMetadata` now refers to a single transaction. `LocalTransactionMetadataStore` now stores one file per transaction and does not use an in-memory cache

* Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review

* Revert "Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review"

This reverts commit d958601711.

* Small code cleanup

* Remove `EncodedAppleReceipt` from `LocalTransactionMetadata`

* Undo `Codable` conformance of `EncodedAppleReceipt`

Not needed for now

* Remove `self.appTransactionJWS` from `LocalTransactionMetadata`

* Adapt to API changes in `SynchronizedLargeItemCache`

* Plug in `LocalTransactionMetadataStore` and store + get stored `LocalTransactionMetadata`

* Removed unused parameter

* Remove `schemaVersion` property from `LocalTransactionMetadata`

* Add `MockLocalTransactionMetadataStore` for tests

* Remove `appUserID` from `PurchasedTransactionData`

* Update tests

* Add tests

* Add tests for `LocalTransactionMetadataStore`

* Have `PurchasesOrchestrator` post transaction with syncing purchases in SK2 go through `TransactionPoster` to include the `LocalTransactionMetadata` logic there

* Correct values for `purchaseCompletedBy` field in `PostReceiptDataOperation`

* Fix finish transaction logic

* Add more tests and fix test compilation

* More tests

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

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

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

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

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

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

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

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

* Fix lint

* Remove TODO

* Fix recovering `PresentedOfferingContext` from `LocalTransactionMetadata`

* Take `source: PurchaseSource` out from `PurchasedTransactionData`

* Rename `PurchaseSource` to `PostReceiptSource` and `ProductRequestData.InitiationSource` to `PostReceiptSource.InitiationSource`

* Pass receipt as parameter directly

* Fix lint

* Remove unused variable

* fix test with previous value in snapshot

* Use variable

* Fix tests after merge from main

* Fix and improved tests

* Fix tests

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2026-01-23 11:34:45 +01:00
Antonio PallaresandRevenueCat Git Bot fa25f1edfe Improve accuracy of transactions origin Part 2: store and fetch transaction metadata (#6014)
* Small cleanup

* add `LocalTransactionDetailsStorage`

* Add models for local transaction metadata caching. Reset PurchasesOrchestrator

* Complete `Codable`, lossless implementation of `LocalTransactionMetadata`

* Rename `LocalTransactionMetadataCache` to `LocalTransactionMetadataStore`

* Comment

* lint

* Remove unnecessary import

* Remove unused method

* Replace `Storefront` with `storeCountry` in both `ProductRequestData` and `PurchasedTransactionData`

This removes the need of a `StorefrontEncodedWrapper` object

* Disambiguate `asDictionary()` method

Now that `SubscriberAttribute` is `Codable`, `Encodable`'s `asDictionary()` and `SubscriberAttribute`'s `asDictionary()` were clashing

* Undo `Codable` conformance of `PresentedOfferingContext` and `TargetingContext`

* lint

* `LocalTransactionMetadata` now refers to a single transaction. `LocalTransactionMetadataStore` now stores one file per transaction and does not use an in-memory cache

* Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review

* Revert "Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review"

This reverts commit d958601711.

* Small code cleanup

* Remove `EncodedAppleReceipt` from `LocalTransactionMetadata`

* Undo `Codable` conformance of `EncodedAppleReceipt`

Not needed for now

* Remove `self.appTransactionJWS` from `LocalTransactionMetadata`

* Adapt to API changes in `SynchronizedLargeItemCache`

* Plug in `LocalTransactionMetadataStore` and store + get stored `LocalTransactionMetadata`

* Removed unused parameter

* Remove `schemaVersion` property from `LocalTransactionMetadata`

* Add `MockLocalTransactionMetadataStore` for tests

* Remove `appUserID` from `PurchasedTransactionData`

* Update tests

* Add tests

* Add tests for `LocalTransactionMetadataStore`

* Have `PurchasesOrchestrator` post transaction with syncing purchases in SK2 go through `TransactionPoster` to include the `LocalTransactionMetadata` logic there

* Correct values for `purchaseCompletedBy` field in `PostReceiptDataOperation`

* Fix finish transaction logic

* Add more tests and fix test compilation

* More tests

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

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

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

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

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

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

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

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

* Fix lint

* Remove TODO

* Fix recovering `PresentedOfferingContext` from `LocalTransactionMetadata`

* Pass receipt as parameter directly

* Fix lint

* Fix tests after merge from main

* Fix and improved tests

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2026-01-23 10:09:34 +01:00
Antonio Pallares 0c3467ae78 Improve accuracy of transactions origin Part 1: refactor to allow caching transaction metadata (#5940)
* Small cleanup

* add `LocalTransactionDetailsStorage`

* Add models for local transaction metadata caching. Reset PurchasesOrchestrator

* Complete `Codable`, lossless implementation of `LocalTransactionMetadata`

* Rename `LocalTransactionMetadataCache` to `LocalTransactionMetadataStore`

* Comment

* lint

* Remove unnecessary import

* Remove unused method

* Replace `Storefront` with `storeCountry` in both `ProductRequestData` and `PurchasedTransactionData`

This removes the need of a `StorefrontEncodedWrapper` object

* Disambiguate `asDictionary()` method

Now that `SubscriberAttribute` is `Codable`, `Encodable`'s `asDictionary()` and `SubscriberAttribute`'s `asDictionary()` were clashing

* Undo `Codable` conformance of `PresentedOfferingContext` and `TargetingContext`

* lint

* `LocalTransactionMetadata` now refers to a single transaction. `LocalTransactionMetadataStore` now stores one file per transaction and does not use an in-memory cache

* Temporarily remove `LocalTransactionMetadata` and `LocalTransactionMetadataStore` for easier PR review

* Undo `Codable` conformance of `EncodedAppleReceipt`

Not needed for now
2026-01-23 08:29:08 +01:00
Antonio Pallares a3df5dd229 Prevent duplicate post receipt requests (#5795)
* Prevent duplicate post receipt requests caused by timestamp-only attribute changes in the `consentStatus` SubscriberAttribute

* unit tests

* More tests

* Simplified MockDateProvider

* Add unit test covering edge case

* Fix lint
2025-11-17 09:25:08 +01:00
Antonio PallaresandRevenueCat Git Bot 510f4e73a4 Add internal Offerings source properties (#5749)
* Add `isLoadShedderResponse` and `isFallbackURLResponse` properties to `VerifiedHTTPResponse`

Keeps the values also when returned by the ETagManager

* lint

* Add some tests for ETagManager

* Rename `isFallbackURLResponse` to `isFallbackUrlResponse` to prevent decoding issues

* Add tests for actual source of original cached response of ETagManager

* Add tests to HTTPClientTests regarding the new booleans

* lint

* Add `originalSource` to `CustomerInfo`

* Adds `isLoadedFromCache` boolean to CustomerInfo

* Add docs

* Unit tests for new customer info properties

* Make `OriginalSource.CustomerInfo` a `String` enum

* Add decoding tests for Customer Info with new original source field

* Add `.loadShedder` check for Customer Info obtained in Load Shedder integration tests

* Update fixture

* improve internal docs

* Add intermediate object `Offerings.Contents` to wrap `OfferingsResponse` + `OriginalSource` and `loadedFromCache`

* Adapt existing unit tests to changes in `Offerings` underlying objects

* Check Offerings original source in existing integration tests

* Fix StoreKit unit tests

* Fix RevenueCatUI tests

* More Offerings.Contents tests

* Fix test

* Add more tests

* Fix lint

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

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

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

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

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

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

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

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

* Use enum for `HTTPResponseSource`

* Update tests

* Adapt to changes in `VerifiedHTTPResponse`

* Lint

* Adapt to changes in `VerifiedHTTPResponse`

* update tests

* Add log when a request to the fallback URL was handled by load shedder (which should never happen)

* rename `HTTPResponseSource` to `HTTPResponseOriginalSource`

* Adapt to changes

* fix lint

* Adapt to changes

* Fix CustomerInfo's OriginalSource implementation + fix unit test

* Fix tests compilation

* Fix RCUI compilation

* Move isFromCache boolean from `Offerings.Contents` to `Offerings`

* Fix compilation of `DebugViewSwiftUITests`

* Fix compilation of RevenueCatUI tests

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2025-11-06 14:12:42 +01:00
Antonio Pallares 8b942f9fd9 Add Internal CustomerInfo source properties (#5737)
* Add `isLoadShedderResponse` and `isFallbackURLResponse` properties to `VerifiedHTTPResponse`

Keeps the values also when returned by the ETagManager

* lint

* Add some tests for ETagManager

* Rename `isFallbackURLResponse` to `isFallbackUrlResponse` to prevent decoding issues

* Add tests for actual source of original cached response of ETagManager

* Add tests to HTTPClientTests regarding the new booleans

* lint

* Add `originalSource` to `CustomerInfo`

* Adds `isLoadedFromCache` boolean to CustomerInfo

* Add docs

* Unit tests for new customer info properties

* Make `OriginalSource.CustomerInfo` a `String` enum

* Add decoding tests for Customer Info with new original source field

* Add `.loadShedder` check for Customer Info obtained in Load Shedder integration tests

* Update fixture

* improve internal docs

* Fix test

* Fix lint

* Use enum for `HTTPResponseSource`

* Update tests

* Adapt to changes in `VerifiedHTTPResponse`

* Lint

* update tests

* Add log when a request to the fallback URL was handled by load shedder (which should never happen)

* rename `HTTPResponseSource` to `HTTPResponseOriginalSource`

* Adapt to changes

* fix lint

* Fix CustomerInfo's OriginalSource implementation + fix unit test
2025-11-06 13:27:46 +01:00
RickandRevenueCat Git Bot 1c24d76d6f Update Xcode 16.0 to 16.4 in tests because of CircleCI's deprecation later this year (#5668)
* Update Xcode 16.0 to 16.4 in tests because of CircleCI's deprecation later this year

* Updated test simulators for Xcode 16.4

* Fixed unused variable warning

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

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

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

* Updated iOS simulator version after rebase

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2025-10-17 10:24:22 +02: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
Pol Piella Abadia 5bfbe3b703 Fixes API tests after changes to public API (#5365) 2025-07-07 18:18:53 +01: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
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
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
Mark VillacampaandToni Rico 72b06f6943 Introduce PurchaseParams to allow passing extra configuration info when making a purchase (#4400)
* Introduce PurchaseParams to allow passing extra configuration info when making a purchase

* simplify naming

* fallback to old method when flag is not enabled

* Add $(inherited) to the test targets so they pick up compiler flags from Local.xcconfig

* Include product/package inside PurchaseParams

* fix tests

* add compiler flag check

* update purchase tester

* fix docs

* add tests

* Update Sources/Networking/Operations/PostReceiptDataOperation.swift

Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>

---------

Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
2024-11-06 16:05:37 +07:00
Mark Villacampa 6f8e53ac40 Fix Xcode 16 warnings (#4334)
When compiling with Xcode 16, we started throwing a bunch of new warnings. Here's a list and explanation of each:

- The most common one was needing to explicitly restate the @unchecked Sendable conformance from a parent class.
- There were a couple switches where a new entry was added in tvOS but only for iOS 18, and needed to add it to make it exhaustive.
- There we a couple of places where we were adding a protocol conformance to a class from a different module e.g. extension DispatchTimeInterval: Comparable. This is considered unsafe now because the owners of the module could add the conformance later. Solved by removing the conformance which wasn't being used widely in the codebase, only in tests.
- Finally there was a class where we were capturing SKRequest and SKProductsRequest instances in a sendable block, but only to either log something, or use them as dictionary key, so I just made the classes Sendable (with the new @retroactice attribute because they belong to a different module)
2024-10-02 11:53:45 +02: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
Will Taylor 348258a41f only retry post receipt for 429 status codes 2024-07-25 16:46:31 -05:00
NachoSoto 2bcd0ce328 5.0.0 changes: support SK2 by default 2024-07-01 14:37:21 -05:00
Josh HoltzandRevenueCat Git Bot a9737aa7fc Add targeting to PresentedOfferingContext (#3730)
### Motivation

Get more context with targeting information 

### Description

- Parse targeting information into `Offerings`
- Add targeting context into `Offerings.current`
- Add targeting context into `getCurrentOffering(forPlacement: String)`
- Post targeting context in post receipts

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2024-02-28 16:10:52 -06:00
61d66bbc32 Add currentOffering(forPlacement: String) to Offerings (#3707)
### Motivation

New function to get `Offering` by a placement identifier

### Description

New `currentOffering(forPlacement: String): Offering?` method on
`Offerings`
- Added demo button for this in purchase tester app

```swift
// Example usage
if let offering = offerings.currentOffering(forPlacement: "onboarding") {
  showPaywall(offering)
}
```

#### Logic flow

- If a placement identifier explicitly exists within the
`placements.offeringIdsByPlacement` dictionary, the method attempts to
fetch the corresponding `Offering` without resorting to a fallback
- Example: when there is a `nil` / null value for a placement, we don't
want to use the fallback
- If the placement identifier does not exist, the method then attempts
to fetch a fallback offering defined by `placements.fallbackOfferingId`

#### New placements object in offerings response

```js
{
    // other offerings stuff
    "placements": {
        "fallback_offering_id": "offering_a", // can be null
        "offering_ids_by_placement": {
            "onboarding_start": null,
            "onboarding_end": "offering_b",
        }
  }
}
```

---------

Co-authored-by: Guido Torres <guido@revenuecat.com>
Co-authored-by: Josh Holtz <me@joshholtz.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2024-02-28 15:11:48 -06:00
32c63cdb46 [SK2] Add support for StoreKit Config files in SK2 (#3365)
When using StoreKit Config file, we do not have access to the
transaction history or subscription status server-side.

To work around this, we introduce a new `StoreKit2Receipt` type that
closely resembles the structure of an SK1 receipt and contains all the
transaction history and subscription status for the customer. It can be
sent in a single request to `/receipt`

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Distiller <distiller@static.38.39.183.137.cyberlynk.net>
Co-authored-by: Distiller <distiller@static.38.23.40.89.cyberlynk.net>
Co-authored-by: Distiller <distiller@static.38.39.185.65.cyberlynk.net>
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
2023-12-13 17:00:37 +01:00
NachoSoto ed80980d08 HTTPClient: verbose logs for request IDs (#3320)
See https://github.com/RevenueCat/khepri/pull/6798.
This can help debug issues as it helps us cross-reference requests on
our end.

### Changes:
- Logging `X-Request-ID` and `X-Amzn-Trace-ID` headers if present (only
in verbose mode)
- Logging these even for failed requests
- Also logging `request_handled_by_load_shedder` for failed requests
- To accomplish that. `HTTPURLResponse` now conforms to our
`HTTPResponseType`, this allows us to extract headers in an uniform way

### Example:
```
[network] DEBUG: ℹ️ API request completed: GET '/v1/subscribers/$RCAnonymousID%3Ad9dc4f3b33054cf28c0f2ec9da0f48d5' (304)
Request-ID: 'be534c50-5231-403b-be2f-7c077861751d'; Amzn-Trace-ID: 'Root=1-6532c6ca-7ef80f961ec9c4145484df13'
```
2023-10-25 20:24:23 -07:00
38b4e3b411 Paywalls: add identifier to events (#3332)
### Motivation

Send a unique `id` on each paywall event

### Description

- Added `id: ID?` to `PaywallEvent.Data`
- Optional so its backwards compatible with events recorded before this
change (see actual tests from below)
- Add `withNewID()` function that will mutate the `id` when tracked for
cancel and close

#### Events recored before `id` changes but posted with `id` changes

<img width="433" alt="Screenshot 2023-10-25 at 9 33 27 AM"
src="https://github.com/RevenueCat/purchases-ios/assets/401294/b6c77f0c-7429-4638-a4ba-7d6068c80cb7">

#### Events recorded and posted with `id` changes

<img width="479" alt="Screenshot 2023-10-25 at 9 37 53 AM"
src="https://github.com/RevenueCat/purchases-ios/assets/401294/8cae5329-2f31-4f03-8997-69dcee609e1f">

---------

Co-authored-by: NachoSoto <ignaciosoto90@gmail.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Distiller <distiller@static.38.39.183.26.cyberlynk.net>
Co-authored-by: Distiller <distiller@static.38.39.184.242.cyberlynk.net>
Co-authored-by: distiller <distiller@static.38.23.41.34.cyberlynk.net>
2023-10-25 20:26:50 -05:00
235a7dcdc1 StoreKit 2: Optionally send JWS tokens instead of receipts to the backend (#3227)
- Added a new option `usesStoreKit2JWS` under `DangerousSettings`. If
enabled, the SDK will send a JWS token instead of a receipt to the
applicable backend endpoints.
- The option must be used in conjunction with `usesStoreKit2IfAvailable`
configuration option.

### ToDo

- [x] Send JWS token when making a purchase in SK2 mode
- [x] Send JWS token when calculating promo offer eligibility
- [x] Update `syncTransactions` to send the latest transaction JWS.
- [x] Add tests

---------

Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Distiller <distiller@static.38.39.183.118.cyberlynk.net>
Co-authored-by: Distiller <distiller@static.198.206.135.125.macminivault.com>
Co-authored-by: Distiller <distiller@static.38.23.39.112.cyberlynk.net>
Co-authored-by: Distiller <distiller@static.38.39.183.58.cyberlynk.net>
Co-authored-by: Distiller <distiller@static.38.39.185.87.cyberlynk.net>
2023-10-16 22:45:29 +02:00
NachoSoto cc307399c3 Paywalls: added Purchases.track(paywallEvent:) (#3160)
This uses `PaywallEventsManager` (#3159) to track events, and saves the
presented `PaywallEvent` to be send along with
`PostReceiptDataOperation`.
2023-09-14 18:28:11 -07:00
NachoSoto 56de917d02 Paywalls: removed presentedPaywallViewMode (#3109)
We're going to track this in a different way in the future.
2023-09-14 18:28:11 -07:00
NachoSoto 8ba761a23f Paywalls: send presented PaywallViewMode with purchases (#2859)
I've moved `PaywallViewMode` from `RevenueCatUI` to `RevenueCat`. This
uses the same approach as #2645. This method is `public` so it can be
used from `RevenueCatUI`, but the documentation stays its purpose.

The behavior is covered with integration tests and a snapshot test.

Depends on https://github.com/RevenueCat/khepri/pull/6364.
2023-09-14 18:28:11 -07:00
NachoSoto 0ea37c5dd8 Refactor: extract HealthOperation (#3154) 2023-09-07 07:18:18 -07:00
NachoSoto 216f1e13c3 PurchasesOrchestrator: disambiguate transactions from the queue (#2890)
This adds tests and a fix for a scenario discovered by @aboedo and
@mcastany.

### Timeline:
- Make a purchase of product_1
- Close app
- Renewals happen in the background
- Subscription expires
- Open app, renewals are in the queue but not processed yet
- Start a purchase of `product_1` (`SKPaymentQueue.add`)
- StoreKit queue shows renewal from step 3 as finished, it's expired
- SDK sees finished transaction in the queue for `product_1`
- SDK sees completion block for `product_1`, considers it a successful
`.purchase`
- SDK posts receipt, `initiation_source: .purchase` 💥
- Backend replies with 422 because StoreKit hasn't processed the
transaction from 6

### Fix:
The fix looks at the date when the completion block was added (the time
`.purchase()` is called).
If the transaction was created _before_ this, we know it didn't come
from the actual purchase, and therefore it should be identified as
`.queue`.
2023-08-07 15:57:10 -07:00
NachoSoto cad8328a60 IntroEligibility: changed products to Set<String> (#2976)
Order is irrelevant when requesting intro eligibility.

Note that I haven't changed the public API because that wouldn't be
backwards compatible.
2023-08-07 13:30:26 -07:00
NachoSoto f7e36fb993 Trusted Entitlements: add support for signing POST body (#2753)
### Changes:
- `HTTPClient` has a new `X-Post-Params-Hash` request header
- Replaced `HTTPRequest.Method.post`'s `Encodable` value with a more
specific `HTTPRequestBody` (like `HTTPResponseBody`)
- `HTTPRequestBody` allows overriding `contentForSignature` (it defaults
to `[]`) which then `Signing` uses for the hash
- Fixed `LogInOperation` not passing the `VerificationResult` to the
result `CustomerInfo`. This is captured in unit and integration tests
now.
- Add support for signing `POST` requests in `Path.logIn` and
`Path.postReceiptData`

Depends on https://github.com/RevenueCat/khepri/pull/6246
2023-07-13 07:45:16 -07:00
NachoSoto 3da1c50925 HTTPRequest.Path: escape appUserID (#2747)
We were escaping users for every (hopefully) request. This refactor
moves that responsibility to the path creation in `HTTPRequest.Path` to
ensure it's done consistently.

Additionally, this is required so that `SigningTests` produce a
consistent path (see #2746).
2023-07-05 15:41:51 -07:00
NachoSoto 01ca7ae635 PostReceiptDataOperation: add new testReceiptIdentifier parameter (#2749) 2023-07-05 10:37:43 -07:00
NachoSoto 3f538fc6f9 Trusted Entitlements: update handling of 304 responses (#2698)
### Changes:
- `ETagManager` no longer stores the verification mode
- Created new `VerifiedHTTPResponse`, which has the
`VerificationResult`, extracted from `HTTPResponse`
- Removed `VerificationResult.from(cache:response:)`
- Changed `SignatureVerificationHTTPClientTests` to test using the real
`ETagManager`, so they become more "end to end" unit tests. A lot of
them were relying on implementation details of the mock instead of the
real `ETagManager`.
- Removed several `HTTPClient` tests that were checking a behavior that
was impossible (for example, no verification result despite it being
enabled), or that checked behavior based on the cached
`VerificationResult`
2023-06-30 12:24:53 -07:00
NachoSoto 2fc7725827 PostReceiptDataOperation: clean up cache key (#2628) 2023-06-13 06:33:38 -07:00