⚠️ Demonstration mock — react-native-encore-mock is not the real Encore SDK and will be deleted after the demo.
Architecture

Architecture

There are two layers: the package (the mock + offer UI) and your app’s wiring (a thin indirection that picks mock or real).

Inside react-native-encore-mock

react-native-encore-mock
├── createMockEncore(config?)   # the in-memory EncoreClient + offer state machine
├── MockEncoreProvider          # hosts the offer UI (retention sheet + sponsored carousel)
├── useEncoreCallbacks(...)     # registers onPurchaseRequest / onPassthrough / onPurchaseComplete
├── isGranted(result)           # granted | completed → true
├── DEFAULT_OFFERS, DEFAULT_PARTNERS
└── types                       # EncoreClient, PlacementResult, MockOffer, PartnerTrial, …

Your app’s wiring

A consuming app keeps a tiny indirection layer. Hotspot Havoc looks like this:

src/lib/
├── encoreConfig.js     # ENCORE_API_KEY, USE_MOCK, Placements
├── offers.js           # this app's offers (pulls in DEFAULT_PARTNERS)
├── encore.js           # THE indirection: createMockEncore() vs real SDK
├── billingService.js   # YOUR billing (RevenueCat / IAP stand-in)
└── encoreSdkStub.js    # build-time stub so the dormant real-SDK import resolves
metro.config.js         # aliases @tryencorekit/react-native → stub in mock mode
App.js                  # <MockEncoreProvider> + useEncoreCallbacks

The one rule

Everything imports Encore from one indirection file — never the package or the real SDK directly. That file decides mock vs real:

src/lib/encore.js
const mockClient = createMockEncore({ offers: GAME_OFFERS });
const Encore = USE_MOCK ? mockClient : realEncore;
export default Encore;

Flipping USE_MOCK re-points the whole app — no call-site edits. See Mock vs Live Mode.

Who owns entitlement state?

Your app does — not Encore. Encore presents offers and reports results; your app records the outcome. Keep entitlement in your own store (or derive it from your billing provider/backend) and update it from placement().show() results + onPurchaseComplete. This is why the same code works identically in mock and live mode.

Data flow

show() resolves with a PlacementResult (the real Encore shape):

interface PlacementResult {
  status: 'granted' | 'completed' | 'not_granted' | 'dismissed' | 'no_offers';
  reason?: string;
  entitlement?: string;
  offerId?: string;
  campaignId?: string;
}

The two channels carry complementary information:

  • show() resolves with result.status — the screen’s primary signal. The isGranted(result) helper is true for granted (a completed paid purchase) or completed (an accepted brand-sponsored offer).
  • onPassthrough fires on dismissal so you can resume the user’s original action (finalize a cancellation, close a paywall) and never trap them behind the SDK.