Paywalls don’t always have to follow the same model, in which customers choose between 2-3 subscription options (e.g., annual or yearly). Sometimes you might want something different. One option could be, for example, “name your price paywall”, where users can choose between differently priced packages, that all unlock the same entitlement:
This is a custom paywall that I built for my app Trippity, which I will be launching (hopefully soon). The reason I built it this way was to offer a single lifetime unlock for features while allowing customers to choose how much they want to support the app. The levels also appear in the app, so later on you can purchase a higher level if you feel like it.
This quick tutorial will look at how to organize your subscriptions to support a “name your price paywall” with five different subscription levels. Code examples are in React Native, but you could easily implement this in any development stack. The main setup takes place in the RevenueCat dashboard or, if you are using our MCP, in your coding agent.
Use the RevenueCat AI Toolkit to shorten development time
If you’re using an AI coding assistant, you can skip most of the manual setup. The new RevenueCat AI Toolkit lets agents like Claude Code, Codex, Gemini CLI, and VS Code create products, entitlements, offerings, and SDK integrations directly from prompts.
Install it with:
1claude plugins marketplace add RevenueCat/ai-toolkit
2
3claude plugins install RevenueCat
Or, for Codex:
1codex plugin marketplace add RevenueCat/ai-toolkit
After authenticating with RevenueCat, you can simply ask your agent to create the products, entitlements, and offering needed for this tutorial. We’ll show both the AI-assisted and manual approaches below.
How “name your price” slider paywall works
The paywall slider lets users choose how much they feel the premium features are worth while still guiding them toward reasonable price anchors (e.g., $3.99 → $9.99). Because all price points unlock the same entitlement, the technical setup stays simple.
There are three parts to the system:
- Multiple IAP products in App Store Connect / Google Play Console
Example: support_399, support_599, support_999 - A slider UI
Each slider position corresponds to a specific product ID - A single entitlement in RevenueCat
No matter which price the user selects, they unlock the same features
This model works with both non-consumable in-app purchases as well as subscriptions (“Pay what feels right” kind of a thing). You could even combine these by making the lowest price a subscription. Just remember to keep it obvious what the total price and monthly price are so your app doesn’t get rejected by Apple or Google for deliberately confusing customers.
Step 1: Create your price-tiered products and entitlement
Our first step is to create the products we will be showing in our custom paywall. In this example, there are five products, with prices ranging from $1.99 to $19.99. I’ll show two ways of doing this: first, through RevenueCat MCP, and then through App Store Connect and importing those products to RevenueCat. The same approach works with the Google Play Store Console.
Create products using RevenueCat MCP
Creating 5 different products using RevenueCat MCP is simple. First, see our setup guide for RevenueCat MCP here, and once that is done, run the following prompt in your MCP-connected agent of choice:
Agent instructions
Using RevenueCat MCP, create 5 non-consumable lifetime in-app purchases in both RevenueCat and App Store Connect. Create a single entitlement called Pro and connect all products to it.
Products:
* Pro Bronze Lifetime — $1.99
* Pro Silver Lifetime — $4.99
* Pro Gold Lifetime — $9.99
* Pro Platinum Lifetime — $14.99
* Pro Diamond Lifetime — $19.99
Requirements:
* All products unlock the same Pro entitlement permanently.
* Users can purchase any tier and receive lifetime Pro access.
* Users may later purchase a higher tier even if they already own a lower tier.
* Product IDs should follow the pattern: pro_bronze_lifetime, pro_silver_lifetime, etc.
* Ensure RevenueCat offerings and App Store Connect products are configured correctly and linked to the Pro entitlement.
Once you run this prompt, you will be potentially asked for access to the MCP. As long as you’ve correctly set up the App Store Connect connection in RevenueCat, products should get created in both places. Alternatively, you can create these products in the RevenueCat Test store if you have not connected App Store Connect to RevenueCat.
Create products using RevenueCat dashboard
If you’d rather set everything up manually, start by creating your products in App Store Connect (or Google Play Console). Create one product for each price point and give them clear identifiers, such as:
- pro_bronze_lifetime — $1.99
- pro_silver_lifetime — $4.99
- pro_gold_lifetime — $9.99
- pro_platinum_lifetime — $14.99
- pro_diamond_lifetime — $19.99
Since every purchase unlocks the same features, keep the product names and descriptions consistent. The only thing changing is the price. Once the products have been created in your store, import them into RevenueCat from Product Catalog → Products. RevenueCat can automatically import products from App Store Connect and Google Play after you’ve connected your stores.
Next, create a single entitlement that represents the premium access users receive. Navigate to Product Catalog → Entitlements, click New Entitlement, and create an entitlement called pro (or any identifier you prefer).
After creating the entitlement, open it and click Attach in the Products section. Select all five lifetime products and save. This tells RevenueCat that purchasing any price tier should unlock the same premium access.
Finally, create an Offering (for example, default) and add all five products to it. Your React Native app can then fetch the offering and display the products in the slider UI we’ll build next.
Step 2: Connect RevenueCat to React Native
Next, we need to add RevenueCat to our app. Once again, I’m going to give you both an agentic way to do this and instructions on how to do it by hand.
Setup RevenueCat using an agent
If you installed the RevenueCat AI Toolkit earlier, open your React Native project in your coding agent and ask it to wire up the SDK:
Agent instructions
Using the RevenueCat AI Toolkit, add RevenueCat to this React Native app.
Requirements:
- Install react-native-purchases
- Configure the SDK on app startup
- Use the correct public SDK key for iOS and Android
- Add any required native setup for iOS and Android
- Create a small purchases helper file for fetching offerings, checking the Pro entitlement, restoring purchases, and making purchases
The agent should install the SDK, add the initialization code, and make sure the native project is configured correctly. You may be asked to authenticate with RevenueCat through OAuth before the agent can access your project.
Set up RevenueCat manually
Install the React Native SDK:
1npm install --save react-native-purchases
2
Then configure RevenueCat when your app starts:
1import { useEffect } from 'react';
2import { Platform } from 'react-native';
3import Purchases, { LOG_LEVEL } from 'react-native-purchases';
4
5export default function App() {
6 useEffect(() => {
7 Purchases.setLogLevel(LOG_LEVEL.DEBUG); // remove for release
8
9 const apiKey = Platform.OS === 'ios'
10 ? 'appl_YOUR_IOS_PUBLIC_SDK_KEY'
11 : 'goog_YOUR_ANDROID_PUBLIC_SDK_KEY';
12
13 Purchases.configure({ apiKey });
14 }, []);
15
16 return /* … your UI … */;
17}
18
You can find your public SDK keys in the RevenueCat dashboard under Project Settings → API Keys.
To check whether the user has unlocked your pro entitlement, fetch their CustomerInfo and use it in a custom hook:
1import Purchases from 'react-native-purchases';
2
3async function hasProAccess() {
4 const customerInfo = await Purchases.getCustomerInfo();
5 return customerInfo.entitlements.active.pro !== undefined;
6}
7
8
You can use this anywhere in your app to determine whether premium features should be unlocked. Since all five of our “name your price” products are attached to the same pro entitlement, it doesn’t matter which price tier the user purchased—RevenueCat will grant access in exactly the same way.
With RevenueCat connected and the entitlement configured, we’re ready to fetch our products and build the slider UI.
Step 3: Fetch the available price tiers
Now that RevenueCat is connected, we can fetch the products that will power the slider. Once again, you can either ask your coding agent to build this for you or implement it manually.
Build the slider using an agent
If you’re using the RevenueCat AI Toolkit, you can ask your agent:
Agent instructions
Build a React Native "name your price" paywall using RevenueCat.
Requirements:
- Fetch the current offering from RevenueCat
- Display all available packages as price tiers
- Sort the tiers from lowest to highest price
- Show a slider where each step maps to one package
- Show the selected price above the slider
- Purchase the selected package when the user taps Unlock
- After purchase, check whether the pro entitlement is active
- Include restore purchases
That you should get to the stage where you have a slider. Prompt it to your liking to get the UI you want.
Code for name your price paywall
Here’s a pretty lengthy code sample that aims to replicate the slider flow you saw in the beginning:
1import { useEffect, useMemo, useState } from 'react';
2import {
3 ActivityIndicator,
4 Alert,
5 Pressable,
6 ScrollView,
7 StyleSheet,
8 Text,
9 View,
10} from 'react-native';
11import { LinearGradient } from 'expo-linear-gradient';
12import Slider from '@react-native-community/slider';
13import Purchases, { PurchasesPackage } from 'react-native-purchases';
14
15const ENTITLEMENT_ID = 'pro';
16
17// Each slider position is a gem tier. All tiers map to the SAME entitlement —
18// only the color, name, and product ID change.
19type GemTier = {
20 id: string;
21 name: string;
22 productIdentifier: string;
23 color: string;
24 gradient: [string, string]; // [light, dark] for the card + button
25 tagline: string;
26 level: number; // 1...5, drives the dot row
27};
28
29const SHARED_FEATURES = [
30 'See data from all years',
31 'Remove watermark from sharing',
32 'Join the leaderboard',
33];
34
35const TIERS: GemTier[] = [
36 { id: 'topaz', name: 'Topaz', productIdentifier: 'lifetime_topaz', color: '#FFB233', gradient: ['#FFCC4D', '#FF991A'], tagline: 'Essential Explorer', level: 1 },
37 { id: 'sapphire', name: 'Sapphire', productIdentifier: 'lifetime_sapphire', color: '#3366FF', gradient: ['#4D80FF', '#1A4DCC'], tagline: 'Sky Master', level: 2 },
38 { id: 'emerald', name: 'Emerald', productIdentifier: 'lifetime_emerald', color: '#33CC66', gradient: ['#4DE680', '#1AB34D'], tagline: 'Eco Traveler', level: 3 },
39 { id: 'ruby', name: 'Ruby', productIdentifier: 'lifetime_ruby', color: '#E63350', gradient: ['#FF4D66', '#CC1A33'], tagline: 'Premium Voyager', level: 4 },
40 { id: 'diamond', name: 'Diamond', productIdentifier: 'lifetime_diamond', color: '#B3CCFF', gradient: ['#FFFFFF', '#99B3E6'], tagline: 'Ultimate Edition', level: 5 },
41];
42
43export function NameYourPricePaywall() {
44 const [selectedIndex, setSelectedIndex] = useState(2); // default to middle tier
45 const [prices, setPrices] = useState<Record<string, string>>({});
46 const [packages, setPackages] = useState<PurchasesPackage[]>([]);
47 const [isPurchasing, setIsPurchasing] = useState(false);
48
49 const selectedTier = TIERS[selectedIndex];
50 const isLoaded = packages.length > 0;
51
52 useEffect(() => {
53 (async () => {
54 try {
55 const offerings = await Purchases.getOfferings();
56 const available = offerings.current?.availablePackages ?? [];
57 setPackages(available);
58
59 const priceMap: Record<string, string> = {};
60 for (const pkg of available) {
61 priceMap[pkg.product.identifier] = pkg.product.priceString;
62 }
63 setPrices(priceMap);
64 } catch (e) {
65 console.error('Failed to load offerings', e);
66 }
67 })();
68 }, []);
69
70 async function handlePurchase() {
71 const pkg = packages.find(
72 (p) => p.product.identifier === selectedTier.productIdentifier
73 );
74 if (!pkg) {
75 Alert.alert('Unavailable', 'That tier is not available right now.');
76 return;
77 }
78
79 setIsPurchasing(true);
80 try {
81 const { customerInfo } = await Purchases.purchasePackage(pkg);
82 if (customerInfo.entitlements.active[ENTITLEMENT_ID]) {
83 Alert.alert('Unlocked', `You're now a ${selectedTier.name} member.`);
84 }
85 } catch (e: any) {
86 if (!e.userCancelled) {
87 Alert.alert('Purchase failed', 'Please try again.');
88 }
89 } finally {
90 setIsPurchasing(false);
91 }
92 }
93
94 async function restorePurchases() {
95 try {
96 const customerInfo = await Purchases.restorePurchases();
97 Alert.alert(
98 customerInfo.entitlements.active[ENTITLEMENT_ID] ? 'Restored' : 'No purchases found',
99 customerInfo.entitlements.active[ENTITLEMENT_ID]
100 ? 'Your access has been restored.'
101 : 'We could not find an active purchase.'
102 );
103 } catch (e) {
104 console.error('Restore failed', e);
105 }
106 }
107
108 return (
109 // Background gradient tints toward the selected tier's color
110 <LinearGradient
111 colors={['#FFFFFF', `${selectedTier.color}26`, `${selectedTier.color}4D`]}
112 style={styles.container}
113 >
114 <ScrollView contentContainerStyle={styles.scroll}>
115 {/* Hero: frequent-flyer style membership card */}
116 <MembershipCard tier={selectedTier} />
117
118 {/* Gem indicators on a connecting line */}
119 <GemRail selectedIndex={selectedIndex} onSelect={setSelectedIndex} />
120
121 {/* Slider — each step is one tier */}
122 <Slider
123 minimumValue={0}
124 maximumValue={TIERS.length - 1}
125 step={1}
126 value={selectedIndex}
127 onValueChange={(v) => setSelectedIndex(Math.round(v))}
128 minimumTrackTintColor={selectedTier.color}
129 thumbTintColor={selectedTier.color}
130 style={styles.slider}
131 />
132
133 {/* Price */}
134 <View style={styles.priceBlock}>
135 {prices[selectedTier.productIdentifier] ? (
136 <>
137 <View style={styles.priceRow}>
138 <Text style={[styles.price, { color: selectedTier.color }]}>
139 {prices[selectedTier.productIdentifier]}
140 </Text>
141 <Text style={[styles.priceTier, { color: selectedTier.color }]}>
142 {selectedTier.name}
143 </Text>
144 </View>
145 <Text style={styles.priceCaption}>
146 One-time payment • Lifetime access
147 </Text>
148 </>
149 ) : (
150 <ActivityIndicator />
151 )}
152 </View>
153
154 {/* Unlock button — gradient matches the tier */}
155 <Pressable
156 onPress={handlePurchase}
157 disabled={isPurchasing || !isLoaded}
158 style={({ pressed }) => [{ opacity: pressed ? 0.9 : 1 }]}
159 >
160 <LinearGradient
161 colors={selectedTier.gradient}
162 start={{ x: 0, y: 0 }}
163 end={{ x: 1, y: 1 }}
164 style={styles.unlockButton}
165 >
166 {isPurchasing ? (
167 <ActivityIndicator color="#fff" />
168 ) : (
169 <Text style={styles.unlockText}>◆ Unlock {selectedTier.name}</Text>
170 )}
171 </LinearGradient>
172 </Pressable>
173
174 <Pressable onPress={restorePurchases} style={styles.restore}>
175 <Text style={styles.restoreText}>Restore Purchases</Text>
176 </Pressable>
177
178 <Text style={styles.terms}>
179 By purchasing, you agree to our Terms of Service and Privacy Policy
180 </Text>
181 </ScrollView>
182 </LinearGradient>
183 );
184}
185
186// MARK: – Membership card
187
188function MembershipCard({ tier }: { tier: GemTier }) {
189 return (
190 <LinearGradient
191 colors={[`${tier.color}E6`, `${tier.color}B3`, `${tier.color}80`]}
192 start={{ x: 0, y: 0 }}
193 end={{ x: 1, y: 1 }}
194 style={[styles.card, { shadowColor: tier.color }]}
195 >
196 {/* Metallic sheen overlay */}
197 <LinearGradient
198 colors={['rgba(255,255,255,0.12)', 'transparent', 'rgba(255,255,255,0.05)']}
199 start={{ x: 0, y: 0 }}
200 end={{ x: 1, y: 1 }}
201 style={StyleSheet.absoluteFill}
202 />
203
204 {/* Header: brand + tier badge */}
205 <View style={styles.cardHeader}>
206 <Text style={styles.brand}>TRIPPITY</Text>
207 <View style={styles.badge}>
208 <Text style={styles.badgeText}>◆ {tier.name.toUpperCase()}</Text>
209 </View>
210 </View>
211
212 <Text style={styles.userName}>Traveler</Text>
213
214 {/* Shared features */}
215 <View style={styles.features}>
216 {SHARED_FEATURES.map((f) => (
217 <Text key={f} style={styles.feature}>✓ {f}</Text>
218 ))}
219 </View>
220
221 {/* Footer: level dots + lifetime label */}
222 <View style={styles.cardFooter}>
223 <View style={styles.dots}>
224 {[0, 1, 2, 3, 4].map((i) => (
225 <View
226 key={i}
227 style={[
228 styles.dot,
229 { backgroundColor: i < tier.level ? '#fff' : 'rgba(255,255,255,0.3)' },
230 ]}
231 />
232 ))}
233 </View>
234 <Text style={styles.lifetime}>LIFETIME MEMBER</Text>
235 </View>
236 </LinearGradient>
237 );
238}
239
240// MARK: – Gem rail (indicators + connecting line)
241
242function GemRail({
243 selectedIndex,
244 onSelect,
245}: {
246 selectedIndex: number;
247 onSelect: (i: number) => void;
248}) {
249 return (
250 <View style={styles.rail}>
251 {TIERS.map((tier, i) => {
252 const isActive = i <= selectedIndex;
253 const isSelected = i === selectedIndex;
254 return (
255 <View key={tier.id} style={styles.railSegment}>
256 <Pressable onPress={() => onSelect(i)} hitSlop={12}>
257 <Text
258 style={[
259 styles.gem,
260 {
261 fontSize: isSelected ? 26 : 16,
262 color: isActive ? tier.color : 'rgba(150,150,150,0.5)',
263 textShadowColor: isActive ? tier.color : 'transparent',
264 textShadowRadius: isSelected ? 10 : 0,
265 },
266 ]}
267 >
268 ◆
269 </Text>
270 </Pressable>
271 {i < TIERS.length - 1 && (
272 <View
273 style={[
274 styles.connector,
275 {
276 backgroundColor:
277 i < selectedIndex ? TIERS[i + 1].color : 'rgba(150,150,150,0.3)',
278 },
279 ]}
280 />
281 )}
282 </View>
283 );
284 })}
285 </View>
286 );
287}
288
289const styles = StyleSheet.create({
290 container: { flex: 1 },
291 scroll: { padding: 24, gap: 20 },
292
293 card: {
294 height: 260,
295 borderRadius: 16,
296 padding: 20,
297 justifyContent: 'space-between',
298 overflow: 'hidden',
299 shadowOpacity: 0.4,
300 shadowRadius: 20,
301 shadowOffset: { width: 0, height: 10 },
302 elevation: 12,
303 },
304 cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
305 brand: { color: 'rgba(255,255,255,0.9)', fontSize: 14, fontWeight: '700', letterSpacing: 2 },
306 badge: {
307 flexDirection: 'row',
308 backgroundColor: 'rgba(255,255,255,0.2)',
309 borderColor: 'rgba(255,255,255,0.3)',
310 borderWidth: 1,
311 borderRadius: 100,
312 paddingHorizontal: 10,
313 paddingVertical: 4,
314 },
315 badgeText: { color: '#fff', fontSize: 11, fontWeight: '700', letterSpacing: 1 },
316 userName: { color: '#fff', fontSize: 24, fontWeight: '700', marginTop: 10 },
317 features: { gap: 6, marginTop: 8 },
318 feature: { color: 'rgba(255,255,255,0.9)', fontSize: 12, fontWeight: '500' },
319 cardFooter: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
320 dots: { flexDirection: 'row', gap: 3 },
321 dot: { width: 6, height: 6, borderRadius: 3 },
322 lifetime: { color: 'rgba(255,255,255,0.6)', fontSize: 8, fontWeight: '500', letterSpacing: 1 },
323
324 rail: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8 },
325 railSegment: { flexDirection: 'row', alignItems: 'center', flex: 1 },
326 gem: { textAlign: 'center', width: 30 },
327 connector: { flex: 1, height: 3, borderRadius: 2 },
328
329 slider: { marginHorizontal: 8 },
330
331 priceBlock: { alignItems: 'center', gap: 4 },
332 priceRow: { flexDirection: 'row', alignItems: 'flex-end', gap: 6 },
333 price: { fontSize: 36, fontWeight: '800' },
334 priceTier: { fontSize: 18, fontWeight: '500', marginBottom: 4 },
335 priceCaption: { fontSize: 12, color: '#888' },
336
337 unlockButton: { borderRadius: 14, paddingVertical: 16, alignItems: 'center' },
338 unlockText: { color: '#fff', fontSize: 16, fontWeight: '600' },
339
340 restore: { alignItems: 'center', paddingTop: 4 },
341 restoreText: { color: '#888', fontSize: 14 },
342 terms: { color: '#aaa', fontSize: 11, textAlign: 'center', paddingHorizontal: 24, paddingBottom: 20 },
343});
This fetches your current RevenueCat Offering, sorts the available packages by price, and maps each package to a slider step. When the user taps Unlock Pro, the app purchases the currently selected package and then checks whether the pro entitlement is active.
Because every price tier unlocks the same entitlement, the app does not need separate access logic for Bronze, Silver, Gold, Platinum, or Diamond. The selected product only controls how much the user pays.
Wrapping up
A “name your price” paywall is surprisingly simple to implement. Under the hood, it’s just multiple products linked to the same entitlement, with a slider that lets users choose the level of support that feels right to them. The approach works especially well for indie apps, creator tools, open-source projects, and products where users genuinely want to support ongoing development. Instead of forcing customers into a single price point, you give them flexibility while keeping your purchase logic and entitlement management simple.
Because everything is powered by RevenueCat Offerings, you can also experiment over time. Add or remove price tiers, change your default recommendation, or test entirely different pricing strategies without rebuilding the underlying purchase flow.
If you build one, I’d love to see it. Tag me on Twitter at @plahteenlahti and let me know what pricing tiers you decided to offer.

