npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@encorekit/react-native

v2.0.2

Published

Encore React Native bridge — thin native module layer delegating to encore-swift-sdk (iOS) and encore-android-sdk (Android)

Readme

Encore React Native SDK

Thin React Native bridge to the native Encore iOS SDK and Encore Android SDK. Presents retention offers with native UI and delegates purchases to your existing subscription manager.

Upgrading from 1.x? 2.0 is a breaking change: onPurchaseRequest / completePurchaseRequest / onPurchaseComplete / onPassthrough are gone, and show() returns a different shape. See MIGRATION.md.

Installation

npm install @encorekit/react-native

iOS

cd ios && pod install

Android

No additional setup required — com.encorekit:encore resolves from Maven Central, which is included by default in Android projects. The React Native module auto-links with RN 0.60+.

Quick Start

1. Wrap Your App with EncoreProvider

import { EncoreProvider } from '@encorekit/react-native';
import type { PurchaseController } from '@encorekit/react-native';

// Encore decides *when* a purchase happens; your app decides *how*.
const purchaseController: PurchaseController = async ({ productId }) => {
  const outcome = await RevenueCat.purchase(productId);
  if (outcome.userCancelled) return 'cancelled';
  if (outcome.deferred) return 'pending';
  return 'purchased';
};

export default function App() {
  return (
    <EncoreProvider
      apiKey="your_api_key"
      options={{ logLevel: 'debug' }}
      purchaseController={purchaseController}
    >
      <YourApp />
    </EncoreProvider>
  );
}

The provider calls configure once, on mount, with the controller as part of it — the same shape as Encore.shared.configure(apiKey:purchaseController:options:) on iOS, Encore.configure(context, apiKey, purchaseController = …) on Android and Encore.shared.configure(apiKey: …, purchaseController: …) on Flutter. Binding the controller is configuring, so there is no window in which the SDK is configured and your purchase path is not.

Outside React, configure directly:

await Encore.configure('your_api_key', {
  logLevel: 'debug',
  purchaseController,
});

If you have no in-app purchase in your Encore flows, omit it. The SDK then never attempts a purchase, and every presentation records publisher: 'not_attempted' — the honest record, not a failure.

2. Identify Users

import Encore from '@encorekit/react-native';

// After authentication
Encore.identify('user_123', {
  email: '[email protected]',
  subscriptionTier: 'premium',
});

// On logout
Encore.reset();

3. Show a Placement

const result = await Encore.placement('paywall').show();

if (result.status === 'presented') {
  const converted = result.claim !== null || result.publisher === 'purchased';
  if (converted) grantAccess();
}

To present the post-action reward surface instead of the churn-intervention sheet — a brand-funded reward at a moment the user has just accomplished something — set the use case and supply your own copy:

import Encore, { UseCase } from '@encorekit/react-native';

const result = await Encore.placement('streak_complete')
  .useCase(UseCase.rewardUsers)
  .headline('7 day streak!')
  .subheadline("Here's a little thank you from us")
  .show();

4. Observe Outcomes

onOutcome is the passive observation channel — analytics forwarding, app-owned state, debugging overlays. It mirrors Swift's AsyncStream<PlacementOutcome> and Kotlin's SharedFlow<PlacementOutcome>, and unlike the 1.x on* setters it appends, so several observers can listen at once.

useEffect(
  () =>
    Encore.onOutcome((outcome) => {
      if (outcome.type === 'presentation') {
        analytics.track('encore_presentation', outcome.result);
      } else {
        // A strict-unlock claim verified server-side after its flow ended —
        // possibly on a later launch. Join back via ClaimedOffer.transactionId.
        refreshEntitlements();
      }
    }),
  [],
);

There is no replay: subscribe at startup, or you will miss outcomes emitted before you subscribed. Control flow belongs at the call site, on the value show() returns — the stream is for observing.

Reading a PresentationResult

The result is the factual record of a presentation. Either nothing appeared, or the interaction ran and the record carries two independent funnels plus how it ended.

type PresentationResult =
  | { status: 'not_presented'; reason: NotPresentedReason }
  | {
      status: 'presented';
      advertiser: AdvertiserOutcome;  // Encore's funnel: how far the claim got
      publisher: PublisherOutcome;    // your funnel: what your controller said
      dismissal: DismissReason;       // how the sheet went away
      claim: ClaimedOffer | null;     // shorthand for a claimed/verified offer
    };

There is no SDK-computed "did this unlock" verdict, because what a claim means is a property of the variant flow that served it — any projection over app-global config could contradict the flow that actually ran. Branch on the raw axes instead:

