@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/onPassthroughare gone, andshow()returns a different shape. See MIGRATION.md.
Installation
npm install @encorekit/react-nativeiOS
cd ios && pod installAndroid
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 afterreset()) 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
dailyLimitprizes 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 toonOutcomeas apresentationoutcome. Granting from both pays twice, so treatonOutcomeas 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_completedwebhook instead, once pertransactionId, 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
