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

@leisuresaas/expo

v0.5.4

Published

Expo SDK for LeisureSaas product OAuth, mobile billing, Public Ads, and App version updates (app_cfg_pk)

Readme

@leisuresaas/expo

Expo / React Native SDK for LeisureSaas product OAuth, mobile billing, platform ads, and app version updates (catalog plans, store confirm/restore, subscription, ad banners, AppUpdateProvider).

Mirrors sdk/go Integration API surface for Expo apps. Production apps should call a product BFF that holds the Integration API Key; use gateway mode only for local dev.

Install

公开发布(npm):

npm install @leisuresaas/expo

metro.config.js 需加入 transpilePackages: ["@leisuresaas/expo"](SDK 为 TypeScript 源码)。
发布流程见 PUBLIC_PUBLISHING.md

Monorepo 本地开发:

{
  "dependencies": {
    "@leisuresaas/expo": "file:../../sdk/expo"
  }
}

Peer dependencies: expo, expo-auth-session, expo-secure-store, expo-web-browser, react, react-native.

Quick start (BFF — recommended)

import {
  LeisureSaasAuthProvider,
  createLeisureSaasClient,
  devAppleSignedTransaction,
  mobilePlatform,
  useLeisureSaasAuth,
} from "@leisuresaas/expo";

const client = createLeisureSaasClient({
  bffBaseUrl: "https://api.myproduct.com",
});

export function App() {
  return (
    <LeisureSaasAuthProvider
      config={{
        issuer: "https://auth.example.com",
        clientId: "my-app-mobile",
        redirectScheme: "myapp",
      }}
    >
      <Home />
    </LeisureSaasAuthProvider>
  );
}

function Home() {
  const { accessToken, login } = useLeisureSaasAuth();
  // client.listPlans(accessToken, mobilePlatform())
  // client.confirmApplePurchase(accessToken, { signedTransaction: devAppleSignedTransaction(sku) })
}

Register OAuth redirect URI in Admin: {scheme}://auth/callback (from makeRedirectUri).

Gateway mode (dev only)

const client = createLeisureSaasClient({
  mode: "gateway",
  gatewayUrl: "http://127.0.0.1:8080",
  integrationApiKey: process.env.EXPO_PUBLIC_INTEGRATION_KEY!,
});

Do not ship Integration API Key in App Store / Play builds.

API

| Method | BFF | Gateway | |--------|-----|---------| | listPlans(token, platform) | ✅ | ✅ | | getSubscription(token) | ✅ | ✅ | | getEntitlement(token) | ✅ | ✅ | | confirmApplePurchase | ✅ | ✅ | | confirmGooglePurchase | ✅ | ✅ | | restoreApplePurchases | ✅ | ✅ | | registerDeviceToken | ✅ | ✅ | | unregisterDeviceToken | ✅ | ✅ | | sendNotification | ✅ | ✅ | | getAdsFeed | ✅ | ✅ | | recordAdEvents | ✅ | ✅ | | getPublicAdsFeed / recordPublicAdEvents | gateway URL + ads_pk | ✅ | | getQuotaUsage | ❌ (proxy on BFF) | ✅ | | consumeQuota | ❌ | ✅ | | checkPermission | ❌ | ✅ |

Dev store confirm

When billing store.verification_mode: dev:

import { devAppleSignedTransaction, devGooglePurchaseToken } from "@leisuresaas/expo";

await client.confirmApplePurchase(token, {
  signedTransaction: devAppleSignedTransaction("com.example.pro.monthly"),
  storeProductId: "com.example.pro.monthly",
});

await client.registerDeviceToken(token, {
  platform: mobilePlatform(),
  token: devDeviceToken(mobilePlatform()),
});
await client.sendNotification(token, {
  templateKey: "product_alert",
  vars: { message: "Hello from Expo" },
});

App version updates (Public App Config)

AI 接入指南plan/product-app-config-integration.md
平台方案plan/app-config-platform.md

未登录冷启动 / 回前台检查更新(app_cfg_pk)。客户端不要自写 semver,只读 update.required / update.recommended

Settings UI 由产品自绘(headless):SDK 只提供状态与动作;样式跟产品设置页一致。

import {
  AppUpdateProvider,
  useAppVersionSettings,
} from "@leisuresaas/expo";

