v1.74.0
Added
-
OnboardingStudio— the SDK's front door, in the shape of the SDKs it sits
alongside (Superwall.configure,Purchases.configure,amplitude.init): one
module-level object owning configuration and user identity.OnboardingStudio.init({ projectId: "…", appVersion: "1.0.0" }); // returns the client OnboardingStudio.setUserProperty("plan", "free"); OnboardingStudio.setUserProperties({ daysSinceInstall: 3 }); // merges OnboardingStudio.setUserProperty("plan", null); // deletes OnboardingStudio.removeUserProperty("plan"); OnboardingStudio.getUserProperties(); OnboardingStudio.reset(); // forget the user OnboardingStudio.getClient() / isInitialized(); const { properties, status } = useUserProperties(); // React read path
initis idempotent for an unchanged config — Fast Refresh re-runs module
scope, and rebuilding the client there would orphan the one the providers
already hold. A genuinely changed config replaces the client and warns.reset()clears user properties, in memory and on disk, and deliberately
leaves the configuration and the payload cache alone: logging out should forget
who someone is, not force a refetch of content that has not changed.
getClient()?.clearCache()is there for both.User properties feed audience resolution for both onboardings and paywalls.
Values arestring | number | boolean; they persist to AsyncStorage and are
hydrated before the first fetch, so a returning user is targeted correctly
on the first launch-frame with no host code. A first-ever install has nothing
to hydrate — seed it withinit({ …, userProperties: { plan: "free" } }),
which runs before anything renders, and even that launch is targeted correctly.register/presentdeliberately do not live on this object, unlike
Superwall'sregister: presenting needs the mounted provider's catalog and
presentation state, so they stay onusePaywall(), where a call cannot be made
before a provider exists. -
clientis now optional on both providers. Omit it and they use the client
init()built; pass one and it still wins, so every existing host is
unaffected. With neither, the two providers behave differently on purpose:
OnboardingProviderthrows (an onboarding with no client has nothing to
render, and a hostErrorBoundarycatches one screen) whilePaywallProvider
warns and renders its children with paywalls inert — it wraps the whole
app, so throwing would take down every screen over a missing paywall client.Eight names are refused with a warning —
projectId,platform,appVersion,
draft,locale,omitNulls,moment,now. The last two are server-owned;
the other six would break the request outright, because the client appends
user params before its own,URLSearchParamspermits duplicates, and the two
server-side readers disagree about which wins (.get()takes the first — the
user's value — whileObject.fromEntriestakes the last). -
register(moment, feature)onusePaywall()— gate a feature on a moment.
Runs the feature immediately when the moment has no paywall, otherwise presents
it and runs the feature only on a purchase. Resolves
{ ran, presented, reason, outcome? }.It gates on the moment alone — there is no entitlement check. Exclude
existing subscribers with a user property plus an audience filter.It fails open: with no reachable catalog it runs the feature and warns,
because failing closed would make gated features silently dead on an offline
launch.reason: "catalog-unavailable"is how a host measures that rate.A Stripe-billed paywall never runs the feature even on a successful
checkout — a Payment Link's entitlement arrives out-of-band through RevenueCat,
so the presentation never reports"purchased".registerwarns when it
presents one. -
registerTimeoutMsonPaywallProvider(default3000) — how long
registerwaits for the catalog to settle before deciding without it. -
resolveRegisterDecision/shouldRunFeatureare exported: they are pure,
so a host building its own gating oncatalogcan reuse the SDK's exact rules
rather than reimplement them slightly differently.
Changed
- Both providers now merge the store over
customAudienceParams, store-wins
per key, and hold their query until the store hydrates. The prop is neither
deprecated nor removed — it becomes the static baseline (build-time facts)
while the store carries what changes at runtime, so existing hosts are
untouched. One consequence worth naming: one store now feeds both
waterfalls, so an onboarding audience and a paywall audience can no longer
disagree about the same user, which two independent props always allowed.
Fixed
-
The AsyncStorage cache keys are now scoped by audience params. The
react-query key always was; the disk key was a bare constant, so a cache-first
read could serve a payload resolved under different params — non-null, and so
indistinguishable from a correct one. Observed in production: an audience gated
onhoursSinceOnboardingPaywall >= 44was served the pre-threshold catalog on
the launch where the user first became eligible, so the arm under test lost
exactly the launch that mattered. Rare while params were a static prop;
mutable properties would have made it the normal path.An empty params hash yields the legacy key byte-for-byte, so existing installs
keep their cache. This is also what makescatalogStatus: "revalidating"
trustworthy — a served catalog now always matches the current params — which
register's decision relies on. -
clearCache()now clears every params variant, via agetAllKeys()prefix
scan rather than naming two keys it can no longer predict. It previously missed
every key but the current one. It deliberately does not clear user
properties: clearing a payload cache must not forget who the user is.