Sprig Monetization Implementation Guide

v2.0 · March 2026 · Based on your plan + deep app audit · React Native (Expo) · iOS · Android · Web deferred

A Plan Modifications Based on App Audit

Before writing a single line of code, here's what the audit found and how it changes the implementation plan:

✅ Already Done — Skip These Steps

RevenueCat SDK is already installed — react-native-purchases@^9.6.7 and react-native-purchases-ui@^9.6.7 are in package.json.
RevenueCat client wrapper exists — mobile/src/lib/revenuecatClient.ts is a complete, production-ready wrapper with typed results, error handling, and Supabase ID linking. Do not replace it — build on it.
Vibecode injects API keys — EXPO_PUBLIC_VIBECODE_REVENUECAT_* keys are auto-injected. No manual env var setup needed.

⚠️ Things That Need to Change From the Plan

No profiles table exists — The plan references a profiles table for subscription_tier. This table does not exist. We'll create a new user_profiles table.

UsageService is a stub — It's hardcoded to return 0/10 and always allows generation. Every user can currently generate unlimited recipes. This needs a real backend.

Sprig AI Chat is ungated — The SprigFAB and ChatSheet are open to all users. Per your plan, this is a Pro-only feature. It needs to be locked behind a paywall.

Rate limiting is not subscription-aware — The existing in-memory rate limiter (20 req/min) is not connected to subscription tiers. We need database-persisted monthly counters.

📱 Web Billing — Recommended Deferral

The plan's Section 9 (Web Billing via @revenuecat/purchases-js) refers to a standalone getsprig.app website. The Expo web build does not support native in-app purchases — react-native-purchases returns no-ops on web. Recommendation: Launch iOS + Android first. Add web billing in a second phase once iOS/Android are live and generating revenue. All prompts below are iOS + Android only.

ℹ️ Other Notes

Sprig Pro = AI Chat — The Sprig AI Chat assistant is the marquee Pro feature. Standard gets all AI generation tools. Free gets manual features only.

Supabase user ID as RevenueCat appUserID — The existing revenuecatClient.ts already has setUserId(). We just need to call it on login.

Backend never calls RevenueCat — The backend checks user_profiles.subscription_tier (kept in sync by webhook). Zero latency, no external dependency in the hot path.

Bundle ID — The plan uses com.getsprig.sprig. Verify this matches your app.json before setting up App Store Connect / Google Play.

Revised Feature Gate Map

FeatureFreeStandard (Sprig)Pro (Sprig Pro)
Browse & view saved recipes✓✓✓
Manual recipe creation✓✓✓
Shopping lists (manual)✓✓✓
Meal planning (basic)✓✓✓
Browse community recipes✓✓✓
AI recipe generation—✓ (20/mo)✓ (unlimited)
Recipe import (URL/photo/text)—✓ (30/mo)✓ (unlimited)
AI image generation—✓ (20/mo)✓ (soft 100/mo)
Recipe modification—✓ (20/mo)✓ (unlimited)
Nutrition analysis, chef tips, substitutions—✓ (unlimited)✓ (unlimited)
Sprig AI Chat Assistant——✓ (Pro only)
Pro badge on community profile——✓
Share to community—✓✓

B RevenueCat Account Setup

🔧 Manual Steps — Do These in Dashboards
Start Here — Vibecode PAYMENTS Tab

Go to the PAYMENTS tab in your Vibecode app and click "Setup Project." This creates your RevenueCat project and injects the API keys automatically. Do this before anything else — you need the project to exist before you can configure entitlements.

Step 1 — Create Entitlements in RevenueCat Dashboard

Go to app.revenuecat.com → your Sprig project → Entitlements → + New Entitlement

Step 2 — Add Apps to RevenueCat Project

Step 3 — Create Offerings & Packages

After creating products in App Store Connect and Google Play (see Sections C & D), come back here:

Product → Entitlement Mapping

Product IDEntitlements to Attach
sprig_standard_monthly_v1standard
sprig_standard_annual_v1standard
sprig_pro_monthly_v1standard + pro
sprig_pro_annual_v1standard + pro

Package Setup

