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

@heymantle/react-native

v0.1.0

Published

React Native SDK for Mantle billing with Apple IAP support

Readme

@heymantle/react-native

React Native SDK for Mantle billing with Apple In-App Purchase support.

Features

  • 🍎 Apple IAP — Wraps react-native-iap for real App Store purchases
  • 🧪 Simulation mode — Full purchase flow testing without Apple credentials or a device
  • 🔐 Feature gatinguseFeature() hook for boolean and limit-based feature checks
  • ♻️ Restore purchases — Standard iOS restore flow synced with Mantle backend
  • 🔄 Lifecycle simulation — Test renewals, expirations, refunds, and more

Installation

npm install @heymantle/react-native @heymantle/client
cd ios && pod install

This installs react-native-iap automatically as a dependency.

Authentication

The SDK requires a customerApiToken — a customer-scoped token that is safe to use on the client. Your backend is responsible for obtaining this token by calling Mantle's identify endpoint with your secret API key:

# Your backend calls identify with your secret API key
POST https://appapi.heymantle.com/v1/identify
X-Mantle-App-Api-Key: your-secret-api-key

{ "platform": "...", "platformId": "...", "name": "...", "email": "..." }

# Response includes the customer API token
{ "apiToken": "cust_abc123..." }

Your backend then passes apiToken to the React Native app (e.g. as part of your auth/login response). The app never needs your secret API key.

Quick Start

1. Wrap your app with MantleProvider

import { MantleProvider } from '@heymantle/react-native';

function App() {
  // customerApiToken comes from your backend's identify call
  const { customerApiToken } = useAuth();

  return (
    <MantleProvider
      appId="your-mantle-app-id"
      customerApiToken={customerApiToken}
    >
      <YourApp />
    </MantleProvider>
  );
}

2. Use the hooks

import { useMantle, useFeature } from '@heymantle/react-native';

function HomeScreen() {
  const { plans, subscription, purchase } = useMantle();
  const { enabled: hasAdvanced } = useFeature('advanced_analytics');

  return (
    <View>
      {!subscription && plans.map(plan => (
        <Button
          key={plan.id}
          title={`Subscribe to ${plan.name}`}
          onPress={() => purchase(plan.id)}
        />
      ))}

      {subscription && <Text>Subscribed to {subscription.plan?.name}</Text>}

      {hasAdvanced && <AdvancedAnalytics />}
    </View>
  );
}

Simulation Mode

Simulation mode must be explicitly enabled via the simulationMode prop. It is never enabled automatically — if Apple credentials aren't configured on your Mantle app, purchases will fail rather than silently falling back to simulation.

<MantleProvider
  appId="your-mantle-app-id"
  customerApiToken={token}
  simulationMode={__DEV__} // Only enable in development
>
  <YourApp />
</MantleProvider>

Simulating Lifecycle Events

In simulation mode, you can test subscription lifecycle events:

import { useMantle } from '@heymantle/react-native';

function DevTools() {
  const { subscription, simulateEvent, billing } = useMantle();

  if (!billing.simulationMode || !subscription) return null;

  return (
    <View>
      <Text>Simulation Dev Tools</Text>
      <Button title="Simulate Renewal" onPress={() => simulateEvent('DID_RENEW')} />
      <Button title="Simulate Expiry" onPress={() => simulateEvent('EXPIRED')} />
      <Button title="Simulate Payment Failure" onPress={() => simulateEvent('DID_FAIL_TO_RENEW')} />
      <Button title="Simulate Refund" onPress={() => simulateEvent('REFUND')} />
      <Button title="Simulate Grace Period Expired" onPress={() => simulateEvent('GRACE_PERIOD_EXPIRED')} />
    </View>
  );
}

Supported events: DID_RENEW, DID_CHANGE_RENEWAL_STATUS, EXPIRED, DID_FAIL_TO_RENEW, GRACE_PERIOD_EXPIRED, REFUND

Purchase Flow

The purchase() function works identically in both modes:

import { usePurchase } from '@heymantle/react-native';

function SubscriptionScreen() {
  const { purchase, restore, purchasing, restoring, error, clearError } = usePurchase();

  return (
    <View>
      <Button
        title={purchasing ? 'Processing...' : 'Subscribe — $9.99/mo'}
        disabled={purchasing}
        onPress={() => purchase('plan-id')}
      />

      <Button
        title={restoring ? 'Restoring...' : 'Restore Purchases'}
        disabled={restoring}
        onPress={restore}
      />

      {error && (
        <View>
          <Text>Error: {error.message}</Text>
          <Button title="Dismiss" onPress={clearError} />
        </View>
      )}
    </View>
  );
}

Feature Gating

import { useFeature } from '@heymantle/react-native';

function FeatureGatedScreen() {
  // Boolean feature
  const { enabled: canExport } = useFeature('can_export');

  // Limit feature
  const { enabled, limit } = useFeature('monthly_exports', { count: currentExports });

  if (!enabled) {
    return (
      <View>
        <Text>You've reached your export limit ({limit} per month)</Text>
        <UpgradeButton />
      </View>
    );
  }

  return <ExportTool />;
}

API Reference

<MantleProvider>

| Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | appId | string | ✅ | — | Your Mantle App ID | | customerApiToken | string | ✅ | — | Customer API token obtained from your backend (see Authentication) | | apiUrl | string | — | https://appapi.heymantle.com/v1 | Mantle API URL | | simulationMode | boolean | — | false | Enable simulation mode for testing without store credentials | | waitForCustomer | boolean | — | false | Block rendering until customer is loaded | | loadingComponent | ReactNode | — | null | Component to show while loading (when waitForCustomer is true) |

useMantle()

Returns the full Mantle context:

| Property | Type | Description | |----------|------|-------------| | client | MantleClient | The underlying MantleClient instance | | customer | Customer \| null | Current customer data | | subscription | Subscription \| null | Current subscription | | plans | Plan[] | Available plans | | features | Record<string, Feature> | Customer features | | loading | boolean | Whether customer data is loading | | billing | BillingState | Billing state (simulationMode, environment) | | purchase(planId) | (string) => Promise<PurchaseResult> | Purchase a plan | | restore() | () => Promise<RestoreResult> | Restore purchases | | cancelSubscription(reason?) | (string?) => Promise | Cancel subscription | | simulateEvent(event) | (SimulateEvent) => Promise<SimulateResult> | Simulate lifecycle event (simulation mode only) | | openSubscriptionManagement() | () => Promise<void> | Open platform subscription settings | | refetch() | () => Promise<void> | Refetch customer data | | isFeatureEnabled(key, count?) | (string, number?) => boolean | Check if feature is enabled | | limitForFeature(key) | (string) => number | Get feature limit (-1 if none) | | purchasing | boolean | Whether a purchase is in flight | | restoring | boolean | Whether a restore is in flight | | error | Error \| null | Last error |

useFeature(featureKey, options?)

| Return | Type | Description | |--------|------|-------------| | enabled | boolean | Whether the feature is enabled | | limit | number | The limit value (-1 if not a limit feature) | | value | any | Raw feature value |

usePurchase()

| Return | Type | Description | |--------|------|-------------| | purchase(planId) | (string) => Promise<PurchaseResult> | Purchase a plan | | restore() | () => Promise<RestoreResult> | Restore purchases | | purchasing | boolean | Whether a purchase is in flight | | restoring | boolean | Whether a restore is in flight | | error | Error \| null | Last error | | clearError() | () => void | Clear the last error | | simulationMode | boolean | Whether in simulation mode |

Important Notes

  1. Apple subscriptions cannot be cancelled programmatically. cancelSubscription() sets cancelAtPeriodEnd: true in Mantle but does NOT cancel with Apple. Users must cancel via iOS Settings → Subscriptions. When downgrading to a free plan while an Apple subscription is active, purchase() returns { success: false, storeCancelRequired: true } — the user must cancel in iOS Settings first:

    const result = await purchase(freePlanId);
    if (result.storeCancelRequired) {
      Alert.alert(
        'Cancel First',
        'To switch to the free plan, cancel your subscription in iOS Settings → Subscriptions. Your current plan will remain active until the end of your billing period.',
        [{ text: 'Open Settings', onPress: () => Linking.openURL('https://apps.apple.com/account/subscriptions') },
         { text: 'OK' }]
      );
    }
  2. react-native-iap is dynamically imported — it's loaded at runtime only when needed (real mode), not at module level.

  3. Simulation mode must be explicitly enabled via the simulationMode prop. It defaults to false and is never auto-enabled.

  4. Transactions are always finished — in real mode, the SDK calls finishTransaction() after Mantle verification to prevent Apple from re-delivering the transaction.

License

ISC