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/web-sdk

v2.2.1

Published

Encore SDK for web applications - display targeted offers and manage user entitlements

Readme

Encore Web SDK

A JavaScript/TypeScript SDK that enables web applications to display targeted offers to users, granting them provisional access to premium features when they complete advertiser offers.

Features

  • 🎯 Targeted Offer Presentation - Display offers at critical moments (cancellation flows, paywalls, onboarding)
  • 🎁 Use Cases - reduceChurn (the default) or rewardUsers — see Use Cases
  • 🔐 Entitlement Management - Two-tier system (provisional + verified) for instant UX and reliable billing
  • 📱 Framework Agnostic - Works with React, Vue, Angular, Svelte, vanilla JavaScript, or any web framework
  • 🎨 Responsive UI - Beautiful, accessible modal interface that works on desktop and mobile
  • 📦 Lightweight - < 50KB gzipped bundle size
  • 🔌 Offline Support - Queues signals and syncs when connectivity restored
  • ♿ Accessible - WCAG 2.1 Level AA compliant

Installation

npm install @encorekit/web-sdk

Or via CDN:

<script src="https://encorekit.com/encore.min.js"></script>

Upgrading to 2.0.2

2.0.2 removed the deferred redemption flow and the layout system. The layout removal stands. The deferred-flow removal was a regression and is undone in 2.0.5 — see Deferred redemption below; if you are on 2.0.2–2.0.4 and never set redemptionMode, that is the section that concerns you.

| Removed | Replacement | |:--------|:------------| | redemptionMode — on configure() and on placement options | None, and none is coming. 2.0.5 restores the deferred flow itself (see below), but what a claim does with the handoff is data on the variant, never a publisher option. | | Encore.redeem(), placement(id).redeem(), getPendingTransaction(), RedeemOptions, RedeemResult | Restored in 2.0.5 — see Deferred redemption below. | | layout: 'list' \| 'thankYou' | useCase — 'reduceChurn' renders the offer carousel, 'rewardUsers' the reward composition. | | header.title, header.subtitle | headline / subheadline on PlacementOptions. | | header.icon | statusIcon on PlacementOptions — the default flips from 'success' to 'none', so the check no longer renders unless you ask for it. | | offerContext, footer, receipt / ShowOptions, Receipt | None. Render any other copy yourself. | | display / HostDisplay | None as a publisher option. The deferred screens restored in 2.0.5 resolve the host's name, trial period and icon from remote config instead, and degrade when they are unset. | | .headline() / .subheadline() builder steps | The flat headline / subheadline options. .useCase() stays. | | 'churn-intervention' / 'post-action-reward' | 'reduceChurn' / 'rewardUsers' — a rename of the TypeScript symbols only; the values sent over HTTP are unchanged. |

Quick Start

import Encore from '@encorekit/web-sdk';

// Configure the SDK
Encore.configure({
  apiKey: 'your-api-key-here',
  environment: 'production'
});

// The SDK auto-generates and persists an anonymous user ID.
// Identify the user after login to attach your own ID (optional)
Encore.identify('user-123', {
  email: '[email protected]',
  subscriptionTier: 'free'
});

// Already know the user at load time (a login, or the userId on a hosted offer
// link)? Name them at configure instead, so the SDK's very first request — the
// /config that assigns experiment arms — runs as that user, not as an anonymous
// id it then abandons. A stored anonymous id is aliased to it as identify() would.
//   Encore.configure({ apiKey: 'your-api-key-here', environment: 'production', userId: 'user-123' });

// Present a placement — defaults to the 'reduceChurn' use case
const result = await Encore.show();
if (result.status === 'claimed') {
  // The user claimed an offer. Keep result.transactionId — it is how the
  // conversion is traced back to this user and app, and what
  // waitForVerification() / didGrant() take.
  //
  // WHEN the advertiser opens depends on the variant's claim behaviour:
  // 'reduceChurn' defaults to deferred, so the claim is banked and you call
  // Encore.redeem() after your own flow. 'rewardUsers' defaults to immediate,
  // so the advertiser already opened in a new tab.
} else {
  // 'dismissed' (with an optional reason) or 'unavailable' — nothing was claimed.
  console.log('Nothing claimed:', result);
}