const converted =
  result.status === 'presented' &&
  (result.claim !== null || result.publisher === 'purchased');

The two axes are independent. A user can claim an advertiser offer and decline the purchase; both are recorded. 'not_attempted' is a real value — "the funnel was open and nothing entered it" — never a missing one.

Platform-specific values

The vocabularies are shared and frozen, but a few members only ever come from one platform:

| Value | Only on | |:------|:--------| | unsupported_ios | iOS | | no_foreground, iap_first_declined | Android | | user_cancelled, last_offer_declined, dismissed (dismissal) | iOS |

ClaimedOffer.offerId and .campaignId are both always present. On iOS an offer is a campaign and the two carry the same value; the bridge emits both so one JS shape serves both platforms.

API Reference

configure(apiKey, options?)

Initialize the SDK. Called automatically by EncoreProvider.

| Parameter | Type | Description | |:----------|:-----|:------------| | apiKey | string | Your Encore API key | | options.purchaseController | PurchaseController | Your purchase code — the SDK's single purchase path | | options.logLevel | 'none' \| 'error' \| 'warn' \| 'info' \| 'debug' | Log verbosity (default: 'none') | | options.unlock | 'optimistic' \| 'strict' | How a claim grants access (default: 'optimistic') |

'strict' verifies claims server-side, persisting unverified ones and re-checking across launches; those settle later as a strict_unlock_verified outcome on onOutcome.

The purchase controller

Encore decides when a purchase happens; your app decides how.

type PurchaseController = (
  request: PurchaseRequest,
) => Promise<PurchaseResult> | PurchaseResult;

type PurchaseResult = 'purchased' | 'cancelled' | 'pending';

Return one of the three results, or throw for a real failure — a throw is recorded as publisher: 'failed' with your message, and the flow continues. Map your billing layer's "user cancelled" error to 'cancelled' rather than throwing it.

'pending' is not a failure: an Ask-to-Buy or SCA purchase may settle minutes or days later, and the store webhook is the source of truth. This is the reason the 1.x boolean completePurchaseRequest(success) is gone — a boolean could only report a deferred purchase as a decline.

request carries productId, plus placementId, promoOfferId (iOS) and basePlanId (Android) where they apply. Android's native controller also receives an Activity; the bridge absorbs it, because a React Native host's billing library resolves the current Activity itself.

setPurchaseController(controller)

Replaces the controller bound at configure(). The controller the native SDK holds is the bridge itself and always forwards to the current handler, so a swap is a JS-side concern that takes effect on the next purchase request. Use it when the controller genuinely has to change at runtime — a billing stack that only exists after login, say.

await Encore.setPurchaseController(newController);

Calling it before configure() also still registers a purchase path, which is how 2.0 wired one up. What no bridge can do is add a controller to an SDK that was already configured without one: both native SDKs take it as a configure() argument and Android has no setter, so that call warns and resolves { success: false }. Pass the controller to configure instead.

identify(userId, attributes?)

Associate a user ID with SDK events.

setUserAttributes(attributes)

Merge attributes into the current user profile.

reset()

Clear user data and generate a new anonymous ID. Call on logout. The registered purchase controller survives — it is build-time wiring, not user state.

setClaimEnabled(enabled)

Grays out and disables the claim CTA on offer cards. Runtime-mutable.

placement(placementId)

Returns a PlacementBuilder. Each option returns a new builder, so a builder is safe to hold and branch from.

| Method | Type | Description | |:-------|:-----|:------------| | useCase(useCase) | UseCase | Which surface to present (default: UseCase.reduceChurn) | | headline(text) | string | Overrides the sheet headline, on every use case | | subheadline(text) | string | Overrides the sheet subheadline, on every use case | | show() | — | Presents the sheet. Resolves with PresentationResult; never rejects |

Copy resolves in strict priority: the value you pass here, then the value configured in the Encore portal, then the template default. A blank string is ignored, so the chain falls through rather than rendering an empty line.

headline / subheadline apply to both use cases — the native side writes them into the variable the active template reads: ${customHeadline} / ${customSubheadline} on churn intervention, and the reward template's own equivalents on post-action reward.

Use cases

UseCase names the capability; its values are the wire format the backend stores. The two are deliberately unrelated, so renaming a key never changes what goes over the wire.

| Value | Wire slug | Presents | |:------|:----------|:---------| | UseCase.reduceChurn | 'churn-intervention' | Second-chance monetisation after a paywall decline. The default. | | UseCase.rewardUsers | 'post-action-reward' | A brand-funded reward for something the user just did. Claim-only; never presents IAP. |

