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

@neostore/cinto

v1.0.90

Published

Generate a Add to Wallet button

Readme

Cinto SDK — Add to Wallet Button for Apple Wallet & Google Wallet

Cinto is a JavaScript / TypeScript SDK that lets you add an Add to Wallet button to any website in minutes. It automatically shows the correct button for Apple Wallet (iOS) or Google Wallet (Android) based on the visitor's device, and opens a pass viewer modal on desktop.

Built and maintained by The Wallet Crew — a Wallet-as-a-Service platform for digital passes, loyalty cards, membership cards, event tickets, and more.

→ Documentation: https://docs.thewalletcrew.io/enroll/on-your-website
→ npm: https://www.npmjs.com/package/@neostore/cinto
→ Website: https://www.thewalletcrew.io


What is Cinto?

Cinto is the front-end SDK for The Wallet Crew. It solves a common problem: how do you let your users save a digital pass to their phone wallet without building separate Apple and Google integrations?

  • Drop in a <script> tag or install via npm — no back-end changes required for basic usage
  • One <div> renders the right platform button automatically
  • Works with loyalty cards, event tickets, membership cards, boarding passes, coupons, gift cards, and any other Apple / Google Wallet pass type
  • Supports real-time pass updates via WebSocket (listenToPassEvents)
  • Fully typed TypeScript API

Who is this for?

| Use case | Example | |---|---| | E-commerce / retail | Add loyalty card or gift card to wallet at checkout | | Events & ticketing | Let attendees save their ticket to Apple or Google Wallet | | Hospitality | Hotel key cards, membership passes | | Healthcare | Appointment cards, insurance cards | | Transport | Transit passes, boarding passes | | Any SaaS with digital passes | Embed the button in your customer portal |


How it works

The SDK detects the user's device and renders the right button automatically:

| Platform | Detected when | Behaviour on click | |-----------|-----------------------|-----------------------------------------------| | apple | iOS / iPadOS device | Downloads the .pkpass file directly | | google | Android device | Opens the Google Pay "Save to wallet" flow | | desktop | Any non-mobile device | Opens a modal overlay with the pass page |


Quick start

CDN (no build step)

Add the snippet below to any page. Replace YOUR_TENANT_ID with your tenant identifier and add a container element with data-neostore-addToWalletButton and data-neostore-passId:

<script type="text/javascript">
    (function (n, e, o) {
        var s = n.createElement("script");
        s.src = "https://sdk.neostore.cloud/scripts/" + e + "/cinto@1";
        s.async = 1;
        s.onload = function () {
            neostore.cinto.initialize(e, o);
        };
        n.body.appendChild(s);
    })(document, "YOUR_TENANT_ID", {});
</script>

<div data-neostore-addToWalletButton data-neostore-passId="KlnqcxVLA9pS4ol5"></div>

initialize() scans the entire page for [data-neostore-addToWalletButton] elements and renders a button in each one. It is safe to call before or after DOMContentLoaded.

The third argument accepts any option from the Options section. Example — force French:

<script type="text/javascript">
    (function (n, e, o) {
        var s = n.createElement("script");
        s.src = "https://sdk.neostore.cloud/scripts/" + e + "/cinto@1";
        s.async = 1;
        s.onload = function () {
            neostore.cinto.initialize(e, o);
        };
        n.body.appendChild(s);
    })(document, "YOUR_TENANT_ID", { language: "fr" });
</script>

<div data-neostore-addToWalletButton data-neostore-passId="KlnqcxVLA9pS4ol5"></div>

npm

npm install @neostore/cinto
import { AddToWalletButton } from "@neostore/cinto";

const btn = new AddToWalletButton("YOUR_TENANT_ID", {
    language: "fr",
    passId: "KlnqcxVLA9pS4ol5",
});
btn.render(document.getElementById("btn"));

Options

Options can be set via the constructor / initialize() call, or as data-neostore-* attributes on the container element. Per-element data attributes always take precedence.

interface Options {
    /**
     * Your tenant identifier. Required.
     */
    tenantId: string;