<AppUpdateProvider
  publishableKey={process.env.EXPO_PUBLIC_APP_CFG_PK!}
  gatewayUrl={process.env.EXPO_PUBLIC_OAUTH_ISSUER!}
>
  {children}
</AppUpdateProvider>

// Settings — product-owned layout
function AppVersionSection() {
  const s = useAppVersionSettings();
  return (
    <YourCard>
      <YourTitle>{s.labels.settingsTitle}</YourTitle>
      <YourMuted>{s.displayVersion}</YourMuted>
      {s.message ? <YourBody>{s.message}</YourBody> : null}
      <YourRow>
        <YourLink onPress={() => void s.checkNow()} disabled={s.busy}>
          {s.busy || s.status === "checking" ? s.labels.checking : s.labels.checkForUpdates}
        </YourLink>
        {s.canOpenStore ? (
          <YourLink onPress={() => void s.openStore()}>{s.labels.openStore}</YourLink>
        ) : null}
      </YourRow>
    </YourCard>
  );
}

可选参考实现(无样式承诺):<AppVersionSettingsCard />,或 render prop:

<AppVersionSettingsCard>
  {(s) => <YourCard>{/* same fields as above */}</YourCard>}
</AppVersionSettingsCard>

| Env | 说明 | |-----|------| | EXPO_PUBLIC_APP_CFG_PK | Admin Access → Publishable keys(App config) | | EXPO_PUBLIC_OAUTH_ISSUER | Gateway 根 URL(与 OAuth issuer 同主机即可) |

可选:labels 覆盖弹窗文案;onUpdateRequired / onUpdateRecommended 自定义冷启动弹窗;skipInDev / skipOnWeb(默认 __DEV__ 与 Web 不弹窗)。required 不可 dismiss;recommendedlatest_version 持久化 dismiss(SecureStore)。

Platform ads (UI components)

AI 接入指南plan/product-ads-integration.md
Public Ads(v0.4.0+):未登录展示用 publishableKey;Integration feed 将于 2026-12-31 下线。

Public Ads(推荐 — 未登录可展示)

import {
  AdsProvider,
  AdBanner,
  createLeisureSaasClient,
  useLeisureSaasAuth,
} from "@leisuresaas/expo";

const client = createLeisureSaasClient({ bffBaseUrl: "https://api.myproduct.com" });

<AdsProvider
  client={client}
  publishableKey={process.env.EXPO_PUBLIC_ADS_PK!}
  publicAdsGatewayUrl="https://gateway.example.com"
  resolveAccessToken={resolveAccessToken}
>
  <AdBanner />
</AdsProvider>
  • publishableKey:Admin 创建的 ads_pk_...(可打进 App 包)
  • publicAdsGatewayUrl:gateway 根 URL(BFF 模式必填;gateway 模式可省略)
  • resolveAccessToken 可选;登录后 impression 可附带 user_id
  • session_id 由 SDK 自动生成并持久化(SecureStore)

Lower-level:

import { getPublicAdsFeed, recordPublicAdEvents, lineupIdFromSource, adsSurfaceKey, appBundleId } from "@leisuresaas/expo";

const ctx = {
  gatewayUrl: "https://gateway.example.com",
  publishableKey: "ads_pk_...",
  surfaceKey: adsSurfaceKey(),
  bundleId: appBundleId(),
};
const feed = await getPublicAdsFeed(ctx, "home_banner");
await recordPublicAdEvents(ctx, sessionId, [{
  adId: feed.ads[0].id,
  eventType: "impression",
  placementKey: feed.placement,
  lineupId: lineupIdFromSource(feed.source),
}]);

Integration Ads(deprecated — 需登录)

Feed returns type (text / image), type-specific layout (text_* / image_*), and rotation (none / fade / slide / stack). Each layout has a dedicated renderer (hero overlay, card shadow, footer pill, callout badge, etc.); customize via AdsProvider theme or per-Ad props.

import {
  AdsProvider,
  AdBanner,
  defaultAdsTheme,
  createLeisureSaasClient,
  useLeisureSaasAuth,
} from "@leisuresaas/expo";

