Commit Graph
10 Commits
Author SHA1 Message Date
Rick 8b024e8009 feat(remote-config): remove the remote config session kill switch (#7524)
* Remove remote config kill switch

# Conflicts:
#	Sources/Checkpoints/CheckpointWorkflowResolver.swift
#	Tests/UnitTests/Checkpoints/CheckpointWorkflowResolverTests.swift

* Improve remote config retry tests

* feat(remote-config): stop decoding offering paywall components

* test(remote-config): align workflow integration test naming

* refactor(remote-config): limit offerings preview APIs to debug builds

* refactor(remote-config): address review cleanup

* fix(remote-config): propagate workflow fetch failures

* refactor(remote-config): derive paywall components from decoding mode

* Remove offerings paywall component decoding

* Update offerings component decoding tests

* Fix CI test fixture compilation

* Fix offerings initializer argument order

* Preserve default offerings component marker
2026-09-08 07:06:04 +02:00
Rick c97d690c90 fix(Checkpoints): Fail rule resolution when the app user changes (#7554)
* fix: fail rule resolution when customer changes

* refactor: use app user terminology

* fix: defer stale audience evaluation errors

* docs: clarify audience evaluation failure

* test(Checkpoints): provide app user ID to rule evaluator
2026-09-07 09:25:32 +02:00
Rick 3415962003 feat(Checkpoints): consume canonical audience configuration (#7543)
* feat(Checkpoints): consume canonical audience configuration

* feat(Checkpoints): support typed backend audience values

* fix(Checkpoints): retry stale checkpoint resolution

* fix(Checkpoints): isolate malformed audience entries

* fix(Checkpoints): align audience identifier handling

* fix(Checkpoints): log unsupported backend audience values

* test(Checkpoints): inline canonical audience fixtures

* feat(Checkpoints): evaluate backend audience predicates

* fix(Checkpoints): bound audience resolution retries

* refactor(Checkpoints): share audience value conversion

* fix(Checkpoints): tighten configuration consistency checks

* test(Checkpoints): format dimension value fixture

* test(Checkpoints): fix null dimension conversion expectation
2026-09-07 07:45:27 +02:00
Rick 967cc710e0 feat(Checkpoints): refine result and listener APIs (#7526)
* feat(Checkpoints): add web checkout outcome

* docs(Checkpoints): align documentation with Android

* refactor(Checkpoints): introduce listener contexts

* refactor(Checkpoints): align resolution behavior with Android

* Refine checkpoint paywall outcome names

* Remove checkpoint model equality conformances
2026-09-01 16:29:11 +02:00
Facundo MenzellaandClaude Opus 5 4b95788014 Evaluate checkpoint audiences 2/2 (#7426)
* Deserialize audience rules for rules engine evaluation

The rules engine takes a predicate as a string, so decoding the audience
as an opaque blob would only mean serializing it back before every
evaluation. Decode `id` and keep `rules` as the JSON string the engine
takes.

Keys are sorted on the way out because Swift dictionaries have no order
of their own, and an unstable predicate string would be a moving target
for anything that caches or compares it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UND779kHH6TRc2662wzaj1

* Evaluate checkpoint audiences

Checkpoint rules each target an audience, but resolution took the first
served rule regardless. Resolve each rule's audience through the
`audiences` topic and evaluate its predicate, so the first rule whose
audience the customer matches wins.

Predicates resolve one rule at a time, so a rule after the match never
pays for a lookup. An audience the SDK couldn't read leaves the outcome
unknown, so it reports `configurationUnavailable` rather than `noMatch`,
and can't let a lower-priority rule win instead.

Also starts honoring `CheckpointParams.customVariables`, which the
resolver dropped until now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UND779kHH6TRc2662wzaj1

* Cover a malformed audience falling through to a later rule

Ports the one case purchases-android#3971 tests that iOS did not: a
predicate the engine can't run is an evaluation failure, not a lookup
failure, so a lower-priority rule can still win on its own merits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UND779kHH6TRc2662wzaj1

* Cover served audience payload shapes and the terminal-offering gate

Decodes through the real Audience decoder so the predicate under test is
what a served payload produces: empty rules match everyone (the engine
only dispatches single-key objects, and every object is truthy), the
backend's always-false sentinel resolves to a clean non-match, and a
predicate on a variable no provider supplies is false rather than an
error.

Also covers that the audience gate runs before the workflow body picks
between presenting and handing back an offering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UND779kHH6TRc2662wzaj1

* Read audiences from the topic item instead of a blob

The audiences topic serves each audience inline in its config item, with
no blob_ref, so the blob read found nothing and every lookup returned
nil. With audience evaluation in place that surfaced as
configurationUnavailable for every checkpoint, so nothing presented.

Read the item's own content first and keep the blob read as a fallback
for any project still served the earlier shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UND779kHH6TRc2662wzaj1

* Pin that a negated unsupplied variable matches everyone

An unresolvable path does not fail closed. Under negation it inverts:
null == "NL" is false, so ! makes it true and the audience matches every
customer, including the ones it was written to exclude.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UND779kHH6TRc2662wzaj1

* Read audiences from item metadata only, matching Android

khepri publishes each audience as item metadata with blob_contents=None
and clear_blob=True, so the blob path is deliberately dead rather than
transitional. Drop the fallback and log the raw metadata before decoding,
matching purchases-android#3971.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UND779kHH6TRc2662wzaj1

* Log audience metadata as JSON instead of parsed values

The interpolation happened at the call site, so the dictionary was
stringified even with debug logging off, and it printed AnyDecodable
case names rather than anything a human wants to read. Reuse the JSON
already built for decoding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UND779kHH6TRc2662wzaj1

* Check config staleness before resolving the matched rule

Audiences are read from their own topic at lookup time, so they are not
covered by the rule snapshot's generation. A refresh landing mid-walk
meant the match was built from config that no longer existed, and the
existing check after resolve() could only discard the result: the
workflow fetch, the offerings load and the asset prewarm had already
happened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VrDQrA37Fr5XZvpT1KvD8D

* Stop the rule walk when the task is cancelled

Neither the audience lookup nor RulesEngine.evaluate throws on
cancellation, so a cancelled resolution kept decoding audiences and
evaluating predicates for every remaining rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VrDQrA37Fr5XZvpT1KvD8D

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 12:13:01 +00:00
Facundo MenzellaandClaude Opus 5 1064faafd8 feat(checkpoints): Support headless offering checkpoints (#7405)
* feat(checkpoints): Support headless offering checkpoints

Port of purchases-android #3951. When a matched checkpoint rule's workflow
is a single terminal `offering` step, resolve it to that offering and hand
it to the app instead of presenting anything.

The workflow body is now read before the offering mapping, since its shape
decides what else the rule needs. An unservable rule still resolves to
`configurationUnavailable` rather than falling through to a later rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPNip3cub65pNgGuNbU8pf

* refactor(checkpoints): Realign headless offerings with android 1c579c7

Follows purchases-android#3951 after review there settled its shape:

- rename the resolution cases to matchedWorkflow / matchedOffering
- route on the initial step's type instead of "any offering step", and
  reject a UI workflow that contains one
- drop the screen / trigger / trigger-action guards, an offering step
  renders nothing so anything else it carries is ignored

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPNip3cub65pNgGuNbU8pf

* fix(checkpoints): Don't prewarm assets for headless offering checkpoints

Prewarming takes its fonts from ui_config rather than from the workflow's
screens, so a screenless offering workflow was still downloading every
downloadable app font on a path that renders nothing. Splits the
non-prewarming read out of getWorkflow and prewarms only on the branch
that goes on to present, matching purchases-android.

Also folds the duplicated offerings lookup into one helper, flattens the
step-type branching, and reuses String.isNotEmpty (which trims newlines
too, unlike the hand-rolled check it replaces).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPNip3cub65pNgGuNbU8pf

* test(checkpoints): Skip prewarming assertions below iOS 15

scheduleAssetPrewarming is behind an availability guard, so on the older
OS versions CI runs the positive assertion would fail and the negative
one would pass for the wrong reason. Matches the existing skip in
WorkflowManagerTests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPNip3cub65pNgGuNbU8pf

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 14:16:29 +02:00
Rick dc636e5417 feat(checkpoints): evaluate checkpoint audience rules locally (#7401) 2026-08-14 17:40:52 +02:00
Rick ace2c161ac feat(checkpoints): Resolve checkpoints from remote configuration (#7385)
* feat(checkpoints): Resolve checkpoints from remote configuration

* fix(checkpoints): Guard resolution config generation

* fix(checkpoints): Distinguish unknown checkpoints

* fix(checkpoints): Report unavailable remote config

* fix(checkpoints): Propagate resolution cancellation

* Use stable unknown checkpoint identifier
2026-08-12 13:15:13 +02:00
Rick 1bf0f27c20 fix(checkpoints): Refine unknown checkpoint and workflow resolution (#7384) 2026-08-07 15:46:00 +02:00
Rick beebc52e06 feat(checkpoints): core engine (#7365)
* Implement checkpoint core engine

* Align checkpoint resolver with Android

* Decouple checkpoint engine from public API

* Keep checkpoint resolver within lint limits

* Forward checkpoint presenter to core manager

* Test checkpoints without offering metadata

* Organize checkpoint workflow resolvers

* Address checkpoint core review feedback

* Clarify checkpoint resolver dependencies

* Address remaining checkpoint core feedback

* Suppress internal checkpoint offerings diagnostics

* Centralize checkpoint no-action reason values

* Preserve offerings for checkpoint workflows
2026-08-07 10:02:33 +02:00