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.
Shipnative uses RevenueCat for subscriptions on iOS, Android, and Web. The payment UI and subscription store are pre-built.
Setup
The wizard prompts for your RevenueCat API keys (iOS, Android, Web).
Or manually add to apps/app/.env:
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
- Create a project at app.revenuecat.com
- Link your App Store Connect and Google Play Console apps
- Get API keys from Project Settings → API Keys
- Create products in App Store Connect / Google Play Console
- Add products to an Offering (typically “default”) in RevenueCat
- Create an Entitlement (e.g., “pro”) and link your products to it
For detailed setup, see RevenueCat Getting Started.
Usage
The app auto-detects the platform and uses the appropriate RevenueCat SDK.
Subscription Store
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
import { useSubscriptionStore } from '@/stores/subscriptionStore'
function PremiumFeature() {
const { isPro } = useSubscriptionStore()
if (!isPro) {
return <PaywallScreen />
}
return <PremiumContent />
}
Custom Purchase UI
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:
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:
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
RevenueCat automatically detects and displays promotional offers configured in App Store Connect or Google Play Console:
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:
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:
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:
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 in App Store Connect
- Android: Configure a test account 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 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.