const theme = {
  layouts: {
    image: { image_hero: { root: { borderRadius: 16 }, title: { color: "#111" } } },
    text: { text_callout: { root: { borderLeftColor: "#2563eb" } } },
  },
  rotation: { dotActive: { backgroundColor: "#2563eb" } },
};

<AdsProvider client={client} resolveAccessToken={resolveAccessToken} theme={theme}>
  <AdBanner contentStyle={{ marginVertical: 8 }} />
</AdsProvider>

Custom render (full control, SDK still tracks impression / click):

<Ad placement="home_banner" renderAd={({ ad, onPress, theme }) => (...)} />

Preview without network (labs / Storybook):

<AdPreview feed={mockFeed} trackImpressions={false} showIndicators />

GET /ads/feed returns click_url. Clients open it for navigation; impressions use POST /ads/events with placement_key / lineup_id (the Ad component handles this after 300ms dwell).

import {
  AdsProvider,
  AdBanner,
  createLeisureSaasClient,
  useLeisureSaasAuth,
} from "@leisuresaas/expo";

const client = createLeisureSaasClient({ bffBaseUrl: "https://api.myproduct.com" });

function Home() {
  const { resolveAccessToken } = useLeisureSaasAuth();

  return (
    <AdsProvider client={client} resolveAccessToken={resolveAccessToken}>
      <AdBanner />
    </AdsProvider>
  );
}

Lower-level API (custom UI):

const feed = await client.getAdsFeed(token, "home_banner");
await client.recordAdEvents(token, [{
  adId: feed.ads[0].id,
  eventType: "impression",
  placementKey: feed.placement,
  lineupId: lineupIdFromSource(feed.source),
}]);
// navigation: open feed.ads[0].click_url (no Bearer)

Plan permissions: not required for feed/events (Integration Auth only). Product backend manage APIs need OAuth scope ads:manage.

Upgrade @leisuresaas/[email protected] (breaking — lineup_id)

Requires gateway without HTTP group_id alias (deploy gateway ≥ 2026-07 lineup_id-only).

| 变更 | 迁移 | |------|------| | AdFeedSource.group_id | 使用 lineup_idlineupIdFromSource(feed.source) | | AdEventInput.groupId | 改为 lineupId | | Impression payload | 只发 lineup_id(SDK 内置;自定义 UI 勿再发 group_id) |

npm install @leisuresaas/[email protected]

Go 后端配套:go get github.com/leisuresaas/[email protected]platform.LineupIDFromSourceAdEventInput.LineupID)。

Upgrade @leisuresaas/[email protected] (feat — headless App version Settings)

设置页改为 headless-first:产品自绘 UI,不再依赖默认卡片样式。

| 新增 | 用法 | |------|------| | useAppVersionSettings | 设置页主路径:displayVersion / checkNow / openStore / message / canOpenStore | | AppVersionSettingsCard children render prop | (state) => ReactNode;无 children 时仍为参考布局 |

npm install @leisuresaas/[email protected]

Upgrade @leisuresaas/[email protected] (feat — App version updates)

Public App Config:冷启动 / 设置页版本检查(服务端 semver)。

| 新增 | 用法 | |------|------| | AppUpdateProvider / useAppUpdate / AppVersionSettingsCard | 见上文 App version updates;Settings 推荐 useAppVersionSettings(0.5.4+) | | EXPO_PUBLIC_APP_CFG_PK | Admin Access → Publishable keys(App config) |

npm install @leisuresaas/[email protected]

Upgrade @leisuresaas/[email protected] (fix — surface_key header)

Gateway Public / Integration ads 已改用 X-Ads-Surface-Key0.5.0 仍发 X-Ads-Platform 会导致 feed 失败)。

| 变更 | 迁移 | |------|------| | Public Ads 请求头 | X-Ads-PlatformX-Ads-Surface-Key | | PublicAdsRequestContext.platform | 改为 surfaceKeyplatform 仍可读作 fallback) | | Integration getAdsFeed / recordAdEvents | SDK 自动附带 X-Ads-Surface-Key: ios\|android |

npm install @leisuresaas/[email protected]
import { adsSurfaceKey } from "@leisuresaas/expo";

// PublicAdsRequestContext
{ surfaceKey: adsSurfaceKey(), publishableKey: "ads_pk_...", gatewayUrl: "..." }

Reference