    /**
     * Pass ID to display.
     * Can also be an async function that resolves to the pass ID.
     * Required unless externalIdentifiers is provided.
     */
    passId: string | (() => Promise<string>);

    /**
     * API environment base URL.
     * @default "https://app.neostore.cloud"
     */
    environment?: string;

    /**
     * Force a display language (ISO 639 code, e.g. "fr", "de", "es-MX").
     * When omitted the browser language is used, falling back to "en".
     */
    language?: string;

    /**
     * Force a platform instead of auto-detecting from the user-agent.
     * @values "apple" | "google" | "desktop"
     */
    platform?: "apple" | "google" | "desktop";

    /**
     * Pass layout name used to build the desktop pass page URL.
     * When omitted and a single passId is used, the SDK resolves the URL
     * through /api/{tenantId}/passes/{passId}/view.
     * When multiple comma-separated pass IDs are provided, defaults to "tickets".
     */
    passLayoutName?: string;

    /**
     * External identifiers used to resolve the passId server-side.
     * Use this when the pass ID is not known up-front.
     * Requires passType to be set as well.
     */
    externalIdentifiers?: Record<string, {
        value: string;
        hmac?: string;    // HMAC-SHA256 signature generated by your back-end
        secret?: string;  // Alternative signing method
    }>;

    /**
     * Pass type used when resolving a passId from externalIdentifiers.
     */
    passType?: string;

    /**
     * Analytics source metadata attached to download/view events.
     * utm_source, utm_campaign, and utm_medium are automatically picked up
     * from the page URL and merged with the values below.
     */
    source?: {
        tags?: string[];   // Enriched by utm_source and utm_campaign from the URL
        medium?: string;   // Falls back to utm_medium from the URL
        origin?: string;   // Defaults to the current page URL (without query string)
    };

    /**
     * Called when the user clicks the button, before the platform action runs.
     */
    onClick?: (e: MouseEvent, data: { options: Partial<Options>; platform: Platform }) => void;

    /**
     * Called when an error occurs (e.g. pass not found, network failure).
     */
    onError?: (error: string) => void;

    /**
     * Enable real-time pass update events via WebSocket.
     * @default false
     */
    listenToPassEvents?: boolean;

    /**
     * Called each time a pass event is received over the WebSocket.
     * See the Pass Events section for the full PassEventArgs interface.
     */
    onPassEvent?: (args: PassEventArgs) => void;
}

Data attributes reference

Every option can also be set directly on the container element:

| Data attribute | Corresponding option | Notes | |---|---|---| | data-neostore-passId | passId | | | data-neostore-passType | passType | | | data-neostore-platform | platform | "apple", "google" or "desktop" | | data-neostore-language | language | ISO 639 code | | data-neostore-listenToPassEvents | listenToPassEvents | "true" or "false" | | data-neostore-onPassEvent | — | Name of a window.* function to call on pass events | | data-neostore-src | source.tags | Comma-separated list of tags | | data-neostore-src-tags | source.tags | Comma-separated list of tags | | data-neostore-src-medium | source.medium | | | data-neostore-src-origin | source.origin | | | data-neostore-externalIdentifiers-{key}-value | externalIdentifiers[key].value | | | data-neostore-externalIdentifiers-{key}-hmac | externalIdentifiers[key].hmac | | | data-neostore-externalIdentifiers-{key}-secret | externalIdentifiers[key].secret | |


Platform detection

Platform is detected from the user-agent at render time:

  • Apple device (iPhone / iPad)apple button — downloads the .pkpass directly
  • Android devicegoogle button — opens the Google Pay "Save to wallet" page
  • Everything else (desktop, tablet, unknown)desktop button — opens a modal overlay with the pass page

Override the detection for testing: data-neostore-platform="apple" or the platform option.


Language detection

The browser's language preference list is used automatically. The language option (or data-neostore-language) pins a specific value. If no match is found the button defaults to en.

Supported languages per platform:

Apple Wallet button: ar, az, bg, cn, cz, de, dk, ee, en, es, es-CA, es-MX, fi, fr, fr-CA, gr, hb, hk, hr, hu, id, in, it, jp, kr, lt, lv, mt, my, nl, no, ph, pl, pt, pt-BR, ro, ru, se, si, sk, th, tr, tw-TC, ua, vn

Google Pay button: ar, az, bg, br, bs, by, ca, cz, de, dk, en, en-AU, en-CA, en-IN, en-SG, en-US, en-ZA, es, es-ES, es-US, et, fr-CA, fr-FR, gr, he, hr, hu, hy, id, is, it, jp, ka, kk, ky, lt, lv, mk, my, nl, no, pl, pt, ro, ru, se, sk, sq, sr, th, tr, uk, uz, vi, zh, zh-HK, zh-TW

Desktop button: ar, az, bg, cs, da, de, el, en, es, fr, it, ja, nl, pl, pt, ru, zh


Customisation

The rendered HTML structure is:

<div data-neostore-addToWalletButton>   ← your container element
  <a class="neostore-link">             ← generated by the SDK
    <img class="neostore-img neostore-img-{platform}">
  </a>
</div>

CSS classes you can target:

| Class | Element | Notes | |---|---|---| | .neostore-link | <a> | The clickable button wrapper | | .neostore-img | <img> | The wallet button image | | .neostore-img-apple | <img> | Present on Apple buttons | | .neostore-img-google | <img> | Present on Google buttons | | .neostore-img-desktop | <img> | Present on desktop buttons | | .neostore-loading | <a> | Added while the click action is in progress | | .neostore-img-loading | <img> | Present until the image has finished loading |

The Apple and Google images comply with their respective official design guidelines.


Usage examples

Multiple buttons on the same page

initialize() automatically processes all [data-neostore-addToWalletButton] elements. Each element can carry its own data-neostore-passId:

<div data-neostore-addToWalletButton data-neostore-passId="pass1"></div>
<div data-neostore-addToWalletButton data-neostore-passId="pass2"></div>

Multiple passes in one button

Pass a comma-separated list of pass IDs. Set passLayoutName to control the layout page (defaults to "tickets" when multiple IDs are detected):

<div
    data-neostore-addToWalletButton
    data-neostore-passId="pass1,pass2,pass3"
></div>

passId as an async function (npm only)

When the pass ID must be fetched at click time, provide a function instead of a string:

const btn = new AddToWalletButton("YOUR_TENANT_ID", {
    passId: async () => {
        const res = await fetch("/api/my-pass");
        return (await res.json()).passId;
    },
});
btn.render(document.getElementById("btn"));

Using external identifiers

When the pass ID is not known up-front, provide external identifiers for the SDK to resolve it server-side. See the back-end setup documentation for generating the HMAC on your server.

Using data attributes:

<script type="text/javascript">
    (function (n, e, o) {
        var s = n.createElement("script");
        s.src = "https://sdk.neostore.cloud/scripts/" + e + "/cinto@1";
        s.async = 1;
        s.onload = function () {
            neostore.cinto.initialize(e, o);
        };
        n.body.appendChild(s);
    })(document, "YOUR_TENANT_ID", {});
</script>

<div
    data-neostore-addToWalletButton
    data-neostore-passType="membership"
    data-neostore-externalIdentifiers-customerId-value="SC103010"
    data-neostore-externalIdentifiers-customerId-hmac="cbbcfc5xxxxx"
></div>

Using the npm component:

import { AddToWalletButton } from "@neostore/cinto";

const btn = new AddToWalletButton("YOUR_TENANT_ID", {
    passType: "membership",
    externalIdentifiers: {
        customerId: {
            value: "SC103010",
            hmac: "cbbcfc5xxxxx", // HMAC-SHA256 signed by your back-end
        },
    },
});
btn.render(document.getElementById("btn"));

Case-sensitive external identifier keys

HTML data-* attribute names are lowercased by the browser. To preserve uppercase letters in an identifier key, prefix the capital letter with _:

