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.4.0

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+.

Expo

Encore ships native code, so it needs a development build (npx expo run:ios / npx expo run:android or EAS Build). In Expo Go the native module is absent: importing the SDK logs one warning and every call resolves its failure value ({ success: false }, not_presented from show(), and nothing from setLanguage, which still rejects a malformed tag) instead of crashing your app.

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();

Language

Encore copy follows the device language with no setup: the SDK sends the device's preferred languages, in order, with every request. Your dashboard copy is served in the first of them that your app has enabled, or in your app's default language if none is.

If your app has its own language picker, pass the choice to setLanguage. It takes a BCP 47 tag ('fr', 'fr-CA', 'pt-BR'); only its language part picks the copy. It is tried before the device languages. If your app has not enabled it, Encore moves on to the device languages, then your default. The choice is a device preference, not user state: it is kept on the device across launches and through reset() (logout), and is never stored on Encore's servers. Only clearLanguage() removes it.

await Encore.setLanguage('pt-BR');

// Back to following the device language
await Encore.clearLanguage();

setLanguage rejects with a RangeError, without reaching the native SDK, when the tag is malformed ('pt_BR', 'english'), so a typo shows up at the call site. Extended language tags such as 'zh-yue-HK' are rejected too: Encore reads a tag as language or language-region, so it would serve that one as Mandarin. Pass 'yue-HK'.

The language attribute on identify / setUserAttributes is deprecated. It still works: the native SDK forwards it to setLanguage. It is removed at the next major version, so move to setLanguage.

Copy you pass at runtime through .headline() / .subheadline() is shown exactly as you pass it, so translate it yourself.

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();

Reward for trying

You can also describe your own prize for trying a partner offer. On a reward-for-trying variant the sheet promises the prize up front and, when the user comes back from the advertiser, asks whether they finished. To give the prize, check result.claim?.userConfirmedCompletion:

// Once, when the user signs in. Anonymous users are never promised a prize.
await Encore.identify(user.id);

const result = await Encore.placement('try_an_offer')
  .useCase(UseCase.rewardUsers)
  .publisherReward({ id: 'coins_100', title: '100 coins', detail: 'Added to your wallet' })
  .show();

if (result.status === 'presented' && result.claim?.userConfirmedCompletion) {
  // transactionId can be undefined, so dedupe on your user id and the campaign.
  grantCoins(100, { dedupeKey: `${user.id}:${result.claim.campaignId}` });
}

userConfirmedCompletion is the user's own word, not a verified conversion. It is true only when the user tapped "Yes, I completed it" and Encore's server granted the prize, so treat it like any other self-reported action: fine for coins, streak saves or a few days of Premium, not for anything you could not afford to give away. Passing a prize is your commitment to pay it when the flag is true. Every other variant ignores the prize, so it is safe to pass on any reward placement.

  • identify() is required. The server keeps the count per identified user, across devices. A user you have not identified (or one after reset()) still gets the question, but never the promise or the flag.
  • The server decides the prize. Every "Yes" is sent to Encore, and the first "Yes" per user per offer earns it, within a daily limit of dailyLimit prizes per user in any 24 hours (default 3; the native SDK clamps it to 1 to 10). Once the limit is reached, the sheet shows no promise and the flag stays false.
  • An offer is hidden after a "Yes". The server stops serving it to that user on every device, so the promise never shows on an offer they already earned.
  • Grant from the show() result only. The same result, flag included, is also delivered to onOutcome as a presentation outcome. Granting from both pays twice, so treat onOutcome as a notification.
  • Pay on verification. If your app's reward settings in the Encore portal pay on verification, a "Yes" never sets the flag. Credit the reward from the offer_completed webhook instead, once per transactionId, and not from the flag. In the default mode, grant from the flag and not from the webhook.

Never grant on the promise having been shown. To change the daily limit:

await Encore.setPublisherRewardPolicy({ dailyLimit: 5 });

Only the prize id leaves the device, as publisher_reward_id on analytics. The title, detail and icon are rendered locally.

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') | | options.transactionLinking | boolean | Links purchases for lift measurement (default: true) |

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

