Overview

Get started with mobile billing in TurboStarter.

Implementing mobile billing can be challenging, especially when you need to handle cross-platform compatibility and comply with the different requirements of the App Store and Google Play.

Be cautious!

Apple has strict guidelines regarding external payment systems and may reject your app if you aggressively redirect users to web-based payment flows. Make sure to review the App Store Review Guidelines carefully and consider implementing native in-app purchases for iOS users to ensure compliance.

TurboStarter's mobile billing is designed around native in-app purchases, so it's fully compliant and ready to use out of the box. However, please be mindful when modifying payment-related features in your mobile app.

TurboStarter makes this easier by providing native in-app billing through RevenueCat and Superwall. These providers abstract the native store APIs, so you can sell subscriptions and manage entitlements without integrating each store SDK yourself or relying on web-based checkout flows.

Billing Providers

Providers

To support both iOS and Android, TurboStarter includes the following providers for mobile billing:

Each provider is configured and set up behind a unified API. You can switch providers by changing the exports, or introduce your own provider without breaking billing-related logic.

Depending on the provider you choose, you'll need to set the corresponding environment variables. By default, the billing package uses RevenueCat. Alternatively, you can use Superwall.

Configuration

Most configuration is done provider-side, following the philosophy that you should be able to change plan configuration (and other settings) without having to release a new version of your app. This is especially useful for A/B testing to determine which offering performs better.

To learn more about configuring products, offerings, and cross-platform support, check the following sections:

Displaying a paywall

The paywall is a crucial part of the billing flow - it displays offerings to the user and lets you trigger purchases/restores at different points in the user journey.

To present a paywall in your app, use the usePaywall hook from the @workspace/billing-mobile package. This hook returns the paywall result directly from the configured provider.

paywall.tsx
import { usePaywall } from "@workspace/billing-mobile";

export default function Paywall() {
  const { present, result } = usePaywall();

  return (
    <>
      <Pressable
        onPress={() =>
          present({
            trigger: "onboarding",
          })
        }
      >
        <Text>Present paywall</Text>
      </Pressable>
      <Text>{result.status}</Text>
    </>
  );
}

Don't forget to pass the trigger parameter, as it's used to identify the template/campaign that needs to be triggered on the provider's side.

If you want to react to paywall lifecycle events, you can pass additional callbacks to usePaywall:

paywall.tsx
import { usePaywall } from "@workspace/billing-mobile";

const { present, result } = usePaywall({
  onPresent: () => {},
  onDismiss: () => {},
  onPurchase: () => {},
  onRestore: () => {},
  onSkip: () => {},
  onError: (error) => {},
});

They're called automatically when the paywall enters a specific state - for example, when the user purchases a plan, onPurchase will be called.

Fetching customer status

After a user purchases a plan in-app, you'll often want to fetch their current billing summary (subscription status, entitlements, credits) to:

  • gate features in your UI
  • show “Current plan” / “Manage subscription” states
  • keep the app in sync across sessions and devices

You can do this via the billing me endpoint (/api/billing/me) using the mobile API client.

To do so, call /api/billing/me to fetch the user's billing summary:

customer-screen.tsx
import { handle } from "@workspace/api/utils";
import { getActivePlan } from "@workspace/billing";

import { api } from "~/lib/api";

export default function CustomerScreen() {
  const summary = useQuery({
    queryKey: ["me"],
    queryFn: handle(api.billing.me.$get),
  });

  if (!summary.data) {
    return null;
  }

  const plan = getActivePlan(summary.data);

  return (
    <View>
      <Text>{plan}</Text>
    </View>
  );
}

Alternatively, you can treat the provider as the source of truth - for example, if you only need to check whether a user has a specific entitlement and want to delegate the rest to the native store handling. To do this, use the useCustomer hook, which returns customer data from the configured provider (RevenueCat or Superwall).

customer-screen.tsx
import { useCustomer } from "@workspace/billing-mobile";

export default function CustomerScreen() {
  const { entitlements } = useCustomer();

  const hasPremium = entitlements.some(
    (entitlement) => entitlement.id === "premium" && entitlement.active,
  );

  /* ... */
}

Which approach you choose depends on how much you want to handle in your backend vs. the native store/provider layer. By default, we recommend using the API to handle billing-related logic because it gives you the most flexibility and control. If you need something native-specific, use the built-in hooks (like useCustomer and usePaywall) to communicate directly with the configured provider.

How is this guide?

Last updated on

On this page

Ship your startup everywhere. In minutes.Get TurboStarter