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

expo-creem-integration

v1.0.2

Published

Creem payment integration for Expo apps — checkout sessions, subscriptions, licenses, customer portal, and deep-link handling. Zero native code required.

Downloads

27

Readme

expo-creem

Creem payment integration for Expo apps. Launch checkout sessions, manage subscriptions, handle licenses, open customer portals, and handle deep-link callbacks — all without ejecting or writing native code.

npm version License: MIT Expo SDK

Features

  • Checkout Sessions — open the Creem-hosted checkout via expo-web-browser and receive the result via deep link
  • Subscription Management — fetch, cancel, update, upgrade, pause, and resume subscriptions
  • License Key Management — activate, validate, and deactivate software licenses
  • Customer Portal — open the Creem billing portal for customers to manage payment methods
  • Products & Discounts — search products, retrieve discounts
  • React HooksuseCreemCheckout, useCreemCheckoutWithDeeplink, useCreemSubscription, useCreemProducts, useCreemLicense, useCreemCustomerPortal
  • Pre-built Components<CreemCheckoutButton>, <SubscriptionStatus>, <SubscriptionBadge>
  • Expo Config Plugin — zero-config URL scheme setup via expo prebuild
  • Server-side Helpers — Node.js / Edge-compatible client for your backend with retry logic
  • Webhook Support — signature verification and typed event handlers
  • Utility HelpersformatPrice, formatDate, formatBillingPeriod, formatRelativeTime, isSubscriptionActive
  • TypeScript First — all types match the Creem OpenAPI spec exactly
  • Cross-platform — iOS, Android, and Expo Web
  • Retry Logic — built-in exponential backoff for resilient API calls
  • PerformanceReact.memo, useMemo, useCallback throughout; cleanup in all effects

Requirements

  • Expo SDK 52+
  • Node.js 18+
  • A Creem account (creem.io)

Installation

npx expo install expo-creem-integration expo-web-browser expo-linking

Setup

1. Add the config plugin

In app.json / app.config.js, add expo-creem-integration to the plugins array. The plugin automatically registers your URL scheme on iOS and Android so deep links from the Creem checkout page reach your app.

{
  "expo": {
    "scheme": "myapp",
    "plugins": [
      ["expo-creem-integration", { "scheme": "myapp" }]
    ]
  }
}

Then run expo prebuild to apply the native changes.

Note: If you omit the scheme option, the plugin reads the top-level expo.scheme field automatically.

2. Wrap your app with <CreemProvider>

import { CreemProvider } from 'expo-creem-integration';

export default function App() {
  return (
    <CreemProvider apiKey="YOUR_API_KEY" environment="sandbox">
      {/* your app */}
    </CreemProvider>
  );
}