transactionLinking: false stops the SDK from reading the store's purchases (StoreKit history on iOS, Play's active purchases on Android) and from sending their transaction ids or purchase tokens to Encore. Encore-attributed claims and purchases still report, but lift against a control group cannot be measured for your app. app_account_id still accompanies analytics events.

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. A key you omit keeps its stored value. The language attribute is deprecated; use setLanguage(tag).

setLanguage(tag)

Serve Encore copy in tag's language instead of the device language. tag is a BCP 47 language tag ('fr', 'pt-BR', 'zh-Hant-TW'). Resolves once the native SDK has it, which keeps it on the device across launches and through reset(); only clearLanguage() removes it. Rejects with a RangeError (a TypeError for a non-string) before crossing the bridge when the tag is malformed. A native failure is logged and resolves, like every other call. Mirrors Encore.shared.setLanguage(_:) on iOS and Encore.shared.setLanguage(tag) on Android.

clearLanguage()

Remove a language set with setLanguage (or the deprecated language attribute), so Encore copy follows the device language again. Keeps the user and every other attribute, and refetches copy only when a language was set. Mirrors Encore.shared.clearLanguage() on iOS and Android.

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. So does a language set with setLanguage, which is a device preference.

setClaimEnabled(enabled)

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

setPublisherRewardPolicy(policy)

Sets the daily limit on the reward-for-trying prize. policy.dailyLimit is prizes per identified user in any 24 hours: default 3, and a fraction is rounded down. The range (1 to 10) is the native SDK's and it clamps there, so a newer native can widen it. Encore's server enforces the limit per user, and each offer earns the prize once per user, ever (see Reward for trying). Runtime-mutable, and it survives reset(). A dailyLimit that is not a finite number is rejected with a warning and resolves { success: false }.

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 | | publisherReward(reward) | PublisherReward | Your prize for trying an offer. Used only by a reward-for-trying variant; see Reward for trying | | 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.

<EncoreTrigger>

An inline Encore trigger: a small entry point that opens the Encore sheet, drawn by the native SDK. Every trigger goes through this one host: it wraps the native EncoreTriggerView on iOS and Android, so the layout, analytics, taps and any automatic open all run natively.

import { EncoreTrigger } from '@encorekit/react-native';

<EncoreTrigger id="giftsButton" placement="home" appearance={{ accentColor: '#5671FF' }} />