<!-- produces the key "customerId" (capital I) -->
<div data-neostore-externalIdentifiers-customer_Id-value="SC103010"></div>

Analytics / source tracking

UTM parameters in the page URL (utm_source, utm_campaign, utm_medium) are captured automatically. You can add extra tags via data attributes or the source option:

<div
    data-neostore-addToWalletButton
    data-neostore-passId="pass1"
    data-neostore-src-tags="email-newsletter,welcome"
    data-neostore-src-medium="email"
></div>

Or via the npm component:

const btn = new AddToWalletButton("YOUR_TENANT_ID", {
    passId: "pass1",
    source: {
        tags: ["email-newsletter", "welcome"],
        medium: "email",
    },
});

Desktop: render a QR code instead of the modal

On desktop the button normally opens a modal overlay. To render a QR code yourself, use getPassPageUrl() to retrieve the URL:

<script src="https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"></script>

<div id="qrcode"></div>
<script type="module">
    import { AddToWalletButton } from "@neostore/cinto";

    const button = new AddToWalletButton("YOUR_TENANT_ID", {
        passId: "KlnqcxVLA9pS4ol5",
    });
    const url = await button.getPassPageUrl();
    new QRCode(document.getElementById("qrcode"), url);
</script>

React integration

import React, { useEffect, useRef } from "react";
import { AddToWalletButton } from "@neostore/cinto";

const CintoButton: React.FC<{ tenantId: string; passId?: string }> = ({ tenantId, passId }) => {
    const ctaRef = useRef<HTMLDivElement>(null);
    const cintoRef = useRef<AddToWalletButton>();

    useEffect(() => {
        cintoRef.current = passId
            ? new AddToWalletButton(tenantId, { passId })
            : undefined;
        if (ctaRef.current) {
            cintoRef.current?.render(ctaRef.current);
        }
    }, [passId, tenantId]);

    return <div ref={ctaRef} />;
};

export default CintoButton;

Pass Events

When listenToPassEvents is enabled, the SDK opens a WebSocket connection to the server and calls your handler whenever the pass is updated (e.g. points balance changed, pass invalidated). Each button manages its own isolated connection.

The connection automatically reconnects on network drops using exponential backoff (up to 20 attempts, capped at 30 s per attempt). Calling args.disconnect() stops reconnection permanently.

Note: listenToPassEvents defaults to false.

Option 1 — DOM custom event

<div id="btn1" data-neostore-addToWalletButton data-neostore-passId="pass1" data-neostore-listenToPassEvents="true"></div>
<div id="btn2" data-neostore-addToWalletButton data-neostore-passId="pass2" data-neostore-listenToPassEvents="true"></div>

<script>
    document.getElementById("btn1").addEventListener("neostore:pass-event", (e) => {
        const args = e.detail; // PassEventArgs
        console.log("Pass updated:", args.data);
        console.log("Pass ID:", args.passId);
        console.log("At:", new Date(args.timestamp));
    });

    document.getElementById("btn2").addEventListener("neostore:pass-event", (e) => {
        console.log("Pass 2 updated:", e.detail.data);
    });
</script>

Option 2 — Named window handler (data attribute)

<div
    data-neostore-addToWalletButton
    data-neostore-passId="pass1"
    data-neostore-listenToPassEvents="true"
    data-neostore-onPassEvent="handlePassEvent"
></div>

<script>
    window.handlePassEvent = (args) => {
        console.log("Pass updated:", args.data);
        console.log("Pass ID:", args.passId);
    };
</script>

Option 3 — onPassEvent callback (npm)

const button = new AddToWalletButton("YOUR_TENANT_ID", {
    passId: "pass1",
    listenToPassEvents: true,
    onPassEvent: (args) => {
        console.log("Pass updated:", args.data);
    },
});
button.render(document.getElementById("btn"));

PassEventArgs

All three options receive the same PassEventArgs object:

| Property / Method | Type | Description | |---|---|---| | args.data | unknown | The event payload received from the server | | args.passId | string | The pass ID that triggered the event | | args.timestamp | number | Unix timestamp (ms) when the event was received | | args.disconnect() | () => void | Close the WebSocket, clear timers, and release resources | | args.reconnect() | () => Promise<void> | Re-open the connection after disconnect() |