// Check entitlement on YOUR server (not client-side)
// See: https://docs.encorekit.com/server-side-validation

Placements

A placement is one presentation of offers. There are two call forms, and they take the same options:

// Builder form
const result = await Encore.placement('milestone_reached', {
  headline: 'Nice work, Sam!',
  subheadline: 'That is your fifth week running',
  appearance: { accentColor: '#0A6' },
})
  .useCase('rewardUsers')
  .onNotGranted((reason) => console.log('closed without a claim', reason))
  .onLoadingStateChange((isLoading) => setSpinner(isLoading))
  .show();                       // takes no arguments

// Flat form — callbacks first, then the same options, then the placement id
await Encore.show(
  { onGranted, onNotGranted, onError },
  { useCase: 'rewardUsers', headline: 'Nice work, Sam!' },
  'milestone_reached'
);

Encore.show()'s first argument is callbacks only — onGranted, onNotGranted, onError. Every other option is a placement option.

PlacementOptions

| Option | Type | Meaning | |:-------|:-----|:--------| | useCase | 'reduceChurn' \| 'rewardUsers' | Why the placement presents, and which composition it renders. Defaults to 'reduceChurn'. | | headline | string | Per-call override of the sheet's primary heading. | | subheadline | string | Per-call override of the line under it. | | appearance | AppearanceConfig | Optional colour and per-element overrides. Advanced — see Appearance. | | statusIcon | 'success' \| 'none' | Status icon above the headline. Defaults to 'none' — nothing renders unless you ask for 'success', which draws a green-circle check. Only 'rewardUsers' has a status block; 'reduceChurn' ignores it. The default matches iOS and Android, whose reward variant renders no checkmark. |

Appearance (advanced — optional)

You can skip this. Omitting appearance renders the SDK's default sheet, which is the normal integration path. Everything below is opt-in.

Changing a colour is one line:

await Encore.show(undefined, { appearance: { accentColor: '#0A6' } });

| Option | Type | What it moves | |:-------|:-----|:--------------| | accentColor | colour | --encore-primary-color: focus rings, links, the churn CTA. | | backgroundColor | colour | --encore-bg-color: the sheet canvas and every surface derived from it. | | textColor | colour | --encore-text-color, from which muted and secondary ink are derived. | | modalBackgroundColor | colour | The modal container only — narrower than backgroundColor. | | dotColor / dotActiveColor | colour | The page indicator's inactive / active dots. | | headline / subheadline | slot | The reward sheet's heading and the line under it. | | claimButton / declineButton | slot | The "Claim gift" CTA and the "No thanks" button. |

Colours accept hex, rgb()/hsl()/oklch() and friends, named colours, and var(--your-token). Slot fields are read by the 'rewardUsers' composition — the one with a heading block, a CTA pair and page dots; 'reduceChurn' renders the bare carousel and uses the colours only.

Slots

A slot takes whichever of these three you need — nothing more:

appearance: {
  // 1. Styled text. No DOM required.
  headline: {
    text: 'Nice work, Sam!',
    color: '#FFD166',
    fontFamily: '"Söhne", system-ui, sans-serif',
    fontSize: '28px',
    fontWeight: 700,
  },

  // 2. Your own element. Render something that is not text at all.
  subheadline: myStreakBadgeElement,

  // 3. A render function, called per presentation.
  claimButton: ({ text, useCase, defaultElement }) => myCta(text),
}

A render function receives text (the copy the SDK resolved), useCase, and defaultElement — the element the SDK would have rendered. Return defaultElement to decorate rather than replace, which keeps every default style and accessibility attribute:

declineButton: ({ defaultElement }) => {
  defaultElement.classList.add('my-ghost-button');
  return defaultElement;
}

Text fields accept text, color, fontFamily, fontSize, fontWeight, letterSpacing and textAlign; button slots add backgroundColor, borderColor and borderRadius.

fontFamily accepts Latin family names, including accented ones ("Söhne", Größe). A family name in a non-Latin script (CJK, Cyrillic, Arabic) is not accepted on this path — use the element or render-function form for those, which applies your own CSS and bypasses the check entirely.