| Prop | Type | Description | |:-----|:-----|:------------| | id | string | The trigger to show: 'giftsButton' or 'claimReminder' | | placement | string | The placement id the trigger and its sheet are served under | | useCase | UseCase | Optional. The sheet the trigger opens. Defaults to UseCase.rewardUsers. UseCase.reduceChurn renders nothing | | autoOpen | AutoOpen | Optional. AutoOpen.off keeps a trigger that can open its sheet by itself (the Gifts button) to taps only. Unset, Encore decides per served look; off always wins. The retired 'on' and 'experiment' are ignored with a warning (Encore decides); any other value is read as off, with a warning | | icon | TriggerIcon | Optional. The icon the look draws: TriggerIcon.gift (default), appIcon (your app's store icon from the Encore portal, else the gift) or none. Changing it starts the trigger over | | alignment | EncoreAlignment | Optional. Where the look sits when the served look is narrower than the trigger: EncoreAlignment.leading, center (default) or trailing. The trigger itself still fills the width it is given. A change moves it in place. An unknown value is ignored with a warning | | headline | string | Optional. Your headline for the look, in place of its own. Unset or blank, the look shows the text set for it on Encore's side, per look. A look with no headline spot keeps its own text. Plain text on one line, cut to 120 UTF-16 units (an emoji can count as two), and text holding ${ shows the look's own; line breaks and control characters are removed. A change redraws in place | | subheadline | string | Optional. Your subheadline for the look, with the same rules as headline | | cta | string | Optional. Your button label for the look, with the same rules as headline. Only a look that draws a button shows it | | appearance | AppearanceConfig | Optional colours for this trigger only (see AppearanceConfig). A change restyles it in place | | values | Record<string, string> | Optional. Your own words and numbers for the look to draw, by key: { minutes: '60' } for a look that says "${values.minutes} min". A change redraws in place, with no new request. Non-string entries are dropped; the native SDK drops the rest of what fails its rules and shows the look's default instead: at most 16 keys; a key starts with a lowercase letter, then letters and digits, up to 32 characters (coinsLeft, not coins_left); a value is plain text up to 64 characters. Values never leave the device. Text for the headline, subheadline and button goes in headline, subheadline and cta; those keys in values are ignored | | publisherReward | PublisherReward | Optional. Your prize for trying an offer, passed to the sheet the trigger opens. Same value and rules as Encore.placement(id).publisherReward(...) | | onResult | (result: PresentationResult) => void | Optional. Called with the result of each sheet the trigger opens: the same PresentationResult that show() resolves to | | style | StyleProp<ViewStyle> | Optional style for an outer wrapper: margins, width, alignSelf (use alignment to place the look inside it). height, minHeight and maxHeight are ignored |

It fills the width it is given and takes the height of the layout it shows. It takes zero height when there is nothing to show (the id is not in this native SDK build, churn, no offers for this user, dismissed, holdout, or an error), so it can sit inline without a placeholder. It renders nothing where the native SDK is not linked (Expo Go, web).

Heights come from the look Encore serves (its server template), so a look and its height can change without an SDK update. Don't reserve a fixed height: the trigger sizes itself.

What a trigger does after a claim (turn into the reminder, stay, hide, or thank the user) is decided by Encore's server, not by props. To draw the entry yourself, see Build your own button.

AppearanceConfig

Hex colours (#RGB, #RRGGBB, #RRGGBBAA, alpha last) for one trigger. They never reach the sheet the trigger opens. An unset accentColor comes from the Encore portal's accent, then the served look's, then Encore's default; an unset backgroundColor from the served look, then Encore's default. Each other field says below what unset does.

| Field | Description | |:------|:------------| | accentColor | Call to action, progress and countdown accents. Unset, the portal accent, then the served look's, then Encore's | | backgroundColor | Background. Unset, the served look's, then Encore's | | backgroundEndColor | Second stop of the background gradient (leading to trailing), for looks that draw one. Unset, the background is solid | | textColor | Primary text. Applies on its own. Unset, a backgroundColor gets a contrasting black or white text; with neither set, the served look's text colour | | secondaryTextColor | Secondary copy. Unset, the text colour faded as each look draws it | | trackColor | Progress tracks. Unset, the text colour faded as each look draws it | | onAccentColor | Ink on the accent (arrows, button labels). Unset, black or white for contrast | | colorScheme | 'system' (follows the device), 'light' or 'dark'. Unset, the app's setting in the Encore portal, else dark. Any other value is dropped |

<EncoreGiftsButton>

Shorthand for <EncoreTrigger id="giftsButton" ...>: a gift the user can open. Tapping it opens the normal Encore sheet for useCase at placement.

import { EncoreGiftsButton } from '@encorekit/react-native';

<EncoreGiftsButton
  placement="streak_complete"
  publisherReward={{ id: 'coins_100', title: '100 coins' }}
  onResult={(result) => {
    if (result.status === 'presented' && result.claim?.userConfirmedCompletion) {
      grantCoins(100); // de-duplicate on your user id plus result.claim.campaignId
    }
  }}
/>

| Prop | Type | Description | |:-----|:-----|:------------| | placement | string | The placement id the button and its sheet are served under | | useCase | UseCase | Optional. The sheet to open. Defaults to UseCase.rewardUsers. UseCase.reduceChurn renders nothing | | autoOpen | AutoOpen | Optional. Unset, Encore decides per served look whether the button may open its sheet by itself (at most once per placement per local day, then it keeps showing the same look). AutoOpen.off opts out: only a tap opens the sheet. off always wins | | publisherReward | PublisherReward | Optional, as for EncoreTrigger | | onResult | (result: PresentationResult) => void | Optional. Called with the result of each sheet the button opens, by a tap or the automatic open | | icon | TriggerIcon | Optional, as for EncoreTrigger | | alignment | EncoreAlignment | Optional, as for EncoreTrigger | | headline | string | Optional, as for EncoreTrigger | | subheadline | string | Optional, as for EncoreTrigger | | cta | string | Optional, as for EncoreTrigger | | appearance | AppearanceConfig | Optional colours, as for EncoreTrigger | | values | Record<string, string> | Optional, as for EncoreTrigger | | style | StyleProp<ViewStyle> | Optional wrapper style, as for EncoreTrigger |

<EncoreClaimReminder>

Shorthand for <EncoreTrigger id="claimReminder" ...>: an inline banner for an offer the user claimed but has not finished. Tapping it opens the Encore sheet, where that offer is listed.

import { EncoreClaimReminder } from '@encorekit/react-native';

<ScrollView>
  <EncoreClaimReminder placement="home" appearance={{ accentColor: '#5671FF' }} />
  {/* The rest of your screen. */}
</ScrollView>

| Prop | Type | Description | |:-----|:-----|:------------| | placement | string | The placement id the reminder and its sheet are served under | | useCase | UseCase | Optional. The sheet to open on tap. Defaults to UseCase.rewardUsers. UseCase.reduceChurn renders nothing | | publisherReward | PublisherReward | Optional, as for EncoreTrigger | | onResult | (result: PresentationResult) => void | Optional. Called with the result of each sheet the reminder opens | | icon | TriggerIcon | Optional, as for EncoreTrigger | | alignment | EncoreAlignment | Optional, as for EncoreTrigger | | headline | string | Optional, as for EncoreTrigger | | subheadline | string | Optional, as for EncoreTrigger | | cta | string | Optional, as for EncoreTrigger | | appearance | AppearanceConfig | Optional colours, as for EncoreTrigger | | values | Record<string, string> | Optional, as for EncoreTrigger | | style | StyleProp<ViewStyle> | Optional wrapper style, as for EncoreTrigger |

Sizing is the same as EncoreTrigger: zero height when there is no open claim, or its offer is not in the sheet for that use case and placement.

Build your own button

The components above are the quickest way in. When your app needs its own design, keep your own button or card and let Encore drive it: Encore decides what it shows, counts a view when at least half of it is on screen for a second with no sheet over it, and opens the reward sheet on a tap. You draw it.

What it turns into after a claim (the claim reminder, or the gifts again), after a completed claim (the gifts, a thank-you or nothing), and whether the sheet may open by itself are set per served look on Encore's side, so they can be tested like any look. If those rules cannot be fetched, the handle behaves as it always has: the reminder while a claim is open, then the gifts again. A failure never hides your button while the sheet has offers. The sheet's own experiments and holdout still apply.

import { EncoreClaimView, useEncoreClaim } from '@encorekit/react-native';

function StreakGift() {
  const { claim, state, dismiss } = useEncoreClaim({ placement: 'streak' });
  if (state.kind === 'hidden') return null;
  return (
    <EncoreClaimView claim={claim} accessibilityLabel="Open your gifts">
      {state.kind === 'reminder' ? (
        <ReminderCard name={state.advertiserName} deadline={state.deadline} onClose={dismiss} />
      ) : (
        <GiftsCard count={state.offerCount} logos={state.logoUrls} thanks={state.kind === 'thanks'} />
      )}
    </EncoreClaimView>
  );
}

Encore.claim(options)

Makes the handle. In a component, use useEncoreClaim (below), which makes and destroys it for you.

| Option | Type | Description | |:-------|:-----|:------------| | placement | string | Your label for where it sits, e.g. 'streak' | | useCase | UseCase | Optional. The sheet it opens. Defaults to UseCase.rewardUsers; UseCase.reduceChurn stays hidden | | autoOpen | AutoOpen | Optional. AutoOpen.off keeps the sheet from opening by itself. Unset, Encore decides per served look (at most once a day, and only after your view was seen); off always wins | | publisherReward | PublisherReward | Optional. Your prize on every sheet it opens; also settable on the handle | | onResult | (result: PresentationResult) => void | Optional. Called once per sheet it opens, after it closes; also settable on the handle. Grant your prize here, only when result.status === 'presented' and result.claim?.userConfirmedCompletion is true, de-duplicated on your user id plus result.claim.campaignId |

The handle's state is one of four:

| state.kind | Fields | Draw | |:-------------|:-------|:-----| | hidden | | Nothing: no offers, holdout, not configured yet, an error, churn, or a completed claim when Encore hides it after one | | gifts | offerCount, logoUrls (up to eight) | Your gifts entry | | reminder | advertiserName, logoUrl?, perk?, claimedAt: Date, deadline: Date, claimCount, transactionId | Your reminder for the newest open claim. deadline is when the reminder ends (24 hours after the claim) | | thanks | offerCount, logoUrls | A thank-you after a completed claim, when Encore shows one; a tap opens the sheet |

| Member | Description | |:-------|:------------| | state | What to draw now | | subscribe(listener) | Calls listener with every new state; returns the unsubscribe | | setVisible(fraction) | How much of your view is on screen, 0 to 1. EncoreClaimView reports it for you | | tap(target?) | Opens the sheet; resolves with what show() resolves to once it closes, the same result onResult receives. Grant the prize in onResult only (when result.claim?.userConfirmedCompletion is true), never also after await tap(). One tap at a time: another tap() meanwhile, or a tap that opens no sheet, resolves not_presented with already_presenting, and onResult is not called. Resolves not_presented while hidden. Never rejects. target (ClaimTarget.button, card or arrow) matters on a reminder, where analytics reports it and button counts as card; on gifts and thanks every tap reports button | | dismiss() | Hides the reminder (every claim it was showing) on this device. Only while the state is a reminder | | destroy() | Stops the handle. Call it when your view goes away. State stops at once; a tap in flight still finishes, so its sheet's result reaches onResult and tap(), and the handle is released after it. A sheet Encore opened by itself that is still up when you call it may not report its result, so destroy the handle when your view goes away, not while a sheet is showing | | publisherReward, onResult | Settable on the live handle |

Never throws: bad options, or a build without the native module, give a hidden handle whose calls do nothing and whose tap resolves not_presented. autoOpen reads as for EncoreTrigger: 'on' and 'experiment' are ignored with a warning, and any other value is read as off, with a warning.

useEncoreClaim(options) and <EncoreClaimView>

In a component, useEncoreClaim makes the handle when it mounts, destroys it when it unmounts, and makes it again when placement, useCase or autoOpen change. publisherReward and onResult apply to the live handle. It returns { claim, state, setVisible, tap, dismiss }.

EncoreClaimView wraps your UI in a native container the SDK measures, so the view is counted. Use one EncoreClaimView per handle at a time: natively only the newest binding is measured, so a second view on the same handle stops the first one counting. With handlesTap (the default) a press anywhere on it calls claim.tap(target) and it reads as a button; pass handlesTap={false} for a view with its own buttons and call tap from them.

| Prop | Type | Description | |:-----|:-----|:------------| | claim | EncoreClaim \| null | The handle to bind. A new one rebinds; unmounting unbinds | | handlesTap | boolean | Optional. Defaults to true | | target | ClaimTarget | Optional. On a reminder, what analytics reports for a press (button counts as card); on gifts and thanks every press reports button | | accessibilityLabel | string | Optional. Read when handlesTap is true | | style | StyleProp<ViewStyle> | Optional. For the container; lay the children out inside your own view |

Types

interface UserAttributes {
  email?: string;
  firstName?: string;
  lastName?: string;
  phoneNumber?: string;
  postalCode?: string;
  city?: string;
  state?: string;
  countryCode?: string;
  latitude?: string;
  longitude?: string;
  /** Whole years, 0 to 120; a fraction is floored, anything else dropped. For a range, send its lowest age. */
  age?: number;
  /** YYYY-MM-DD. Native works out the age on the device and never sends the date; given both, the lower age wins. */
  dateOfBirth?: string;
  gender?: string;
  /** @deprecated Use Encore.setLanguage(tag); removed at the next major. */
  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;
  // The user said "Yes, I completed it" (reward for trying). Their word, not a
  // verified conversion. Always present; false unless they said yes.
  userConfirmedCompletion: boolean;
}

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 PublisherReward {
  id: string;       // the only field that leaves the device
  title: string;    // blank means no prize
  detail?: string;
  iconUrl?: string; // http:// or https:// with a host, else ignored
}

interface PublisherRewardPolicy {
  dailyLimit?: number; // default 3; the native SDK clamps it to 1 to 10
}

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

Requirements

  • React Native 0.72+ (the version the Android bridge is built and tested against)
  • 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