Skip to content

feat: bridge getCustomerInfo() and customerInfoDidChange from the native SDKs - #227

Merged
ianrumac merged 2 commits into
superwall:mainfrom
Davedeji:feat/customer-info-bridge
Sep 13, 2026
Merged

ianrumac merged 2 commits into
superwall:mainfrom
Davedeji:feat/customer-info-bridge

Conversation

@Davedeji

@Davedeji Davedeji commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Bridges the native SDKs' customer info surface into Expo. Both SuperwallKit (4.10.0+) and Superwall Android (2.6.6+) expose getCustomerInfo() and a customerInfoDidChange delegate 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 customerInfoDidChange already 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 the customerInfo StateFlow until it differs from CustomerInfo.empty() (isPlaceholder is 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 as onCustomerInfoChange in useSuperwallEvents (with the same buffer-and-replay semantics as the other delegate events) and as customerInfoDidChange on the compat SuperwallDelegate.
  • Reactive customerInfo state on useSuperwall/useUser — seeded non-blocking after configure() (the change event never fires for the initial value), cleared and reseeded across identify/reset so one identity's purchases can't leak into the next, and kept current via the change event.
  • Compat SDK: Superwall.shared.getCustomerInfo() returning the existing CustomerInfo class.
  • iOS serializers for CustomerInfo/SubscriptionTransaction/NonSubscriptionTransaction, mirroring the key names and shapes of the existing Android serializer (json/CustomerInfo.kt) one-to-one.
  • Example app: a customer-info screen showing the seeded snapshot, manual refresh, and a live change-event log.
  • Changeset (minor).

Design notes

  • Entitlements keep Expo's reduced { id, type } shape. The native models carry much more (dates, productIds, store, renewal state); enriching the shared Entitlement type touches every existing API that returns one, so it's left as a separable follow-up.
  • Identity transitions clear rather than overwrite. Unlike user/subscriptionStatus (whose refetches are awaited inside identify), the customer-info reseed shouldn't block identify() — the native getter waits on receipt processing — so the snapshot is cleared to null for the transition and reseeded after.
  • Writes use the store's existing unguarded fetch-then-set pattern (same as user/subscriptionStatus). A stale in-flight fetch overwriting newer state is accepted as negligible and will self-correct on the next customerInfoDidChange. A shared ordering guard across all three would be a natural follow-up (the pre-existing TODO in useSuperwall.ts about event-driven user state calls out something similar).
  • offerType values differ by platform (iOS: trial/code/promotional/winback; Android adds subscription/revoked via LatestPeriodType) — inherited from the native SDKs and documented on the TS type rather than normalized, since the Android serializer already ships these strings inside PaywallInfo.customerInfo.

How I verified it

  • Tests: 14 jest tests pass (3 new: configure-seeding + event-driven store updates, event routing with buffer/replay through useSuperwallEvents, and identify clearing/reseeding). TypeScript build and Biome clean.
  • Builds: full example iOS app builds from a clean DerivedData (Xcode, simulator); Android example builds via gradlew clean + assembleDebug; the module Kotlin compiles against Superwall Android 2.7.24.
  • End-to-end on iOS (simulator): with a local StoreKit configuration file, the example's paywall renders live prices, a StoreKit Test purchase completes, and customerInfoDidChange fires across the bridge — the Customer Info screen updates in place to Subscriptions (1) with the transaction's productId, active/renewing state, expiration date and store, and logs the subs 0 → 1 transition, with no manual refresh. (Used my own project key and placement for demo, added StoreKit config file to show the App Store products)
  • End-to-end on Android (emulator): the screen seeds via getCustomerInfo() on load, manual refresh works, and toggling an entitlement in Superwall's Test Mode fires customerInfoDidChange (entitlements 1 → 1) through the same path.
0-ios-demo.mp4
4-customerinfo-updated-live 5-android-customerinfo

Note on the second commit

test: mock expo-asset so the suite loads on Node < 24.9 — the test suite imports SuperwallProvider, which transitively imports the ESM expo-asset; jest can't
require() 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. Split
into its own commit so it's independently droppable.

…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).
@pkg-pr-new

pkg-pr-new Bot commented Sep 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/expo-superwall@227

commit: dbef510

@ianrumac

Copy link
Copy Markdown
Contributor

Thanks for the PR @Davedeji , looks good, merging it in for the upcoming release!

@ianrumac ianrumac closed this Sep 13, 2026
@ianrumac ianrumac reopened this Sep 13, 2026
@ianrumac
ianrumac merged commit 09aabea into superwall:main Sep 13, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants