●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
A deep dive into RevenueCat's advanced features for preventing churn and re-engaging lapsed subscribers. Covers offer codes, Win-Back campaigns, Grace Periods, cancellation flows, and LTV analytics — all tailored for Rork Max apps.
The first time I got a subscription-cancellation notification, it reframed how I think about growth. When you run apps on your own, it's easy to fixate on the download graph — but what actually moves revenue is whether the people who leave ever come back.
Acquiring new users is expensive. Retaining and reactivating existing ones is far more cost-effective — often more so than any acquisition channel. Even a 1% improvement in monthly churn can lift LTV by tens of percent, meaningfully shifting your app's long-term trajectory. A defensive move quietly becomes room to go on the offensive.
This guide walks through RevenueCat's most powerful retention features — offer codes, Win-Back Offers, Grace Periods, and cancellation surveys — and shows how to wire them into a Rork Max app, ordered the way you'd actually implement them: quick wins first, measurement-heavy work later.
Understanding RevenueCat's Retention Toolkit
Before diving into implementation, let's map out the tools RevenueCat provides.
Offerings & Packages
The building blocks for subscription plans, including Win-Back Offerings. Each Offering can target different user states (new, expired, returning).
Promotional Offers
Discounted pricing tiers for existing or former subscribers using Apple StoreKit 2 or Google Play Billing. You can offer free trial extensions, percentage discounts, or fixed prices.
Offer Codes
Redeemable codes you distribute through email campaigns, social media, or influencers. They grant free periods or discounts to targeted audiences.
Grace Period
When a renewal payment fails, the Grace Period keeps the user's access active (up to 16 days on iOS, 30 days on Android) while the payment system retries — eliminating a major source of involuntary churn.
Navigate to App Store Connect → Your App → In-App Purchases → Subscriptions → select a subscription → Offer Codes tab.
Configure the offer type — free period, pay-as-you-go discount, or pay up front — along with expiry dates and batch code generation (up to 1,000 codes per offer).
Building the Redemption UI in Rork Max
import Purchases from 'react-native-purchases';export const RedeemOfferCodeScreen = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); const handleRedeemIOS = async () => { setIsLoading(true); setError(null); try { // Presents Apple's native code redemption sheet await Purchases.presentCodeRedemptionSheet(); // Refresh customer info to check if subscription is now active const customerInfo = await Purchases.getCustomerInfo(); const isPremium = customerInfo.entitlements.active['premium'] !== undefined; if (isPremium) { navigation.replace('PremiumHome'); } } catch (e: any) { setError('Failed to apply the code. Please try again.'); } finally { setIsLoading(false); } }; return ( <View style={styles.container}> <Text style={styles.title}>Redeem Offer Code</Text> <Text style={styles.description}> Enter the code from your email or social media to unlock a special discount. </Text> <TouchableOpacity style={[styles.redeemButton, isLoading && styles.disabled]} onPress={Platform.OS === 'ios' ? handleRedeemIOS : handleRedeemAndroid} disabled={isLoading} > {isLoading ? ( <ActivityIndicator color="#fff" /> ) : ( <Text style={styles.buttonText}>Apply Code</Text> )} </TouchableOpacity> {error && <Text style={styles.errorText}>{error}</Text>} </View> );};
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A holdout-based design (with code) so Win-Back lift is measured, not wishfully counted
✦A situation-based priority map for involuntary churn versus voluntary churn
✦A staged rollout of Grace Period, Win-Back, and save offers you can ship incrementally
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Win-Back Offers target users who previously held a subscription but have since cancelled. StoreKit 2 makes these highly effective, presenting a compelling re-engagement deal right inside the app.
Configuring Win-Back Offers in App Store Connect
Under the subscription's "Subscription Offers" section, create a Win-Back offer with:
Eligibility: Only users who previously subscribed
Offer type: Free period (2–4 weeks is most effective) or deep discount (50–70% off first 1–2 months)
Timing: 30–90 days post-expiration maximizes redemption rates
Use RevenueCat Webhooks to trigger a timed follow-up sequence from your backend:
// Backend (Hono on Cloudflare Workers)app.post('/webhooks/revenuecat', async (c) => { const { event } = await c.req.json(); switch (event.type) { case 'EXPIRATION': { const userId = event.app_user_id; // Day 1: Graceful "we'll miss you" email — no hard sell await scheduleEmail(userId, 'expiration_day1'); // Day 7: Feedback request + 30% discount offer await scheduleEmail(userId, 'expiration_day7', { offerCode: 'COMEBACK30', expiresAt: addDays(new Date(), 14), }); // Day 30: Win-Back push notification — "We have a special offer for you" await schedulePushNotification(userId, 'winback_day30'); // Day 90: Final deep-discount offer (50% off, 2 months) await scheduleEmail(userId, 'expiration_day90', { offerCode: 'WINBACK50', isLastOffer: true, }); break; } case 'RENEWAL': { // User resubscribed — cancel all pending Win-Back messages await cancelScheduledMessages(event.app_user_id); break; } } return c.json({ received: true });});
Grace Period: Eliminating Involuntary Churn
Involuntary churn — caused by failed payments — accounts for 8–15% of subscription revenue loss in most apps. Enabling Grace Periods gives users time to resolve payment issues without losing access.
iOS Setup: App Store Connect → Your App → App Information → enable Grace Period (up to 16 days)
Android Setup: Google Play Console → Monetize → Subscriptions → enable Grace Period (up to 30 days)
Cancellation Survey: Intercept Churners Before They Leave
A well-designed cancellation survey can prevent 10–20% of voluntary cancellations by presenting a tailored offer based on the user's stated reason for leaving.
type CancellationReason = | 'too_expensive' | 'not_using_enough' | 'missing_features' | 'technical_issues' | 'temporary_break' | 'other';const RETENTION_OFFERS = { too_expensive: { type: 'discount', discount: 50 }, not_using_enough: { type: 'pause', pauseDays: 30 }, missing_features: { type: 'feedback' }, technical_issues: { type: 'support' }, temporary_break: { type: 'pause', pauseDays: 30 }, other: { type: 'discount', discount: 30 },};export const CancellationFlowScreen = () => { const [step, setStep] = useState<'survey' | 'offer' | 'confirmed'>('survey'); const [reason, setReason] = useState<CancellationReason | null>(null); const reasons = [ { key: 'too_expensive' as const, label: "It's too expensive" }, { key: 'not_using_enough' as const, label: "I'm not using it enough" }, { key: 'missing_features' as const, label: "Missing features I need" }, { key: 'technical_issues' as const, label: "I'm having technical issues" }, { key: 'temporary_break' as const, label: "I need a break" }, { key: 'other' as const, label: "Other" }, ]; if (step === 'survey') { return ( <View style={styles.container}> <Text style={styles.title}> Before you go — what's the reason? </Text> {reasons.map(({ key, label }) => ( <TouchableOpacity key={key} style={styles.reasonRow} onPress={() => { setReason(key); setStep('offer'); }} > <Text style={styles.reasonText}>{label}</Text> <Ionicons name="chevron-forward" size={16} color="#999" /> </TouchableOpacity> ))} </View> ); } // Render tailored offer card based on reason... return ( <RetentionOfferCard offer={RETENTION_OFFERS[reason!]} onAccept={() => applyRetentionOffer(reason!)} onDecline={() => openSubscriptionManagement()} /> );};
Where to Start — Prioritizing by Your App's Situation
We've covered a lot of machinery. You don't need to ship all of it at once. In fact, the "first move that pays off" depends entirely on your app's current reality.
Churn wears two faces: involuntary churn, caused by things like failed payments, and voluntary churn, where a user actively decides to leave. Which one dominates completely changes what you should build first.
Running apps on my own taught me a hard lesson: bolting on retention tactics without first understanding why people leave mostly spins your wheels. Figure out which kind of churn is hurting you, and the rest falls into place. Get the order right and you lock down your defense with far less effort.
Your situation
Ship this first
Why
Noticeable churn from failed payments (lots of renewal-failure logs)
Grace Period
Config-only, instant effect. Recovers tens of percent of unintended drop-off
You don't know why users cancel
Cancellation Survey
Collect data first. Without reasons, every offer is a guess
Users bail right at the cancel screen
Save offer (Promotional Offer)
A temporary discount lands with the price-sensitive segment — but base it on survey data
Many lapsed former subscribers
Win-Back Offers
Within 30 days there's real room to return. Reawaken the dormant
You can't see the revenue picture
LTV dashboard
Without a cohort view you can't judge whether any tactic is working
Each addition makes the next move clearer. Once Grace Period thins out the failure logs, the churn that remains is genuinely voluntary. Once surveys pile up reasons, your offers get specific. Confirming your footing one step at a time is, in the end, faster than rushing everything in at once.
Measuring Win-Back With a Holdout
Run a Win-Back campaign and some people always come back. The catch is that most implementations can't tell whether a returner came back because of the offer or would have returned anyway. Credit every returner to the offer and you overstate its impact.
The fix is a holdout (a control group). Split the target audience in two: send the offer to one group, send nothing to the other. The difference in reactivation rate between them is the offer's true incremental lift.
// Deterministically split target users into two groups.// Bucket by a hash of user.id so groups stay stable across re-runs.const assignWinbackGroup = (userId: string): 'treatment' | 'holdout' => { // Simple but stable hash (a small FNV-1a variant) let hash = 2166136261; for (let i = 0; i < userId.length; i++) { hash ^= userId.charCodeAt(i); hash = Math.imul(hash, 16777619); } // Route 15% to holdout (offer goes to the remaining 85%) return (hash >>> 0) % 100 < 15 ? 'holdout' : 'treatment';};// When running the campaignconst runWinbackCampaign = async (lapsedUsers: LapsedUser[]) => { for (const user of lapsedUsers) { const group = assignWinbackGroup(user.id); // Record the assignment so you can reconcile rates later await recordCampaignAssignment({ userId: user.id, campaign: 'winback_2026q3', group, assignedAt: new Date().toISOString(), }); // Send the offer only to the treatment group if (group === 'treatment') { await sendWinbackOffer(user); } }};
Detect reactivation via RevenueCat webhooks — pick up the event that signals a re-purchase after dormancy, compute the reactivation rate for both groups, and report only the increment as the effect.
// Compute incremental liftconst measureIncrementalLift = (results: { treatmentReactivated: number; treatmentTotal: number; holdoutReactivated: number; holdoutTotal: number;}) => { const treatmentRate = results.treatmentReactivated / results.treatmentTotal; const holdoutRate = results.holdoutReactivated / results.holdoutTotal; // The reactivation the offer genuinely drove const incrementalLift = treatmentRate - holdoutRate; return { treatmentRate: (treatmentRate * 100).toFixed(1) + '%', holdoutRate: (holdoutRate * 100).toFixed(1) + '%', incrementalLift: (incrementalLift * 100).toFixed(1) + 'pt', // Whether the discount is worth it comes from this increment x LTV };};
A holdout means giving up, in the short term, the "15% who might have returned if we'd emailed them." It feels wasteful. But that 15% is exactly what tells you whether the discount is worth handing out at all. Discounts to former subscribers cost real money. Only by looking at the increment can you tell whether that money is an investment or a leak. Reading the numbers honestly pays off over the long life of an app.
Building an LTV Analytics Dashboard
Once your retention mechanics are in place, measure their impact through RevenueCat's metrics API.
When all these systems work together, the compounding effect is substantial. Grace Period alone typically reduces involuntary churn by 30–50%. Cancellation surveys with targeted offers prevent 10–20% of voluntary cancellations. Win-Back campaigns recover 10–25% of lapsed users within 30 days. Combined, an app with a 3% monthly churn rate can realistically reach 2% — and that difference, sustained over a year, translates to roughly 12% more users still subscribed at the 12-month mark.
Closing Thoughts
Churn prevention is the unglamorous side of growth — but it's often where the biggest gains hide. You don't need all of it at once.
Start with Grace Period: a few-minute setup that immediately trims involuntary churn. Then add a cancellation survey to gather reasons, and once the data accumulates, design save offers that match those reasons. Finally, use Win-Back to reach the people who have already drifted away — in that order, your defense compounds with modest effort.
And always confirm Win-Back's effect with a holdout. Once you're in the habit of reading incremental lift, you can treat discount spend as an investment rather than a leak.
RevenueCat absorbs the billing complexity so you can focus on the experience itself. Build these into your Rork Max app one piece at a time, and you'll grow a revenue base that holds up. Thank you for reading.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.