feat: bridge getCustomerInfo() and customerInfoDidChange from the native SDKs - #227
Merged
Merged
Conversation
…ive SDKs
- getCustomerInfo() resolves subscription/non-subscription transactions and
entitlements, waiting for real (non-placeholder) data on both platforms
- customerInfoDidChange event with {from, to}, exposed via useSuperwallEvents
(onCustomerInfoChange) and the compat SuperwallDelegate
- reactive customerInfo on useSuperwall/useUser: seeded post-configure,
cleared and reseeded across identify/reset, synced via the change event
- example app: customer-info screen demoing snapshot + live change events
- changeset (minor)
Notes:
- customer info writes intentionally use the same unguarded fetch-then-set
pattern as the existing user/subscriptionStatus writers; a stale in-flight
fetch overwriting newer state is accepted as negligible and self-corrects
via customerInfoDidChange. A shared ordering guard would be a natural
follow-up (see TODO in useSuperwall.ts re: event-driven user state).
- unlike user/subscriptionStatus (awaited refetch), the reseed can't block
identify(), so the snapshot is cleared to null for the transition instead
of overwritten in place.
- entitlements keep Expo's reduced {id, type} shape; enrichment (dates,
productIds, store, state) is a separable follow-up.
expo-asset is ESM; jest can't require() it below Node 24.9 (active LTS is 22).
commit: |
Contributor
|
Thanks for the PR @Davedeji , looks good, merging it in for the upcoming release! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bridges the native SDKs' customer info surface into Expo. Both SuperwallKit (4.10.0+) and Superwall Android (2.6.6+) expose
getCustomerInfo()and acustomerInfoDidChangedelegate callback, and both are already present in the wrapper's pinned versions (iOS 4.16.1 / Android 2.7.24) — but neither was wired up in the bridge. Expo apps could see coarse subscription status, but couldn't read the customer's purchase history or entitlements on demand, or react when they change.Note: an analytics event named
customerInfoDidChangealready existed (#158) — that only reports that the event occurred. This PR wires the actual delegate callback with{ from, to }snapshots, plus the getter.What's included
getCustomerInfo()— resolves the customer's subscription transactions, non-subscription transactions and entitlements. Waits for real (non-placeholder) data on both platforms: iOS via the native async getter; Android by suspending on thecustomerInfoStateFlow until it differs fromCustomerInfo.empty()(isPlaceholderis internal to both native SDKs, but it participates in Android's data-class equality, so this distinguishes "not loaded" from "no purchases").customerInfoDidChange— new bridge event emitting{ from, to }snapshots from both delegate bridges. Exposed asonCustomerInfoChangeinuseSuperwallEvents(with the same buffer-and-replay semantics as the other delegate events) and ascustomerInfoDidChangeon the compatSuperwallDelegate.customerInfostate onuseSuperwall/useUser— seeded non-blocking afterconfigure()(the change event never fires for the initial value), cleared and reseeded acrossidentify/resetso one identity's purchases can't leak into the next, and kept current via the change event.Superwall.shared.getCustomerInfo()returning the existingCustomerInfoclass.CustomerInfo/SubscriptionTransaction/NonSubscriptionTransaction, mirroring the key names and shapes of the existing Android serializer (json/CustomerInfo.kt) one-to-one.customer-infoscreen showing the seeded snapshot, manual refresh, and a live change-event log.Design notes
{ id, type }shape. The native models carry much more (dates, productIds, store, renewal state); enriching the sharedEntitlementtype touches every existing API that returns one, so it's left as a separable follow-up.user/subscriptionStatus(whose refetches are awaited insideidentify), the customer-info reseed shouldn't blockidentify()— the native getter waits on receipt processing — so the snapshot is cleared tonullfor the transition and reseeded after.user/subscriptionStatus). A stale in-flight fetch overwriting newer state is accepted as negligible and will self-correct on the nextcustomerInfoDidChange. A shared ordering guard across all three would be a natural follow-up (the pre-existing TODO inuseSuperwall.tsabout event-driven user state calls out something similar).offerTypevalues differ by platform (iOS: trial/code/promotional/winback; Android adds subscription/revoked viaLatestPeriodType) — inherited from the native SDKs and documented on the TS type rather than normalized, since the Android serializer already ships these strings insidePaywallInfo.customerInfo.How I verified it
useSuperwallEvents, and identify clearing/reseeding). TypeScript build and Biome clean.gradlew clean+assembleDebug; the module Kotlin compiles against Superwall Android 2.7.24.customerInfoDidChangefires across the bridge — the Customer Info screen updates in place toSubscriptions (1)with the transaction's productId, active/renewing state, expiration date and store, and logs thesubs 0 → 1transition, with no manual refresh. (Used my own project key and placement for demo, added StoreKit config file to show the App Store products)getCustomerInfo()on load, manual refresh works, and toggling an entitlement in Superwall's Test Mode firescustomerInfoDidChange(entitlements 1 → 1) through the same path.0-ios-demo.mp4
Note on the second commit
test: mock expo-asset so the suite loads on Node < 24.9— the test suite importsSuperwallProvider, which transitively imports the ESMexpo-asset; jest can'trequire()ESM below Node 24.9, so the suite fails to load on Node 22 (active LTS). The mock makes the suite hermetic; nothing in it exercises real asset resolution. Splitinto its own commit so it's independently droppable.