Precedence and fallbacks

Copy resolves per field, most specific first:

appearance.headline.text → PlacementOptions.headline → backend-resolved copy → the SDK's built-in fallback.

A blank or whitespace-only value falls through to the next tier rather than blanking the slot, so omit a field instead of passing ''.

Two guarantees

A bad override costs you the override, never the sheet. Render functions run inside a guard and colours/fonts/sizes go through an allow-list, so a throw, a non-element return, a wrong type or a malformed colour falls back to the SDK's default element. The modal cannot be taken down by anything you pass.

The SDK keeps the action. On claimButton and declineButton you supply appearance only. Your element is mounted inside an SDK-owned control that carries the role, the tab stop and the listener; the listener runs in the capture phase, so the SDK's claim/decline runs first and a handler you attached to your own element does not run. Read the outcome from show()'s resolved ShowResult — that is the supported way to react to a claim or a dismissal.

The element you pass is adopted: it is moved into the sheet and removed with it. Pass a fresh element (or use a render function) rather than one that is live in your page, and do not pass the same element to two slots.

The result

show() never rejects. It resolves one of three statuses:

| Status | Meaning | |:-------|:--------| | { status: 'claimed', offerId, campaignId, advertiserName, transactionId? } | The user claimed an offer. The advertiser opened in a new tab inside the tap gesture and the SDK renders nothing afterwards — the host owns that moment, so render your own confirmation if you want one. The payload mirrors iOS's and Android's ClaimedOffer field-for-field. transactionId carries the attribution — it is how a completion that lands days later is traced back to this user and app, and it is the argument didGrant() and waitForVerification() take. It is optional because the transaction write can fail while the claim genuinely happened; the SDK logs a warning when that occurs. | | { status: 'dismissed', reason? } | The sheet closed without a claim. Failures land here too, carrying reason.error. | | { status: 'unavailable' } | Nothing was presented — no eligible offers, or the surface did not resolve. |

claimed deliberately makes no assertion about advertiser conversion: that is asynchronous and server-authoritative. Verify entitlements and conversions server-side, by user, never from the client.

Use Cases

A placement's use case says why it presents — and it is what selects the composition. It defaults to 'reduceChurn'.

| Value | Composition | Meaning | |:------|:------------|:--------| | 'reduceChurn' | Offer carousel | The default. A user is cancelling or downgrading; the offer is an alternative to leaving. | | 'rewardUsers' | Reward composition | The user completed something worth celebrating — a purchase, a milestone, a streak, a level. The offer is a gift. |

It is both a plain placement option and a builder step — use whichever reads better:

Encore.placement('milestone_reached', { useCase: 'rewardUsers' });
Encore.placement('milestone_reached').useCase('rewardUsers');

The wire values are unchanged. reduceChurn / rewardUsers are the TypeScript symbols. Over HTTP the SDK still sends churn-intervention / post-action-reward (GET /config?useCase=, POST /transactions, and the use_case analytics property), so existing dashboards and attribution are unaffected by the rename.

Supplying your own headline and subheadline

Pass your copy at presentation time. This is the intended way to set it — you know what the user just did, so you can name it:

await Encore.placement('milestone_reached', {
  useCase: 'rewardUsers',
  headline: 'Nice work, Sam!',
  subheadline: 'That is your fifth week running',
}).show();

Both are per-call overrides: they beat whatever copy Encore resolved for the surface, and they work on either use case. headline fills the sheet's primary heading and subheadline the line under it.

Blank falls through — it does not clear the copy. A blank or whitespace-only value is treated as "not supplied" and falls through to the copy Encore resolved, so passing '' is never a way to blank the sheet. Omit the option instead. This keeps a value that is empty because an interpolation came back empty — a headline built from a template literal whose name was undefined — from rendering a headless sheet.

If you supply neither, the sheet falls back to Encore's resolved copy for the surface, and finally to a built-in string, so the heading is never empty.

rewardUsers grants no entitlement

A reward claim is a gift the brand funds directly, not a subscription unlock. So show() resolves { status: 'claimed' } and no entitlement is granted — on this path the SDK skips the automatic provisional grant it performs for 'reduceChurn', so no grant signal reaches Encore. Nothing was promised, so nothing is unlocked. (Reward activation is confirmed server-side via the offer-completed webhook, an entirely separate mechanism.)

