> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shipnative.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Payments

> In-app purchases and subscriptions with RevenueCat

Shipnative uses [RevenueCat](https://www.revenuecat.com/) for subscriptions on iOS, Android, and Web. The payment UI and subscription store are pre-built.

## Setup

```bash theme={null}
yarn setup
```

The wizard prompts for your RevenueCat API keys (iOS, Android, Web).

Or manually add to `apps/app/.env`:

```bash theme={null}
EXPO_PUBLIC_REVENUECAT_IOS_KEY=your_ios_key
EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=your_android_key
EXPO_PUBLIC_REVENUECAT_WEB_KEY=your_web_key
```

**Without keys:** Mock mode simulates purchases. Connect real RevenueCat before production to test sandbox purchases.

## RevenueCat Configuration

1. Create a project at [app.revenuecat.com](https://app.revenuecat.com/)
2. Link your App Store Connect and Google Play Console apps
3. Get API keys from **Project Settings** → **API Keys**
4. Create products in App Store Connect / Google Play Console
5. Add products to an **Offering** (typically "default") in RevenueCat
6. Create an **Entitlement** (e.g., "pro") and link your products to it

For detailed setup, see [RevenueCat Getting Started](https://www.revenuecat.com/docs/getting-started).

## Usage

The app auto-detects the platform and uses the appropriate RevenueCat SDK.

### Subscription Store

```typescript theme={null}
import { useSubscriptionStore } from '@/stores/subscriptionStore'

function MyComponent() {
  const {
    isPro,               // Is user subscribed?
    packages,            // Available subscription packages
    loading,             // Purchase in progress?
    purchasePackage,     // Start a purchase
    restorePurchases,    // Restore previous purchases
    addLifecycleListener // Listen to subscription events (new!)
  } = useSubscriptionStore()
}
```

### Gating Features

```typescript theme={null}
import { useSubscriptionStore } from '@/stores/subscriptionStore'

function PremiumFeature() {
  const { isPro } = useSubscriptionStore()

  if (!isPro) {
    return <PaywallScreen />
  }

  return <PremiumContent />
}
```

### Custom Purchase UI

```typescript theme={null}
import { getPromotionalOfferText, calculateSavings } from '@/utils'

function CustomPricing() {
  const { packages, purchasePackage, loading } = useSubscriptionStore()

  return (
    <View>
      {packages.map((pkg, index) => {
        // Calculate savings for annual plans
        const monthlyPkg = packages.find(p => p.billingPeriod === 'monthly')
        const savings = pkg.billingPeriod === 'annual' && monthlyPkg
          ? calculateSavings(monthlyPkg.price * 12, pkg.price)
          : null

        // Get promotional offer text (free trials, intro pricing)
        const promoText = getPromotionalOfferText(pkg)

        return (
          <View key={pkg.id}>
            {/* Show savings badge */}
            {savings > 0 && <Badge>Save {savings}%</Badge>}

            {/* Show promotional offer */}
            {promoText && <Text>{promoText}</Text>}

            {/* Free trial badge */}
            {pkg.freeTrialPeriod && <Text>🎉 {pkg.freeTrialPeriod} free</Text>}

            <Button
              title={`${pkg.title} - ${pkg.priceString}`}
              onPress={() => purchasePackage(pkg)}
              disabled={loading}
            />
          </View>
        )
      })}
    </View>
  )
}
```

### Subscription Lifecycle Events

Track subscription events like trial starts, renewals, and cancellations:

```typescript theme={null}
import { useEffect } from 'react'
import { useSubscriptionStore } from '@/stores/subscriptionStore'

function SubscriptionTracker() {
  const addLifecycleListener = useSubscriptionStore(s => s.addLifecycleListener)

  useEffect(() => {
    const unsubscribe = addLifecycleListener((event) => {
      console.log('Event:', event.event) // trial_started, subscription_renewed, etc.

      // Trigger actions based on events
      switch (event.event) {
        case 'trial_started':
          analytics.track('Trial Started')
          break
        case 'subscription_cancelled':
          showRetentionOffer()
          break
        case 'billing_issue':
          showPaymentUpdateAlert()
          break
      }
    })

    return unsubscribe // Cleanup
  }, [])

  return null
}
```

**Available events:**

* `trial_started` - Free trial started
* `trial_converted` - Trial converted to paid
* `trial_cancelled` - Trial cancelled
* `subscription_started` - New subscription
* `subscription_renewed` - Auto-renewed
* `subscription_cancelled` - Cancelled (still active until expiry)
* `subscription_expired` - Access ended
* `subscription_restored` - Restored from previous purchase
* `billing_issue` - Payment problem detected

### Price Localization

Helper functions for proper price formatting:

```typescript theme={null}
import {
  formatLocalizedPrice,
  calculateSavings,
  getMonthlyEquivalent,
  formatExpirationStatus
} from '@/utils'

// Format prices with user's locale
const price = formatLocalizedPrice(9.99, 'USD') // "$9.99"
const euroPrice = formatLocalizedPrice(9.99, 'EUR', 'de-DE') // "9,99 €"

// Calculate savings percentage
const savings = calculateSavings(119.88, 99.99) // 17

// Get monthly equivalent for annual plans
const monthlyEquiv = getMonthlyEquivalent(99.99, 'annual') // 8.33

// Format subscription expiration
const status = formatExpirationStatus(customerInfo) // "Renews in 5 days"
```

## Advanced Features

### Promotional Offers & Intro Pricing

RevenueCat automatically detects and displays promotional offers configured in App Store Connect or Google Play Console:

```typescript theme={null}
import { useSubscription } from '@/hooks/useSubscription'

function PricingCard() {
  const { packages } = useSubscription()

  return packages.map(pkg => {
    const product = pkg.product

    // Check for free trial
    if (product.freeTrialPeriod) {
      return (
        <Text>
          {product.freeTrialPeriod.value} {product.freeTrialPeriod.unit} free trial
          then {product.priceString}/{product.subscriptionPeriod}
        </Text>
      )
    }

    // Check for intro pricing
    if (product.introPrice) {
      return (
        <Text>
          {product.introPrice.priceString} for {product.introPrice.period} {product.introPrice.periodUnit}
          then {product.priceString}
        </Text>
      )
    }

    // Regular pricing
    return <Text>{product.priceString}/{product.subscriptionPeriod}</Text>
  })
}
```

### Lifecycle Event Details

The `addCustomerInfoUpdateListener` provides detailed event tracking:

```typescript theme={null}
import { subscriptionService } from '@/services/revenuecat'

// Available event types
type SubscriptionEvent =
  | 'trial_started'        // Free trial activated
  | 'trial_converted'      // Trial converted to paid
  | 'trial_cancelled'      // Trial cancelled before conversion
  | 'subscription_started' // New paid subscription
  | 'subscription_renewed' // Auto-renewal succeeded
  | 'subscription_cancelled' // User cancelled (still active until expiry)
  | 'subscription_expired' // Subscription ended (no longer active)
  | 'subscription_restored' // Purchase restored from another device
  | 'billing_issue'        // Payment failed or grace period started
  | 'product_changed'      // Upgraded/downgraded plans

// Listen to all events
subscriptionService.addCustomerInfoUpdateListener((customerInfo) => {
  const status = customerInfo.status
  const expirationDate = customerInfo.expirationDate
  const isTrial = customerInfo.isTrial

  // Track in analytics
  trackEvent('subscription_status_changed', {
    status,
    isTrial,
    expiresAt: expirationDate,
  })
})
```

### Price Localization Helpers

Format prices correctly for international users:

```typescript theme={null}
import {
  formatLocalizedPrice,
  calculateSavings,
  getMonthlyEquivalent,
  formatExpirationStatus
} from '@/utils/subscriptionHelpers'

// Format with user's locale
const priceUSD = formatLocalizedPrice(9.99, 'USD')        // "$9.99"
const priceEUR = formatLocalizedPrice(9.99, 'EUR', 'de-DE') // "9,99 €"
const priceJPY = formatLocalizedPrice(1200, 'JPY', 'ja-JP') // "¥1,200"

// Calculate discount percentage
const savings = calculateSavings(119.88, 99.99) // 17% savings

// Show monthly equivalent for annual plans
const monthlyEquiv = getMonthlyEquivalent(99.99, 12) // "$8.33/mo"

// Format subscription status
const status = formatExpirationStatus({
  expirationDate: '2026-02-12',
  willRenew: true,
  isActive: true,
}) // "Renews on Feb 12, 2026"

const expiredStatus = formatExpirationStatus({
  expirationDate: '2026-01-01',
  willRenew: false,
  isActive: false,
}) // "Expired on Jan 1, 2026"
```

### Customer Info Storage

Store subscription details for offline access:

```typescript theme={null}
import { subscriptionService } from '@/services/revenuecat'

// Get current subscription info
const info = await subscriptionService.getSubscriptionInfo()

// Returns structured data:
{
  platform: 'revenuecat',
  status: 'active' | 'trial' | 'expired' | 'cancelled' | 'none',
  productId: 'pro_monthly' | null,
  expirationDate: '2026-02-12T00:00:00Z' | null,
  willRenew: true | false,
  isActive: true | false,
  isTrial: true | false,
}
```

For more advanced patterns including paywalls, A/B testing, and analytics integration, see `boilerplate/vibe/SUBSCRIPTION_ADVANCED.md`.

## Testing

**Mock mode:** Without API keys, purchases simulate success. Includes realistic promotional offers (7-day trial for monthly, 14-day trial + intro pricing for annual). Dev menu includes a Free/Pro toggle.

**Sandbox testing:**

* **iOS:** Configure a [sandbox tester](https://docs.revenuecat.com/docs/apple-app-store#testing-in-sandbox) in App Store Connect
* **Android:** Configure a [test account](https://docs.revenuecat.com/docs/google-play-store#testing-in-google-play) in Google Play Console
* **Web:** Use Stripe test mode with RevenueCat Web Billing

## Webhooks

For real-time subscription status updates, configure webhooks in **RevenueCat** → **Project Settings** → **Integrations** → **Webhooks**.

See [RevenueCat Webhooks](https://www.revenuecat.com/docs/webhooks) for setup.

## Troubleshooting

**"No products registered" errors?**

* Expected when API keys are set but products aren't configured in RevenueCat
* Create products and add them to an Offering, then restart the app

**Packages not loading?**

* Verify API keys are correct
* Check that products are linked to an Offering in RevenueCat
* Call `restorePurchases()` if user has an active subscription

**Still in mock mode?**

* Verify `apps/app/.env` has the correct keys
* Restart Metro: `yarn start --clear`

For more help, see [RevenueCat Troubleshooting](https://www.revenuecat.com/docs/troubleshooting).