Use environment="sandbox" for testing (points at https://test-api.creem.io) and environment="production" (or omit it) for live payments.

Provider Options

| Prop | Type | Default | Description | |---|---|---|---| | apiKey | string | required | Your Creem API key | | environment | 'production' \| 'sandbox' | 'production' | Which API environment to use | | baseUrl | string | — | Override the base URL (e.g. proxy through your own backend) | | retries | number | 2 | Number of retry attempts for failed requests | | retryDelay | number | 300 | Base delay in ms for exponential backoff |


Usage

Launch a checkout with the pre-built button

import { CreemCheckoutButton } from 'expo-creem-integration';

<CreemCheckoutButton
  options={{
    product_id: 'prod_xxx',
    success_url: 'myapp://creem/success',
    customer: { email: '[email protected]' },
  }}
  title="Subscribe Now"
  loadingTitle="Opening checkout..."
  variant="primary"  // 'primary' | 'secondary' | 'outline'
  size="large"       // 'small' | 'medium' | 'large'
/>

Launch a checkout with the hook

import { useCreemCheckout } from 'expo-creem-integration';

function SubscribeButton() {
  const { status, error, startCheckout, reset } = useCreemCheckout({
    product_id: 'prod_xxx',
    success_url: 'myapp://creem/success',
    onComplete: (session) => console.log('Done', session.id),
    onCancel: () => console.log('Cancelled'),
    onError: (err) => console.error(err),
  });

  return (
    <Button
      onPress={startCheckout}
      disabled={status === 'loading'}
      title={status === 'loading' ? 'Loading...' : 'Subscribe'}
    />
  );
}

Deep-link variant (for external routers)

useCreemCheckoutWithDeeplink opens the browser and then resolves via an incoming URL event rather than blocking. Useful with Expo Router or React Navigation.

import { useCreemCheckoutWithDeeplink } from 'expo-creem-integration';

const { startCheckout, status } = useCreemCheckoutWithDeeplink({
  product_id: 'prod_xxx',
  success_url: 'myapp://creem/success',
  onComplete: (session) => { /* ... */ },
});

Show subscription status

import { SubscriptionStatus, SubscriptionBadge } from 'expo-creem-integration';

// Full status with custom renderers
<SubscriptionStatus
  subscriptionId="sub_xxx"
  showDetails
  pollInterval={30_000}
  onStatusChange={(s) => console.log('Status changed:', s)}
  renderActive={(sub) => (
    <Text>Active — renews {new Date(sub.current_period_end_date).toLocaleDateString()}</Text>
  )}
  renderTrialing={(sub) => <Text>Free trial active!</Text>}
  renderCanceling={(sub) => <Text>Subscription ending soon</Text>}
  renderPaused={() => <Text>Subscription paused</Text>}
  renderInactive={() => <Text>No active subscription</Text>}
/>

// Compact badge
<SubscriptionBadge subscriptionId="sub_xxx" pollInterval={30_000} />

Manage subscriptions

import { useCreemSubscription } from 'expo-creem-integration';

const { subscription, status, isLoading, cancelSubscription, updateSubscription, upgradeSubscription, pauseSubscription, resumeSubscription } =
  useCreemSubscription('sub_xxx');

// Cancel at end of billing period:
await cancelSubscription({ mode: 'scheduled', onExecute: 'cancel' });

// Cancel immediately:
await cancelSubscription({ mode: 'immediate' });

// Update seat count:
await updateSubscription({
  items: [{ id: 'item_xxx', units: 5 }],
  update_behavior: 'proration-charge-immediately',
});

// Upgrade to a different product:
await upgradeSubscription({
  product_id: 'prod_premium',
  update_behavior: 'proration-charge-immediately',
});

// Pause/resume:
await pauseSubscription();
await resumeSubscription();

Browse products

import { useCreemProducts } from 'expo-creem-integration';

const { products, isLoading, hasMore, loadMore, refetch } = useCreemProducts({
  page: 1,
  pageSize: 10,
});

License key management

import { useCreemLicense } from 'expo-creem-integration';

const { license, status, isLoading, activate, validate, deactivate, reset } = useCreemLicense();

// Activate a license on first use:
await activate({ key: 'license_key_here', instance_name: 'my-macbook-pro' });

// Validate on app startup:
await validate({ key: 'license_key_here', instance_id: 'inst_xxx' });

// Deactivate when switching devices:
await deactivate({ key: 'license_key_here', instance_id: 'inst_xxx' });

Customer portal

import { useCreemCustomerPortal } from 'expo-creem-integration';

const { openPortal, isLoading } = useCreemCustomerPortal('cust_xxx');

// Opens an in-app browser with the Creem billing portal:
await openPortal();

Utility helpers

import { formatPrice, formatDate, formatRelativeTime, isSubscriptionActive } from 'expo-creem-integration';

formatPrice(1999, 'USD');           // "$19.99"
formatPrice(1999, 'EUR', 'de-DE');  // "19,99 €"
formatDate('2026-03-30T00:00:00Z'); // "Mar 30, 2026"
formatRelativeTime('2026-04-30T00:00:00Z'); // "in 31 days"
isSubscriptionActive('active');     // true
isSubscriptionActive('canceled');   // false

API Reference

Hooks

useCreemCheckout(options) / useCreemCheckoutWithDeeplink(options)

| Field | Type | Description | |---|---|---| | product_id | string | Required. The Creem product ID | | customer | { id?: string; email?: string } | Pre-fill customer info | | units | number | Quantity | | discount_code | string | Pre-fill a discount code | | success_url | string | Deep-link URL to redirect to on success | | request_id | string | Idempotency key | | metadata | Record<string, string> | Arbitrary metadata | | custom_fields | CreemCustomFieldRequest[] | Custom field definitions | | onComplete | (session) => void | Called after a successful checkout | | onCancel | () => void | Called when the user cancels | | onError | (error) => void | Called on error | | autoCloseDelay | number | Delay in ms before firing onComplete |

Return value:

{
  status: 'idle' | 'loading' | 'success' | 'canceled' | 'error';
  session: CreemCheckoutSession | null;
  error: CreemError | null;
  startCheckout: () => Promise<void>;
  reset: () => void;
}

useCreemSubscription(subscriptionId, options?)

| Option | Type | Description | |---|---|---| | pollInterval | number | Re-fetch every N ms. 0 = no polling | | onStatusChange | (status) => void | Fires when the status changes | | enabled | boolean | Enable/disable the fetch. Default: true |

Return value:

{
  subscription: CreemSubscription | null;
  status: SubscriptionStatus | null;
  isLoading: boolean;
  error: CreemError | null;
  lastUpdated: Date | null;
  refetch: () => Promise<void>;
  cancelSubscription: (options?) => Promise<void>;
  updateSubscription: (options) => Promise<void>;
  upgradeSubscription: (options) => Promise<void>;
  pauseSubscription: () => Promise<void>;
  resumeSubscription: () => Promise<void>;
}

useCreemProducts(options?)

| Option | Type | Description | |---|---|---| | page | number | Page number. Default: 1 | | pageSize | number | Items per page. Default: 10 | | enabled | boolean | Enable/disable fetching. Default: true | | pollInterval | number | Poll interval in ms. Default: 0 |

Return value:

{
  products: CreemProduct[];
  total: number;
  page: number;
  hasMore: boolean;
  isLoading: boolean;
  error: CreemError | null;
  refetch: () => Promise<void>;
  loadMore: () => Promise<void>;
}

useCreemLicense()

Return value:

{
  license: CreemLicenseKey | null;
  instance: CreemLicenseInstance | null;
  status: 'active' | 'inactive' | 'expired' | 'disabled' | null;
  isLoading: boolean;
  error: CreemError | null;
  activate: (options: CreemActivateLicenseOptions) => Promise<void>;
  validate: (options: CreemValidateLicenseOptions) => Promise<void>;
  deactivate: (options: CreemDeactivateLicenseOptions) => Promise<void>;
  reset: () => void;
}

useCreemCustomerPortal(customerId)

Return value:

{
  portalUrl: string | null;
  isLoading: boolean;
  error: CreemError | null;
  openPortal: () => Promise<void>;
  generatePortalUrl: () => Promise<string | null>;
  reset: () => void;
}

Components

<CreemProvider>

| Prop | Type | Default | Description | |---|---|---|---| | apiKey | string | required | Your Creem API key | | environment | 'production' \| 'sandbox' | 'production' | API environment | | baseUrl | string | — | Override base URL | | retries | number | 2 | Retry count | | retryDelay | number | 300 | Retry backoff base (ms) |

<CreemCheckoutButton>

| Prop | Type | Default | Description | |---|---|---|---| | options | UseCreemCheckoutOptions | required | Checkout options | | title | string | 'Subscribe' | Button label | | loadingTitle | string | — | Label shown while loading | | disabled | boolean | false | Disable the button | | variant | 'primary' \| 'secondary' \| 'outline' | 'primary' | Visual style | | size | 'small' \| 'medium' \| 'large' | 'medium' | Button size | | style | ViewStyle | — | Custom container style | | textStyle | TextStyle | — | Custom text style |

<SubscriptionStatus>

| Prop | Type | Default | Description | |---|---|---|---| | subscriptionId | string \| null | required | Subscription ID | | pollInterval | number | 0 | Poll interval (ms) | | showDetails | boolean | false | Show renewal/cancel dates | | onStatusChange | (status) => void | — | Status change callback | | renderLoading | () => ReactNode | — | Custom loading renderer | | renderError | (error) => ReactNode | — | Custom error renderer | | renderInactive | () => ReactNode | — | Custom inactive renderer | | renderActive | (sub) => ReactNode | — | Custom active renderer | | renderTrialing | (sub) => ReactNode | — | Custom trial renderer | | renderCanceling | (sub) => ReactNode | — | Custom canceling renderer | | renderPaused | (sub) => ReactNode | — | Custom paused renderer |

<SubscriptionBadge>

| Prop | Type | Default | Description | |---|---|---|---| | subscriptionId | string \| null | required | Subscription ID | | pollInterval | number | 0 | Poll interval (ms) | | showLabel | boolean | true | Show text label |

Types

type CheckoutStatus = 'pending' | 'processing' | 'completed' | 'expired';
type SubscriptionStatus = 'active' | 'canceled' | 'unpaid' | 'past_due' | 'paused' | 'trialing' | 'scheduled_cancel';
type LicenseStatus = 'active' | 'inactive' | 'expired' | 'disabled';
type BillingType = 'one_time' | 'recurring';
type BillingPeriod = 'day' | 'week' | 'month' | 'year';
type UpdateBehavior = 'proration-charge-immediately' | 'proration-charge' | 'proration-none';

interface CreemCancelSubscriptionOptions {
  mode?: 'immediate' | 'scheduled';
  onExecute?: 'cancel' | 'pause';
}

interface CreemUpdateSubscriptionOptions {
  items: Array<{ id: string; units: number }>;
  update_behavior?: UpdateBehavior;
}

interface CreemUpgradeSubscriptionOptions {
  product_id: string;
  update_behavior?: UpdateBehavior;
}

Server-side Usage

Use CreemServerClient in your Node.js / Edge backend to create checkout sessions without exposing your API key to clients.

import { CreemServerClient } from 'expo-creem-integration/server';

const creem = new CreemServerClient({ apiKey: process.env.CREEM_API_KEY! });
// or for the test environment:
const creem = CreemServerClient.sandbox({ apiKey: process.env.CREEM_API_KEY! });

// Create a checkout session
const session = await creem.createCheckoutSession({
  product_id: 'prod_xxx',
  customer: { email: '[email protected]' },
  success_url: 'https://yourapp.com/success',
  metadata: { userId: '42' },
});
console.log(session.checkout_url); // redirect the user here

// Full API coverage:
await creem.getSubscription('sub_xxx');
await creem.cancelSubscription('sub_xxx', { mode: 'scheduled' });
await creem.updateSubscription('sub_xxx', { items: [...] });
await creem.upgradeSubscription('sub_xxx', { product_id: 'prod_new' });
await creem.pauseSubscription('sub_xxx');
await creem.resumeSubscription('sub_xxx');
await creem.getProduct('prod_xxx');
await creem.searchProducts(1, 10);
await creem.createProduct({ name: 'Pro', price: 1999, currency: 'USD', billing_type: 'recurring' });
await creem.getCustomer('cust_xxx');
await creem.generateCustomerPortalLink('cust_xxx');
await creem.activateLicense({ key: 'xxx', instance_name: 'server-1' });
await creem.validateLicense({ key: 'xxx', instance_id: 'inst_xxx' });
await creem.deactivateLicense({ key: 'xxx', instance_id: 'inst_xxx' });
await creem.getDiscount('disc_xxx');
await creem.getDiscountByCode('SUMMER2024');
await creem.createDiscount({ name: 'Sale', code: 'SALE20', type: 'percentage', percentage: 20 });
await creem.deleteDiscount('disc_xxx');
await creem.getTransaction('txn_xxx');
await creem.searchTransactions('cust_xxx');

Webhook verification

import { validateWebhookSignature, parseWebhookEvent, processWebhookEvent } from 'expo-creem-integration/server';

// Express example
app.post('/webhooks/creem', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['creem-signature'] as string;
  const payload = req.body.toString();

  if (!validateWebhookSignature(payload, signature, process.env.CREEM_WEBHOOK_SECRET!)) {
    return res.status(400).send('Invalid signature');
  }

  const event = parseWebhookEvent(payload);
  // handle the event ...
  res.status(200).send('OK');
});