This is why claimed is its own result and not folded into dismissed: a user who successfully claimed a reward must never run your "user declined" branch.

Unrecognised values fail closed

Anything that is not one of the two values above is rejected before anything is presented, rather than quietly falling back to the churn surface. show() still never rejects — it resolves:

{ status: 'dismissed', reason: { type: 'error', error: { code: 'INVALID_REQUEST', … } } }

If you call Encore.show({ onError, onNotGranted }, { useCase }), both callbacks fire. Via Encore.placement(...) only the builder's .onNotGranted() exists — there is no .onError() on the builder — so read the returned reason.error.

Enablement affects SPEED, not availability

Requesting a use case is all it takes to be served it. There is no setup call, and you do not need Encore to switch anything on first:

| Your app | What happens | |:---------|:-------------| | Enabled for the use case | Its config arrives with the one /config fetch at configure() — the first presentation is instant | | Not enabled | The SDK fetches that one surface on the first present, then caches it — slower once, instant thereafter |

When the second row applies the SDK logs a one-off [INTEGRATION] notice saying the surface was not prefetched. It is a speed hint, not an error — nothing has failed. Ask Encore to enable the use case for your app to remove the first-present round trip.

Encore can still switch a surface off. An explicit kill switch is the one thing that blocks serving, and it is deliberate — it is how a live surface is stopped without a deploy. A switched-off surface no-ops exactly as below.

Per-placement reward variants (opt-in)

Encore can serve a different reward variant per placement label, for example a code screen for 'email_code' and the plain gift picker for 'email'. The variant prefetched at configure() cannot carry that choice, so ask for it:

Encore.configure({ apiKey: 'your-api-key-here', resolveVariantsByPlacement: true });

// One GET /config?useCase=post-action-reward&placementId=email_code before the
// sheet renders; the variant it names is presented and reported.
await Encore.placement('email_code', { useCase: 'rewardUsers' }).show();

A settled answer is reused for that label for the rest of the session (until reset()), while a failed or discarded request is retried on the next presentation. A failed or slow request falls back to what the SDK does with the option off (the prefetched variant, or the on-demand fetch when there is none); it never blocks or breaks the presentation. Off (the default), the SDK's requests are unchanged.

When nothing presents

A use case that cannot be served resolves { status: 'unavailable' }, reports sdk_offer_presentation_failed with reason no_template, and never falls back to another use case's offers or copy. It does not throw.

unavailable is the same result you get when there are simply no eligible offers, and it has several causes — so do not read it as "not enabled":

  • Encore has switched the surface off, or no variant was eligible for it;
  • the /config response had not arrived within ~2.5s of the call, or failed;
  • the on-demand fetch for the surface failed.

⚠️ This is silent by default. The explanatory log is emitted at info, and every log level is gated behind logLevel: 'debug' — which is not the default. When integrating a reward placement, configure logLevel: 'debug' and confirm you see an actual presentation. A clean console is not evidence that it worked.

See the use-case serving model for the full design note.

Deferred redemption

A claim and the advertiser handoff are two moments, and a placement decides whether they are the same moment. That is claimBehavior, and it is data on the variant — Encore configures it per placement. There is no publisher option for it, deliberately: redemptionMode was one until 2.0.2, and its default flipping underneath integrations that never set it is exactly the failure this replaces.

| Behaviour | On claim | redeem() | Survives a reload | |:----------|:---------|:-----------|:------------------| | immediate | Redirects to the advertiser inside the tap gesture. Nothing is stored. | Resolves unavailable | n/a | | deferred | Banks the claim and shows a primer. No redirect. | Presents the claim, redirects on the CTA, marks it redeemed | Yes | | deferredWhileAlive | Holds the claim in memory for this page. | Presents it while the page lives | No, by design |

Defaults: reduceChurn is deferred, rewardUsers is immediate.

// A deferred placement: show() resolves once the user passes the primer.
// Nothing has opened yet — the claim is banked.
const result = await Encore.placement('cancel_flow').show();

