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:

  1. Multiple IAP products in App Store Connect / Google Play Console
    Example: support_399, support_599, support_999
  2. A slider UI
    Each slider position corresponds to a specific product ID
  3. 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              >
268269              </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.

Sources