Example: process one event then stop

const button = new AddToWalletButton("YOUR_TENANT_ID", {
    passId: "pass1",
    listenToPassEvents: true,
    onPassEvent: (args) => {
        console.log("Received:", args.data);
        args.disconnect(); // releases the WebSocket immediately
    },
});
button.render(document.getElementById("btn"));

Example: pause and resume

const button = new AddToWalletButton("YOUR_TENANT_ID", {
    passId: "pass1",
    listenToPassEvents: true,
    onPassEvent: async (args) => {
        if (args.data.type === "update-needed") {
            args.disconnect();
            await doWork();
            await args.reconnect();
        }
    },
});
button.render(document.getElementById("btn"));

API reference

initialize(tenantId, options?) — CDN

Scans the page for [data-neostore-addToWalletButton] elements and renders a button in each one. Safe to call at any point; waits for DOMContentLoaded if the DOM is not yet ready.

neostore.cinto.initialize("YOUR_TENANT_ID", { language: "fr" });

AddToWalletButton — npm

Creates a button instance for a single element.

const btn = new AddToWalletButton(tenantId, options?);

| Method / Getter | Returns | Description | |---|---|---| | render(element) | Promise<void> | Renders the button into element | | perform() | Promise<void> | Programmatically triggers the button action (download / open modal) | | getPassPageUrl() | Promise<string> | Returns the destination URL — useful for custom QR code rendering | | platform | "apple" \| "google" \| "desktop" | Detected platform (read-only getter, synchronous) |

getPassId(externalIdentifiers, options) — npm

Looks up an existing pass ID from external identifiers. Returns null if no pass matches.

getOrCreatePassId(externalIdentifiers, options) — npm

Looks up or creates a pass from external identifiers.

getViewUrl(passId, options) — npm

Returns the direct wallet URL for a pass:

  • Apple → shoebox:// deep-link
  • Google → https://pay.google.com/gp/v/save/{jwt}

FAQ

How do I add an "Add to Apple Wallet" button on my website?
Install @neostore/cinto via npm or use the CDN snippet. Add a <div data-neostore-addToWalletButton data-neostore-passId="..."> element. The SDK detects iOS automatically and renders the official Apple Wallet button that downloads the .pkpass file.

How do I add a "Save to Google Wallet" button on my website?
Same setup as above — the SDK detects Android and renders the Google Pay "Save to wallet" button, which opens the Google Wallet save flow.

Does it work without a build tool?
Yes. Use the CDN <script> snippet. No npm, no bundler required.

Do I need separate code for Apple and Google?
No. One <div> and one SDK call handles both. Platform is detected automatically from the visitor's user-agent.

What types of passes are supported?
Any pass type supported by The Wallet Crew: loyalty cards, membership cards, event tickets, coupons, gift cards, boarding passes, transit passes, hotel keys, and more.

What languages are supported?
The button UI is localised in 50+ languages across Apple and Google platforms. The browser language is detected automatically. See the Language detection section for the full list.

Can I use this with React / Vue / Angular?
Yes — see the React integration example. The AddToWalletButton class is a plain TypeScript class that wraps any HTMLElement.

What happens when the user is on a desktop (not a phone)?
A modal overlay opens with the pass page, letting the user scan a QR code or view the pass details. Alternatively you can use getPassPageUrl() to render your own QR code.

Can I listen for real-time pass updates?
Yes — set listenToPassEvents: true. The SDK opens a WebSocket and dispatches a neostore:pass-event custom DOM event each time the pass is updated server-side. See Pass Events.

Is the pass ID always required up-front?
No. You can pass an async function as passId, or use externalIdentifiers to let the SDK resolve the pass ID from your own identifiers (e.g. customer ID, order ID) with HMAC signing.

Is this open source?
The SDK is MIT licensed. The back-end pass management platform is provided by The Wallet Crew and requires an account.