// Or use the typed handler system:
await processWebhookEvent(event, {
  'checkout.completed': async (data) => {
    console.log('Payment completed:', data);
  },
  'subscription.active': async (data) => {
    console.log('Subscription activated:', data);
  },
  'subscription.canceled': async (data) => {
    console.log('Subscription canceled:', data);
  },
}, (event) => {
  console.log('Unhandled event:', event.eventType);
});

Environment Variables

# Client (prefix with EXPO_PUBLIC_ to expose to the RN bundle)
EXPO_PUBLIC_CREEM_API_KEY=your_api_key
EXPO_PUBLIC_PRODUCT_ID=prod_xxx

# Server only
CREEM_API_KEY=your_api_key
CREEM_WEBHOOK_SECRET=your_webhook_secret

Example App

cd example
npm install
npx expo start

Architecture

expo-creem-integration/
├── src/
│   ├── types/             # All TypeScript definitions (25+ interfaces)
│   ├── hooks/             # React hooks (6 hooks)
│   │   ├── useCreemCheckout.ts
│   │   ├── useCreemSubscription.ts
│   │   ├── useCreemProducts.ts
│   │   ├── useCreemLicense.ts
│   │   └── useCreemCustomerPortal.ts
│   ├── components/        # UI components
│   │   ├── CreemCheckoutButton.tsx
│   │   └── SubscriptionStatus.tsx
│   ├── utils/             # Client + helpers
│   │   ├── client.ts      # CreemClient with retry logic
│   │   └── context.tsx    # Provider + formatting helpers
│   ├── server/            # Server-side helpers
│   │   └── creem-server.ts
│   ├── launcher.ts        # Browser launch logic
│   └── index.ts           # Barrel exports
├── plugin/
│   └── index.js           # Expo config plugin (pure JS)
├── example/
│   └── App.tsx            # Full demo app
├── app.plugin.js          # Plugin entry point
└── package.json

Platform Support

| Platform | Support | |---|---| | iOS | Full (SFSafariViewController via expo-web-browser) | | Android | Full (Chrome Custom Tabs via expo-web-browser) | | Web | Full (redirect flow) |

License

MIT