These are the same names the iOS (.reduceChurn) and Android (REDUCE_CHURN) SDKs use, so a placement reads the same on all three.

Because UseCase is a const object rather than a TypeScript enum, its type resolves to the wire slugs — .useCase('post-action-reward') still compiles and still sends the same bytes. The capability names are the documented form; the slugs are accepted, not deprecated.

A use case with no enabled variant resolves to { status: 'not_presented', reason: { type: 'use_case_unavailable' } } rather than falling back to the churn-intervention sheet — presenting an in-app purchase at a moment the user was meant to be rewarded is worse than presenting nothing. Enable the use case in the Encore portal to fix it.

onOutcome(handler)

Subscribes to the outcomes stream. Returns an unsubscribe function. Appends rather than replaces — several observers can listen at once.

type PlacementOutcome =
  | { type: 'presentation'; placementId: string; result: PresentationResult }
  | { type: 'strict_unlock_verified'; transactionId: string };

EncoreProvider

React context provider. Props:

| Prop | Type | Description | |:-----|:-----|:------------| | apiKey | string | Your Encore API key | | options | ConfigureOptions | Optional configuration | | purchaseController | PurchaseController | Your purchase code, forwarded into configure() |

useEncoreContext()

Hook returning the full EncoreSDK interface. Must be used within EncoreProvider.

Types

interface UserAttributes {
  email?: string;
  firstName?: string;
  lastName?: string;
  phoneNumber?: string;
  postalCode?: string;
  city?: string;
  state?: string;
  countryCode?: string;
  latitude?: string;
  longitude?: string;
  dateOfBirth?: string;
  gender?: string;
  language?: string;
  subscriptionTier?: string;
  monthsSubscribed?: string;
  billingCycle?: string;
  lastPaymentAmount?: string;
  lastActiveDate?: string;
  totalSessions?: string;
  custom?: Record<string, string>;
}

type PurchaseResult = 'purchased' | 'cancelled' | 'pending';

interface PurchaseRequest {
  productId: string;
  placementId?: string;
  promoOfferId?: string; // iOS
  basePlanId?: string;   // Android
}

interface ClaimedOffer {
  offerId: string;
  campaignId: string;
  advertiserName: string;
  transactionId?: string;
}

interface EncoreErrorInfo {
  type: string; // e.g. 'network_error', 'http_error', 'not_configured'
  message: string;
}

type AdvertiserOutcome =
  | { type: 'not_attempted' }
  | { type: 'claimed'; offer: ClaimedOffer }
  | { type: 'verified'; offer: ClaimedOffer }
  | { type: 'cooldown' }
  | { type: 'failed'; error: EncoreErrorInfo };

type PublisherOutcome =
  | 'not_attempted'
  | 'purchased'
  | 'cancelled'
  | 'pending'
  | 'failed';

type DismissReason =
  | 'close_button'
  | 'swipe_dismiss'
  | 'user_cancelled'
  | 'last_offer_declined'
  | 'dismissed'
  | 'provisional_cooldown'
  | 'flow_completed'
  | 'interrupted';

type NotPresentedReason =
  | {
      type:
        | 'not_configured'
        | 'already_presenting'
        | 'unsupported_ios'
        | 'no_offer_available'
        | 'experiment_control'
        | 'use_case_unavailable'
        | 'no_foreground'
        | 'iap_first_declined';
    }
  | { type: 'error'; error: EncoreErrorInfo };

type PresentationResult =
  | { status: 'not_presented'; reason: NotPresentedReason }
  | {
      status: 'presented';
      advertiser: AdvertiserOutcome;
      publisher: PublisherOutcome;
      dismissal: DismissReason;
      claim: ClaimedOffer | null;
    };

// Keys name the capability; values are the frozen wire format.
const UseCase = {
  reduceChurn: 'churn-intervention',
  rewardUsers: 'post-action-reward',
} as const;

type UseCase = (typeof UseCase)[keyof typeof UseCase];

interface PlacementOptions {
  useCase?: UseCase;
  headline?: string;
  subheadline?: string;
}

Requirements

  • React Native 0.60+
  • iOS 15.0+
  • Android API 26+
  • Node 16+

Note on Entitlements

Entitlement tracking is handled by the native SDKs and your subscription manager. The React Native bridge does not expose entitlement query methods directly — use your subscription manager's React Native SDK (RevenueCat, Adapty, etc.) for entitlement checks.

License

MIT