Package IdentifieriOS ProductAndroid Product
$rc_monthlysprig_standard_monthly_v1sprig_standard_monthly_v1
$rc_annualsprig_standard_annual_v1sprig_standard_annual_v1
pro_monthlysprig_pro_monthly_v1sprig_pro_monthly_v1
pro_annualsprig_pro_annual_v1sprig_pro_annual_v1

Step 4 — Webhook Setup

C App Store Connect Setup (iOS)

🍎 Manual Steps — App Store Connect

D Google Play Console Setup (Android)

🤖 Manual Steps — Google Play Console

E Implementation Prompts — Backend

💻 Copy & Paste into Vibecode
⚠️ Do Sections C & D first

Complete App Store Connect and Google Play product setup, then configure RevenueCat entitlements and the webhook URL before running these prompts. You need the webhook secret env var for Prompt 2.

Phase 1 — Backend Infrastructure (3 prompts)
1

Database Tables for Subscription & Usage Tracking

Backend · ~5 min
Set up the database tables and backend types needed for Sprig's subscription and usage tracking system. SUPABASE SQL — Create a new file at backend/src/migrations/subscription-tables.sql with the following SQL, then add code to the backend startup (in the existing migration runner) to execute it automatically on server start. SQL to include in the file: -- Table 1: user_profiles (subscription state per user) CREATE TABLE IF NOT EXISTS user_profiles ( id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, subscription_tier text NOT NULL DEFAULT 'free' CHECK (subscription_tier IN ('free', 'standard', 'pro')), subscription_platform text CHECK (subscription_platform IN ('ios', 'android', 'web')), billing_period_start timestamptz, revenuecat_customer_id text, updated_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY; CREATE POLICY IF NOT EXISTS "Users read own profile" ON user_profiles FOR SELECT USING (auth.uid() = id); -- Service role can do anything (for webhooks): CREATE POLICY IF NOT EXISTS "Service role all" ON user_profiles FOR ALL TO service_role USING (true); -- Auto-create profile when a new user signs up: CREATE OR REPLACE FUNCTION public.create_user_profile_on_signup() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN INSERT INTO public.user_profiles (id) VALUES (NEW.id) ON CONFLICT (id) DO NOTHING; RETURN NEW; END; $$; DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.create_user_profile_on_signup(); -- Table 2: user_generation_usage (monthly counters per user/type) CREATE TABLE IF NOT EXISTS user_generation_usage ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, generation_type text NOT NULL CHECK (generation_type IN ('recipe', 'image', 'import', 'modify')), billing_period_start date NOT NULL, count integer NOT NULL DEFAULT 0, updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(user_id, generation_type, billing_period_start) ); ALTER TABLE user_generation_usage ENABLE ROW LEVEL SECURITY; CREATE POLICY IF NOT EXISTS "Users read own usage" ON user_generation_usage FOR SELECT USING (auth.uid() = user_id); -- Table 3: api_usage_log (granular per-call log for cost analysis) CREATE TABLE IF NOT EXISTS api_usage_log ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, endpoint text NOT NULL, generation_type text, estimated_cost_cents integer DEFAULT 0, platform text, created_at timestamptz NOT NULL DEFAULT now() ); BACKEND TYPES — Create backend/src/types/subscription.ts with: - SubscriptionTier type: 'free' | 'standard' | 'pro' - GenerationType type: 'recipe' | 'image' | 'import' | 'modify' - STANDARD_LIMITS and PRO_LIMITS constants: Standard: recipe=20, image=20, import=30, modify=20 Pro: recipe=null (unlimited), image=100 (soft cap), import=null, modify=null - UserProfile interface matching the table - UsageEntry interface matching user_generation_usage - UsageSummary interface: { tier, limits, usage: Record } Also add a GET /api/usage endpoint in a new backend/src/routes/usage.ts file that: - Gets the user's current subscription_tier from user_profiles (create the row if it doesn't exist via upsert) - Gets the current billing period usage from user_generation_usage for all 4 generation types - Returns UsageSummary shaped response with { data: UsageSummary } Register the new usage router in the main backend/src/index.ts file at /api/usage. Run typecheck after. Fix any errors.
Notes: This also auto-creates profiles for new users via a Supabase trigger. The backend reads existing migration patterns in the codebase.
2

RevenueCat Webhook Endpoint

Backend · ~5 min
Create a RevenueCat webhook endpoint that keeps subscription state in sync between RevenueCat and our Supabase user_profiles table. Create backend/src/routes/webhooks.ts with a POST /api/webhooks/revenuecat handler that: 1. AUTHENTICATION: Reads the Authorization header and compares it to env.REVENUECAT_WEBHOOK_SECRET. If they don't match, return 401. This is a shared secret set in RevenueCat's webhook configuration. 2. PARSING: Parses the JSON body. RevenueCat sends an event object with this shape: { event: { type: string, // "INITIAL_PURCHASE" | "RENEWAL" | "CANCELLATION" | "EXPIRATION" | "PRODUCT_CHANGE" | "BILLING_ISSUE" app_user_id: string, // This is the Supabase user ID we set as appUserID entitlement_ids: string[] | null, // ["standard"] or ["standard","pro"] or null store: string, // "APP_STORE" | "PLAY_STORE" | "STRIPE" purchased_at_ms: number, expiration_at_ms: number | null, } } 3. TIER MAPPING: Map event type + entitlements to subscription_tier: - INITIAL_PURCHASE or RENEWAL: - if entitlement_ids includes "pro" → tier = 'pro' - else if entitlement_ids includes "standard" → tier = 'standard' - else → tier = 'free' - CANCELLATION, EXPIRATION, BILLING_ISSUE → tier = 'free' (access revoked) - PRODUCT_CHANGE → same logic as INITIAL_PURCHASE 4. PLATFORM MAPPING: Map store to platform: - "APP_STORE" → "ios" - "PLAY_STORE" → "android" - "STRIPE" → "web" - anything else → null 5. UPSERT: Upsert into user_profiles: - id = event.app_user_id - subscription_tier = computed tier - subscription_platform = computed platform - billing_period_start = new Date(event.purchased_at_ms) if it exists, else null - updated_at = now() 6. LOGGING: Log the event type, user_id, and new tier to console for debugging. 7. RESPONSE: Return 200 { data: { received: true } } on success. Return 200 even on errors that aren't auth failures (RevenueCat will retry on non-2xx). Add REVENUECAT_WEBHOOK_SECRET to backend/src/env.ts as an optional string (don't crash if missing — just log a warning and skip signature validation during development). Register the webhooks router in backend/src/index.ts at /api/webhooks. Important: the webhook route should NOT require auth middleware (RevenueCat calls it without user tokens). Make sure it's registered before the auth middleware or at the same level but without the auth guard. Run typecheck. Fix any errors.
Notes: The REVENUECAT_WEBHOOK_SECRET env var should be added via the Vibecode ENV tab once you have the secret from RevenueCat's webhook setup page.
3

Usage Tracking Middleware + Limit Enforcement

Backend · ~8 min
Create a usage tracking middleware system for the backend that enforces monthly generation limits based on subscription tier. Create backend/src/lib/usageTracking.ts with two functions: FUNCTION 1 — checkAndIncrementUsage(userId, generationType, supabase): 1. Get the user's subscription_tier from user_profiles (upsert with 'free' default if not exists) 2. Get the STANDARD_LIMITS and PRO_LIMITS from the subscription types file 3. Determine the current billing_period_start: - Use the first day of the current calendar month (YYYY-MM-01) as a fallback - This is simpler than tracking real billing dates and works fine for launch 4. Get or create the usage row in user_generation_usage for (user_id, generation_type, billing_period_start) 5. Check if limit is exceeded: - tier = 'free': always return { allowed: false, reason: 'upgrade_required', tier: 'free' } - tier = 'standard': check count against STANDARD_LIMITS[generationType]. If count >= limit: return { allowed: false, reason: 'limit_reached', tier: 'standard', limit, count, resetAt } - tier = 'pro': check soft cap only for 'image' (100). All others: allowed. 6. If allowed: increment count by 1 (use upsert with conflict update: count = count + 1) 7. Log to api_usage_log: user_id, endpoint=generationType, generation_type, estimated_cost_cents (recipe=3, image=4, import=2, modify=2) 8. Return { allowed: true, tier, count, limit } FUNCTION 2 — requiresStandardOrPro(): Hono middleware factory Returns a Hono middleware that: - Gets userId from context - Calls checkAndIncrementUsage with a generationType parameter (passed when creating the middleware) - If not allowed: - reason = 'upgrade_required': return 403 { error: { message: "This feature requires a Sprig subscription.", code: "SUBSCRIPTION_REQUIRED", upgradeRequired: true } } - reason = 'limit_reached': return 429 { error: { message: "You've reached your monthly limit for this feature. Resets on [resetAt date].", code: "USAGE_LIMIT_EXCEEDED", limit, count, resetAt } } - If allowed: call next() APPLY THE MIDDLEWARE to these existing routes: - POST /api/generate-recipe → generationType = 'recipe' - POST /api/generate-image (if it exists, or add it) → generationType = 'image' - POST /api/import-recipe (if it exists) → generationType = 'import' - POST /api/modify-recipe → generationType = 'modify' - POST /api/nutrition → still allowed for Standard users (unlimited) — skip limit check but still log For routes I'm not sure exist, read the backend routes first to find the correct endpoint names. DO NOT apply this middleware to: - /api/auth/* - /api/webhooks/* - /api/usage - /api/chat/* (Sprig chat is gated on the mobile side for now) Run typecheck. Fix all errors.
Notes: Using calendar month as billing period start is a simplification for launch. Can refine to actual billing dates using RevenueCat webhook data later.

F Implementation Prompts — Mobile Core

Phase 2 — Mobile Core (3 prompts)
4

Subscription Store + RevenueCat Integration

Mobile · ~8 min
Create a Zustand subscription store for Sprig that wraps the existing RevenueCat client and exposes subscription state throughout the app. Read mobile/src/lib/revenuecatClient.ts first to understand the existing wrapper (it has getCustomerInfo, hasEntitlement, purchasePackage, restorePurchases, setUserId, getOfferings, etc.). Create mobile/src/store/useSubscriptionStore.ts: STATE: - tier: 'free' | 'standard' | 'pro' (default 'free') - isStandard: boolean (computed from tier — standard or pro) - isPro: boolean (computed from tier — pro only) - isLoading: boolean - customerInfo: CustomerInfo | null (from RevenueCat) - offerings: PurchasesOfferings | null ACTIONS: - initialize(userId: string): Call revenuecatClient.setUserId(userId). Then call refreshSubscription(). - refreshSubscription(): Call revenuecatClient.getCustomerInfo(). If ok, parse entitlements: - if 'pro' entitlement active → tier = 'pro' - else if 'standard' entitlement active → tier = 'standard' - else → tier = 'free' Update isStandard (tier !== 'free'), isPro (tier === 'pro'), customerInfo. Also call revenuecatClient.getOfferings() and store in offerings. - purchasePackage(pkg: PurchasesPackage): Promise<{ ok: boolean; cancelled?: boolean; error?: string }> Call revenuecatClient.purchasePackage(pkg). On success, call refreshSubscription(). Return result. - restorePurchases(): Promise<{ ok: boolean; error?: string }> Call revenuecatClient.restorePurchases(). On success, call refreshSubscription(). Return result. - logout(): Call revenuecatClient.logoutUser(). Reset tier = 'free', isStandard = false, isPro = false. IMPORTANT: Use Zustand selectors — never select the whole store. Import individual values. INTEGRATION — Update mobile/src/store/useAuthStore.ts (read it first): - When a user successfully logs in, call useSubscriptionStore.getState().initialize(userId). - When a user logs out, call useSubscriptionStore.getState().logout(). INTEGRATION — Update mobile/src/services/UsageService.ts: Replace the stub implementation to call GET /api/usage from the backend using the existing API client pattern in the codebase. The response has: { tier, usage: { recipe: { count, limit, resetAt }, image: ..., import: ..., modify: ... } }. Update fetchMonthlyUsage() to return real data, and canGenerate() to check the recipe usage against limits. Run typecheck. Fix all errors.
Notes: The existing revenuecatClient.ts already handles the case where RevenueCat isn't configured (returns ok:false gracefully), so the store should handle those cases without crashing.
5

Paywall Screen

Mobile · ~12 min
Build a beautiful, native-feeling paywall screen for Sprig. This is one of the most important screens in the app — it should feel premium and generous, not pushy. Create mobile/src/screens/PaywallScreen.tsx. This screen is presented modally (it will be pushed as a modal via router.push('/paywall') or shown from a bottom sheet). DESIGN DIRECTION: - Clean, airy iOS aesthetic. White/off-white background. No aggressive popups. - Top: Sprig logo + tagline "Your personal AI chef" - Main card area: annual/monthly toggle with "Save 40%" badge on annual - Two subscription option cards: "Sprig" and "Sprig Pro" (side by side or stacked) - Each card shows: price, billing period, key features (3-4 bullet points), CTA button - Sprig Pro card is visually emphasized (slightly larger, accent color) - Feature comparison: small scrollable list of what each tier gets - Below CTA: "Start 7-day free trial — cancel anytime" (small, reassuring text) - Bottom: "Restore Purchases" link + legal disclaimer (very small, gray) TIER CARDS: Sprig ($6.99/mo or $49.99/yr): • AI recipe generation (20/month) • Recipe import from URLs & photos • AI image generation • Nutrition analysis, tips & substitutions Sprig Pro ($11.99/mo or $79.99/yr): • Everything in Sprig • AI Chef Assistant (unlimited chat) • Unlimited recipe & image generation • Priority AI + Pro community badge FUNCTIONALITY: - Use useSubscriptionStore to get offerings (packages) - Find packages by identifier: $rc_monthly, $rc_annual, pro_monthly, pro_annual - Show real prices from RevenueCat packages (pkg.product.priceString) - If offerings aren't loaded yet, show a skeleton/loading state - On pressing a plan's CTA: call purchasePackage(pkg) from the store - Show loading state on the button - On success: show brief "Welcome to Sprig Pro! 🎉" toast and dismiss the paywall - On cancellation (user cancelled): do nothing - On error: show error toast - "Restore Purchases" button: call restorePurchases(), show appropriate feedback - Add a close button (X) in the top right to dismiss (for users who want to stay Free) NAVIGATION: Register PaywallScreen in the app's router (_layout.tsx or RootNavigator). Make it a modal presentation. EXPORT a usePaywall() helper hook in mobile/src/hooks/usePaywall.ts: - Has showPaywall() function that calls router.push('/paywall') or similar - Other components can call showPaywall() to trigger the paywall Run typecheck. Fix all errors.
Notes: Don't mock subscription state. If RevenueCat isn't configured yet, the paywall should show a loading/unavailable state gracefully.
6

Onboarding Trial Prompt

Mobile · ~6 min
After a user completes onboarding, show a soft paywall offering the 7-day Pro trial. The experience should feel exciting, not intrusive — users should feel they're being offered something valuable. Read mobile/src/screens/OnboardingScreen.tsx first to understand how onboarding completes and what happens after (where it navigates to). CHANGES TO ONBOARDING FLOW: After the user completes onboarding (the final step), instead of navigating directly to the main app: 1. Navigate to the paywall screen with a special "trialMode" param: router.push('/paywall?mode=trial') 2. On the paywall, when mode=trial: - Change the header to: "Start your free trial" - Change subtext to: "7 days of Sprig Pro, completely free. No charge until your trial ends." - Make the "Sprig Pro" plan the default/highlighted selection - Change the close/skip button text to: "Start with Free" (so it's clear it's a skip, not a dismiss) - The rest of the paywall works the same way Update PaywallScreen.tsx to read the mode param from the route and adjust the UI accordingly. This should feel like: "Here's something amazing we're offering you" — not "please give us money." If the user taps "Start with Free" — navigate to the main app. If the user starts a trial and it succeeds — navigate to the main app with a welcome message. Run typecheck. Fix all errors.
Notes: The plan calls for a Day 5 push notification reminder. That can be added in a later prompt once push notifications are set up.

G Implementation Prompts — Feature Gating

Phase 3 — Feature Gating (3 prompts)
7

Gate AI Generation Features (Standard/Pro)

Mobile · ~10 min
Gate AI generation features throughout the app so they require at least a Standard subscription. Read each file before modifying it. FILES TO READ FIRST: GenerateScreen.tsx, any ImportSheet or import-related component, ManualRecipeScreen.tsx, and any screen that calls nutrition/chef tips/substitution endpoints. RULE: isStandard = tier is 'standard' or 'pro'. When a Free user tries to access a gated action, call showPaywall() from usePaywall() hook instead of performing the action. CHANGES: 1. GenerateScreen.tsx — read the file first: - At the top of the generate action handler, check isStandard from useSubscriptionStore - If !isStandard: call showPaywall() and return early - Remove the usage.canGenerate() check (replaced by backend enforcement + this gate) - Show a visual indicator for Standard users when they're near their limit (read from UsageService) 2. Recipe Import (find the import sheet/screen — read it first): - Gate URL import, photo import, and text import behind isStandard - If !isStandard: tap → showPaywall() - If isStandard: proceed normally 3. RecipeDetailScreen.tsx — read it first: - Gate "Modify Recipe" button behind isStandard - Gate "Generate Image" (if it exists as a button) behind isStandard - Gate "Nutrition" tab/button behind isStandard - For gated features: show a lock icon + "Upgrade" label instead of hiding them - Tapping a locked feature opens the paywall 4. Any screen with chef tips or substitution suggestions — gate behind isStandard IMPORTANT: - Never hide features entirely — show them with a lock icon so users know what they're missing - The backend ALSO enforces limits (Prompt 3), so the mobile gate is just UX, not the only protection - Use the usePaywall() hook created in Prompt 5 - Use useSubscriptionStore(s => s.isStandard) selector pattern (primitive selector, not whole store) Run typecheck. Fix all errors.
Notes: Backend enforcement is the real security layer. Mobile gating is for UX. Both are needed.
8

Gate Sprig AI Chat (Pro Only)

Mobile · ~5 min
Gate the Sprig AI Chat Assistant behind the Pro subscription tier. The chat is currently accessible to all users — it needs to be locked to Pro only, as it's the marquee Pro feature. Read mobile/src/components/SprigFAB.tsx and mobile/src/components/chat/ChatSheet.tsx first. CHANGES: 1. SprigFAB.tsx — read the file first: - Get isPro from useSubscriptionStore(s => s.isPro) - When the FAB is pressed: - If isPro: proceed as normal (open chat sheet) - If !isPro: instead of opening chat, call showPaywall() from usePaywall() AND show a small tooltip/badge on the FAB: "Pro feature" with a subtle lock icon - Visually: when user is NOT Pro, show a small gold/sparkle badge on the FAB to indicate it's a premium feature (not a red X — make it feel aspirational, not blocked) 2. ChatSheet.tsx — add a secondary guard: - If the sheet somehow opens and user is not Pro, show an upgrade prompt inside the sheet instead of the chat UI. This is a safety net. - The upgrade prompt inside the sheet: "Sprig AI Chat is a Pro feature. Upgrade to chat with your personal AI chef." with an "Upgrade to Pro" button that calls showPaywall(). 3. PlanScreen.tsx — the "Plan with Sprig" button (read the file first): - Gate it the same way: if !isPro → showPaywall() instead of opening chat IMPORTANT: Use useSubscriptionStore(s => s.isPro) — not the whole store. Run typecheck. Fix all errors.
Notes: Make the lock feel aspirational, not punitive. Users should want to upgrade, not feel blocked.
9

Profile Screen — Subscription Management

Mobile · ~8 min
Update the Profile screen to show the user's real subscription status and enable subscription management. Read mobile/src/screens/ProfileScreen.tsx first — it currently shows a hardcoded "Free Plan" badge and a disabled "Manage Subscription" button. CHANGES TO ProfileScreen.tsx: 1. SUBSCRIPTION BADGE — replace the hardcoded "Free Plan" badge: - Get tier from useSubscriptionStore(s => s.tier) - Free: show gray "Free Plan" badge (as before) - Standard: show green "Sprig" badge with a leaf icon - Pro: show a premium gold/warm badge "Sprig Pro" with a sparkle or star icon 2. USAGE DISPLAY — read UsageService and understand the current usage bar: - Connect to real data from updated UsageService (which now calls /api/usage) - Standard users: show recipe generation usage (e.g. "8 of 20 recipe generations used") - Pro users: show "Unlimited generations" — no counter - Free users: show "0 recipe generations — upgrade to generate" 3. "MANAGE SUBSCRIPTION" ROW — make it functional: - If user has a subscription (isStandard or isPro): - On press: open the native subscription management. Use Linking.openURL() with: - iOS: "https://apps.apple.com/account/subscriptions" - Android: "https://play.google.com/store/account/subscriptions" - Use Platform.OS to pick the right URL - If user is Free: on press → showPaywall() - Remove the "Coming soon" badge and the disabled state 4. ADD "RESTORE Purchases" ROW — below Manage Subscription: - Label: "Restore Purchases" - On press: call restorePurchases() from useSubscriptionStore - Show loading state while restoring - On success: show toast "Purchases restored!" or "No purchases found" appropriately - On error: show error toast 5. UPGRADE CTA (for Free users only) — add a tasteful banner above settings: - A subtle card: "Unlock AI features — Start your free trial" - Tapping it: showPaywall() - Don't show this if user is already paid Run typecheck. Fix all errors.

H Implementation Prompts — Polish & Extras

Phase 4 — Polish (2 prompts)
10

Pro Badge on Community Profile

Mobile + Backend · ~5 min
Add a Pro badge to community profiles for Sprig Pro subscribers. This is both a visible perk for Pro users and a subtle social signal that encourages upgrades. Read the community profile schema and relevant components first: - supabase_community_schema.sql or equivalent - mobile/src/components/community/ directory — find the component that renders community usernames/profiles - backend/src/routes/ — find the community profile endpoint BACKEND CHANGES: 1. Add a boolean column is_pro to community_profiles: ALTER TABLE community_profiles ADD COLUMN IF NOT EXISTS is_pro boolean NOT NULL DEFAULT false; 2. In the RevenueCat webhook handler (backend/src/routes/webhooks.ts created in Prompt 2): after updating user_profiles, also update community_profiles.is_pro: set to true if tier is 'pro', false otherwise. Use ON CONFLICT DO NOTHING if the community profile doesn't exist yet. MOBILE CHANGES: 1. Find the component that renders a community user's name/avatar (look in mobile/src/components/community/) 2. If a user's community profile has is_pro = true, show a small "PRO" badge next to their username 3. Badge design: small rounded rectangle, warm gold color (#D97706 background, white text), text "PRO", font-size 10px 4. This badge should appear: in recipe cards by the author name, in comments, in profile headers MOBILE — OWN PROFILE: In ProfileScreen, after a user subscribes/upgrades to Pro, automatically update their community_profile.is_pro via a backend call. The simplest approach: after refreshSubscription() succeeds and tier is 'pro', call a PATCH /api/community/profile endpoint (read the existing endpoint first) to set is_pro=true. Run typecheck. Fix all errors.
11

Usage Warning Notifications + Limit Messages

Mobile · ~5 min
Implement the usage warning system: surface remaining counts to Standard users when they're approaching their monthly limits, and show appropriate upgrade prompts when limits are hit. Read GenerateScreen.tsx, the import screen/component, and RecipeDetailScreen.tsx first. CHANGES: 1. APPROACHING LIMIT BANNER (75% threshold): - When a Standard user has used 75% or more of any generation type (e.g. 15/20 recipe generations), show a subtle banner at the top of the relevant screen: "5 recipe generations left this month · Upgrade for unlimited →" - Design: thin banner, warm amber background (#FFFBEB), small text, "Upgrade" in accent color - Tapping the "Upgrade" link: showPaywall() - Dismiss: user can tap X to dismiss for the session (don't persist this dismissal) 2. LIMIT HIT TOAST + UPGRADE PROMPT: - When the backend returns a 429 USAGE_LIMIT_EXCEEDED error: - Show a bottom sheet (not just a toast) with: - Title: "You've hit your monthly limit" - Body: "You've used all [N] [type] generations for this month. Resets on [resetAt date]." - Primary button: "Upgrade to Sprig Pro →" → showPaywall() - Secondary: "OK" to dismiss - Read the existing error handling patterns in the API client to understand how to intercept 429s 3. PRO USER EXPERIENCE: - Pro users never see usage counters or limit warnings (unlimited) - In ProfileScreen, show "Unlimited generations" in place of the usage bar 4. FREE USER EXPERIENCE: - Free users who tap any AI feature → showPaywall() immediately (Prompt 7 already handles this) - Don't show usage numbers for Free users (they have 0 access, not a counter) Run typecheck. Fix all errors.

I Testing Checklist

🧪 Manual Testing Steps

iOS (StoreKit Sandbox)

Android (Google Play Test Track)

Backend


Sprig Monetization Guide · v2.0 · March 2026
iOS · Android · Web (deferred) · Pricing subject to adjustment based on real-world data