feat(checkpoints): Add StoreDimensionProvider with store.country (#3969)
### Description
Adds the store as a third dimension source, with one dimension for now:
`store.country`.
Note that what we get in Android is a different code than in iOS. For
example, we get the 2-letter `US` instead of the 3-letter `USA` as in
iOS. In this PR, I try to mimick the format in iOS, so we can
consolidate the rules.
It fetches through `awaitStorefrontCountryCode` rather than reading the
cached property, so the dimension is available on the first checkpoint
of a session instead of missing until the billing client happens to
connect. A store that can't answer contributes nothing instead of
failing the snapshot — that is a routine outcome (the Galaxy Store never
answers), while a failure would abort the evaluation and take an
otherwise resolvable checkpoint down with it.
Similar to RevenueCat/purchases-ios#7399.
Also fixes `RulesDimensionResolver` nesting an empty object for a source
with nothing to contribute. An empty object is truthy in JSON Logic, so
`{"var": "store"}` would have read as present for a customer whose store
never answered — reachable for the first time now that a provider
routinely returns nothing.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes checkpoint rule evaluation semantics (new dimension and
absent-vs-empty namespace behavior) and adds an async storefront fetch
on the evaluation path, though store failures are isolated and do not
abort snapshots.
>
> **Overview**
> Checkpoint **local rules** can now read **`store.country`** (ISO
3166-1 alpha-3, e.g. `USA`) so predicates can align with iOS. Android
alpha-2 storefront codes are converted at evaluation time; unmapped or
invalid codes are omitted.
>
> **`StoreDimensionProvider`** is registered on
**`LocalRulesEvaluator`** and resolves the country via
**`awaitStorefrontCountryCode()`** on each evaluation (not a cached
snapshot), so the first checkpoint in a session can still get a value
once the store responds. Store errors, missing answers, or teardown
mid-evaluation yield **no store namespace** instead of failing the whole
dimension snapshot.
>
> **`RulesDimensionResolver`** no longer nests an empty object when a
source has nothing to contribute (including empty **custom** variables).
Empty objects are truthy in JSON Logic, so absent namespaces correctly
mean “store/custom not present” for predicates like `{"var": "store"}`.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ee6973ec9a534cf6caa670171c703c2fe35f0cbc. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> T
Toni Rico committed
9eae1c1836abb20034b326be50c8f53ec6f0b4a2
Parent: 243a162
Committed by GitHub <noreply@github.com>
on 8/14/2026, 1:22:06 PM