// ...your own cancellation flow runs, possibly on a later page load...

// Phase 2. Presents the claim and sends the user to the advertiser.
const redemption = await Encore.placement('cancel_flow').redeem();
if (redemption.status === 'redeemed') {
  // The advertiser opened inside the CTA gesture and the claim is consumed.
}

redeem() never rejects. It resolves:

| Result | Meaning | |:-------|:--------| | { status: 'redeemed' } | The handoff happened and the claim is consumed. | | { status: 'dismissed', reason? } | The user closed the sheet, or an internal failure occurred (reason.type === 'error'). The claim is untouched, so retrying later is safe. | | { status: 'unavailable' } | Nothing is pending. Strictly that — never an error in disguise. Every immediate placement always resolves this. |

To render your own CTA instead of the SDK's, read the pending claim first:

const pending = Encore.getPendingTransaction('cancel_flow'); // or () for the latest
if (pending) showMyOwnBanner(pending.offer.advertiserName);

Scoping is strict: placement(id).redeem() and getPendingTransaction(id) resolve only the claim made at that placement and never cross to another's. The bare forms take the most recent claim for the user.

The provisional grant follows the handoff, not the claim. On a deferred placement it is sent when redeem() performs the redirect, not when the user taps Claim. Verified access is server-side either way — check it on your backend, never from the client.

Browser Support

  • Chrome/Edge (last 2 versions)
  • Firefox (last 2 versions)
  • Safari (last 2 versions)
  • iOS Safari 15+

iOS WebView Integration

The simplest possible integration: build a URL and open it in a WKWebView. No SDK, no dependencies, nothing to implement. Encore hosts the offer page and its query string is the entire configuration surface.

Quick Start

// 1. Build the URL (copy EncoreURLBuilder.swift into your project)
guard let url = EncoreURL.build(apiKey: "pk_live_...", userId: "user_123") else { return }

// 2. Create a webview and load it
let webView = WKWebView(frame: view.bounds)
view.addSubview(webView)
webView.load(URLRequest(url: url))

Complete Example

import UIKit
import WebKit

func showEncoreOffer() {
    guard let url = EncoreURL.build(
        apiKey: "pk_live_...",
        userId: currentUser.id,
        attributes: [
            "subscriptionTier": currentUser.tier,
            "countryCode": currentUser.countryCode
        ]
    ) else { return }

    let webView = WKWebView(frame: view.bounds)
    view.addSubview(webView)
    webView.load(URLRequest(url: url))
}

Targeting attributes are coarse by design. email, phoneNumber, names, dateOfBirth, gender and precise location are refused, because a query string is written to webview history and to hosting access logs.

SwiftUI Version

import SwiftUI

struct MyView: View {
    var body: some View {
        EncoreWebView(
            apiKey: "pk_live_...",
            userId: currentUser.id
        )
    }
}

What You Get

  • Zero dependencies: one Swift file to copy
  • Three lines of code: build URL, create webview, load
  • Works everywhere: UIKit, SwiftUI, and any other platform with a webview
  • Nothing to maintain: the page is hosted, so the contract is the URL

Knowing when a user earned something

The page does not call your app back. There is no native bridge on any platform. Your backend asks Encore instead:

GET https://api.encorekit.com/encore/publisher/sdk/v1/entitlements/server?userId=<userId>

authenticated with your API key plus an HMAC signature you compute with your API secret. The secret itself is never sent.

Documentation

When to Use

| Feature | Hosted page (URL) | Native iOS SDK | |---------|-------------------|------------| | Integration | 3 lines | ~50 lines | | Dependencies | None | Requires SPM | | Native entitlement callbacks | No, poll from your backend | Yes | | StoreKit and offline handling | No | Yes |

Use the hosted page when you want the smallest integration. Use the native SDK when you need entitlement callbacks inside the app.

Documentation

Local Development

Develop, run, and validate the SDK locally — all through make:

npm install
make dev     # build + watch the SDK, open the harness at http://localhost:3000
make         # print all targets

See docs/runbooks/local-development.md for the full guide: framework examples (make dev FRAMEWORK=react), published-release validation (make published), environment switching, and troubleshooting.