<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Russel Dsouza</title>
    <description>The latest articles on DEV Community by Russel Dsouza (@russel_dsouza_bd584a3cb2a).</description>
    <link>https://dev.to/russel_dsouza_bd584a3cb2a</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3939420%2F1d36f555-5b2b-48e8-a97c-2acbb7603dbd.png</url>
      <title>DEV Community: Russel Dsouza</title>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/russel_dsouza_bd584a3cb2a"/>
    <language>en</language>
    <item>
      <title>Push Notifications in React Native: The Complete 2026 Guide</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 10 Jun 2026 05:25:46 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/push-notifications-in-react-native-the-complete-2026-guide-2d4g</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/push-notifications-in-react-native-the-complete-2026-guide-2d4g</guid>
      <description>&lt;p&gt;If you searched for "React Native push notifications" in 2026 and landed on a 2022 tutorial, half the code in it is broken. The legacy FCM endpoint was sunset in June 2024, Expo Go can't receive push on Android anymore, and Android 13+ needs a runtime permission your old guide doesn't mention.&lt;/p&gt;

&lt;p&gt;This is a working 2026 guide for the three paths developers actually use:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expo + &lt;code&gt;expo-notifications&lt;/code&gt;&lt;/strong&gt; — easiest, recommended for most teams&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bare React Native + &lt;code&gt;@react-native-firebase/messaging&lt;/code&gt;&lt;/strong&gt; — full native control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid with Notifee&lt;/strong&gt; — when you need rich UI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Code is copy-paste ready. Let's go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed and why your old code is broken
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FCM legacy API is dead.&lt;/strong&gt; &lt;code&gt;https://fcm.googleapis.com/fcm/send&lt;/code&gt; returns 404. New endpoint is &lt;code&gt;v1/projects/{id}/messages:send&lt;/code&gt; with OAuth 2.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Go can't receive remote push&lt;/strong&gt; since SDK 53. You need a development build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android 13+ requires &lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt;&lt;/strong&gt; at runtime. Forget it and your notifications silently never appear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS 15+ added interruption levels&lt;/strong&gt; (&lt;code&gt;passive&lt;/code&gt;, &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;time-sensitive&lt;/code&gt;, &lt;code&gt;critical&lt;/code&gt;) that change how notifications break through Focus modes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Path 1: Expo + expo-notifications
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install + dev build
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx expo &lt;span class="nb"&gt;install &lt;/span&gt;expo-notifications expo-device expo-constants
eas build &lt;span class="nt"&gt;--profile&lt;/span&gt; development &lt;span class="nt"&gt;--platform&lt;/span&gt; ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You cannot test push in Expo Go anymore — accept the dev build cost up front.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request permission and get the token
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Device&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-device&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Constants&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-constants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerForPushNotificationsAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDevice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Physical device required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;android&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNotificationChannelAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;importance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AndroidImportance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;vibrationPattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPermissionsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermissionsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;final&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Permission denied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Constants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expoConfig&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;eas&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getExpoPushTokenAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send from your server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Expo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-server-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Expo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendPush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Expo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isExpoPushToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/inbox/42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;expo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chunkPushNotifications&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;expo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendPushNotificationsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chunkPushNotifications&lt;/code&gt; is required — Expo caps batches at 100.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 2: Bare RN + FCM HTTP v1
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @react-native-firebase/app @react-native-firebase/messaging
&lt;span class="nb"&gt;cd &lt;/span&gt;ios &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pod &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop &lt;code&gt;google-services.json&lt;/code&gt; into &lt;code&gt;android/app/&lt;/code&gt; and &lt;code&gt;GoogleService-Info.plist&lt;/code&gt; into iOS. Enable Push Notifications + Background Modes in Xcode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permission + token
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;messaging&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native-firebase/messaging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PermissionsAndroid&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerForPushNotifications&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;android&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;PermissionsAndroid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;PermissionsAndroid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;POST_NOTIFICATIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;messaging&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;requestPermission&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;messaging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AuthorizationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DENIED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Permission denied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;messaging&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Server send with FCM HTTP v1
&lt;/h3&gt;

&lt;p&gt;The 2026 endpoint. Authenticate with a service account JSON, not a server key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google-auth-library&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;keyFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;service-account.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.googleapis.com/auth/firebase.messaging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendFcmV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FIREBASE_PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://fcm.googleapis.com/v1/projects/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/messages:send`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/inbox/42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HIGH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;apns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;aps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;interruption-level&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;time-sensitive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two big changes from the legacy API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Platform overrides live in &lt;code&gt;android&lt;/code&gt; and &lt;code&gt;apns&lt;/code&gt; blocks&lt;/li&gt;
&lt;li&gt;OAuth token rotates ~hourly — cache it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Path 3: Notifee for rich UI
&lt;/h2&gt;

&lt;p&gt;Neither expo-notifications nor RNFirebase gives you full control over how a notification looks. Notifee does. Pair it with your delivery layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;notifee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AndroidStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AndroidImportance&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@notifee/react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;messaging&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native-firebase/messaging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;messaging&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setBackgroundMessageHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channelId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;notifee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChannel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;importance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AndroidImportance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;notifee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;displayNotification&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AndroidStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MESSAGING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;person&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reply&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pressAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reply&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mark read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pressAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mark-read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gets you Messenger-style chat notifications, ongoing progress bars, and full-screen incoming-call screens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing every tutorial gets wrong: cold start
&lt;/h2&gt;

&lt;p&gt;When a user taps a notification with your app killed, your handlers are not mounted. You have to check the initial payload synchronously on first render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expo:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useLastNotificationResponse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lastResponse&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lastResponse&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;RNFirebase:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;messaging&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getInitialNotification&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always test by force-quitting the app, killing it from recents, and tapping the notification cold. That's the path that breaks in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token lifecycle (the 90% nobody writes about)
&lt;/h2&gt;

&lt;p&gt;Working &lt;code&gt;getToken()&lt;/code&gt; is 10% of the job. The rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Upsert on every cold start.&lt;/strong&gt; Treat the token your app sends as the source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soft-delete on &lt;code&gt;UNREGISTERED&lt;/code&gt; / &lt;code&gt;NotRegistered&lt;/code&gt;.&lt;/strong&gt; Stale tokens silently kill delivery rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model &lt;code&gt;(user_id, device_id, token, platform, last_seen_at)&lt;/code&gt; rows.&lt;/strong&gt; Not &lt;code&gt;users.push_token&lt;/code&gt;. One user has N devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry with exponential backoff.&lt;/strong&gt; 5xx and 429 are real. 250ms -&amp;gt; 500ms -&amp;gt; 1s -&amp;gt; 2s -&amp;gt; 4s with a circuit breaker.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick decision matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want…&lt;/th&gt;
&lt;th&gt;Use…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fastest setup&lt;/td&gt;
&lt;td&gt;Expo + &lt;code&gt;expo-notifications&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full native control&lt;/td&gt;
&lt;td&gt;Bare RN + &lt;code&gt;@react-native-firebase/messaging&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rich layouts, action buttons, ongoing notifications&lt;/td&gt;
&lt;td&gt;+ Notifee on top&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted dashboard, A/B testing&lt;/td&gt;
&lt;td&gt;OneSignal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle marketing&lt;/td&gt;
&lt;td&gt;Customer.io / Braze&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the whole tree.&lt;/p&gt;




&lt;p&gt;If you want the long-form version with the production server architecture, decision matrix, and PAA section, the canonical post is on &lt;a href="https://www.rapidnative.com/blogs?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-push-notifications-complete-guide-2026" rel="noopener noreferrer"&gt;the RapidNative blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What did your last broken-push-notifications debugging session look like? Drop it in the comments — I'm collecting failure modes for a follow-up.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Side Project to App Store - A Non-Technical Founder's 2026 Guide</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 03 Jun 2026 05:35:53 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/side-project-to-app-store-a-non-technical-founders-2026-guide-28d8</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/side-project-to-app-store-a-non-technical-founders-2026-guide-28d8</guid>
      <description>&lt;p&gt;If you're a non-technical founder shipping a React Native side project, the build is the easy part. The hard part lives between "my Expo dev build runs on my phone" and "my app is live in the App Store."&lt;/p&gt;

&lt;p&gt;This is the practical checklist for the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five stages
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Idea → Concept (3–5 evenings of validation)&lt;/li&gt;
&lt;li&gt;Concept → Prototype (1–2 weekends with an AI app builder)&lt;/li&gt;
&lt;li&gt;Prototype → Real phones (1 week, Expo Go + TestFlight)&lt;/li&gt;
&lt;li&gt;The boring stuff (1–2 weeks of submission prep)&lt;/li&gt;
&lt;li&gt;Submission and review (1–2 weeks)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total: 6–10 weeks of evenings for a solo non-technical founder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: The prototype
&lt;/h2&gt;

&lt;p&gt;For non-technical founders, the modern path is an AI app builder that emits real React Native and Expo code — not the no-code platforms that lock you into a hosted runtime. The output is normal Expo source: you can clone it, extend it, run &lt;code&gt;npx expo prebuild&lt;/code&gt;, and ship it through EAS like any other React Native project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After exporting from the AI builder:&lt;/span&gt;
git clone &amp;lt;your-repo&amp;gt;
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npx expo start
&lt;span class="c"&gt;# Or to build for the stores:&lt;/span&gt;
eas build &lt;span class="nt"&gt;--platform&lt;/span&gt; all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is yours. No lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: The submission checklist (where most projects die)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Developer accounts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apple Developer Program     $99/year
Google Play Console         $25 one-time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apple takes 1–3 days for identity verification. Google now requires a 14-day closed-testing track with 12 testers before a first-time developer can publish their first app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Required policy + flows
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Privacy policy at a public URL&lt;/li&gt;
&lt;li&gt;In-app account deletion (both stores require this; "email us" is rejected)&lt;/li&gt;
&lt;li&gt;Sign in with Apple if you offer any third-party sign-in&lt;/li&gt;
&lt;li&gt;App Tracking Transparency prompt if any SDK touches an identifier&lt;/li&gt;
&lt;li&gt;Privacy manifest declaring third-party SDKs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Required assets
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;App icon: 1024×1024 PNG, no transparency, no rounded corners&lt;/li&gt;
&lt;li&gt;Six screenshots minimum&lt;/li&gt;
&lt;li&gt;30-char app name, 80-char subtitle, 4000-char description&lt;/li&gt;
&lt;li&gt;Optional: 15–30s preview video, no audio narration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  App Store Connect setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bundle ID (set once, can't change without re-publishing as a new app)&lt;/li&gt;
&lt;li&gt;Primary + secondary category&lt;/li&gt;
&lt;li&gt;IAP products configured &lt;em&gt;before&lt;/em&gt; submission&lt;/li&gt;
&lt;li&gt;Push notification certificates if applicable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stage 5: Surviving review
&lt;/h2&gt;

&lt;p&gt;The top first-submission rejection reasons in 2026, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Missing privacy declarations (anything you collect, including crash logs, must be declared)&lt;/li&gt;
&lt;li&gt;Missing Sign in with Apple alongside Google or Facebook sign-in&lt;/li&gt;
&lt;li&gt;Missing in-app account deletion&lt;/li&gt;
&lt;li&gt;"Insufficient functionality" — looks like a website wrapped in WebView&lt;/li&gt;
&lt;li&gt;Misleading screenshots showing features that don't exist&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Submit Tuesday or Wednesday. First-app review is currently 5–7 days; resubmissions are 24–48 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  After launch
&lt;/h2&gt;

&lt;p&gt;Set up three things in the first 90 days:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ASO experiments&lt;/strong&gt;: test subtitle, first screenshot, and icon — the only acquisition lever that compounds without ad spend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single conversion event&lt;/strong&gt; in analytics. Anything more and you'll read dashboards instead of fixing the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An exit interview flow&lt;/strong&gt; for churned users. The best research interviews come from people who stopped using your app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reading the code
&lt;/h2&gt;

&lt;p&gt;Even if you don't write it, learn to read it. Glance at the generated React Native files weekly. You don't need to be fluent — you need to be able to spot when a component is doing too much, when state is in the wrong place, or when the AI took a shortcut that'll bite later.&lt;/p&gt;

&lt;p&gt;A non-technical founder who can read React Native is the most leveraged version of the role in 2026.&lt;/p&gt;




&lt;p&gt;The full long-form guide with screenshots, stage-by-stage timelines, and the post-launch playbook is &lt;a href="https://www.rapidnative.com/blogs/side-project-to-app-store-founders-guide?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=side-project-to-app-store-founders-guide" rel="noopener noreferrer"&gt;here on our blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to skip Stage 2 entirely and start from a prompt, &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=side-project-to-app-store-founders-guide" rel="noopener noreferrer"&gt;try RapidNative&lt;/a&gt;. It generates real Expo code you can clone and extend.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Posting instructions for Dev.to:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the frontmatter at the top exactly as shown — Dev.to parses it for tags, canonical, and cover image.&lt;/li&gt;
&lt;li&gt;Tags max 4: &lt;code&gt;reactnative&lt;/code&gt;, &lt;code&gt;mobile&lt;/code&gt;, &lt;code&gt;startup&lt;/code&gt;, &lt;code&gt;webdev&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cross-post 1–2 days after the canonical version is live so Google indexes your canonical first.&lt;/li&gt;
&lt;li&gt;Best posting time: Tuesday or Wednesday, 8am EST.&lt;/li&gt;
&lt;li&gt;Engage with the first 5 comments within an hour — Dev.to's algorithm rewards early engagement.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>startup</category>
      <category>webdev</category>
    </item>
    <item>
      <title>We rewrote our pricing page 3 times. Here's what worked.</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Fri, 29 May 2026 09:32:06 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/we-rewrote-our-pricing-page-3-times-heres-what-worked-k92</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/we-rewrote-our-pricing-page-3-times-heres-what-worked-k92</guid>
      <description>&lt;p&gt;We rewrote &lt;code&gt;components/pricing-cards.tsx&lt;/code&gt; three times in six months. Same product, same Stripe checkout, same three license tiers (&lt;code&gt;single | multiple | enterprise&lt;/code&gt;). What changed was the component. The conversion went from ~1.1% to ~3.4%.&lt;/p&gt;

&lt;p&gt;This post is the actual diffs and the actual numbers. No theory, no funnel-graph PNGs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=pricing-page-rewrite" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; sells full-stack React Native + Expo templates. Each template has three license tiers stored as rows in a Supabase &lt;code&gt;product_licenses&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;license_type&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;license_type&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'single'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'multiple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'enterprise'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;price_usd&lt;/span&gt;        &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="n"&gt;allowed_users&lt;/span&gt;    &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="n"&gt;allowed_projects&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="n"&gt;is_commercial&lt;/span&gt;    &lt;span class="nb"&gt;boolean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tiers are correct. The component rendering them was wrong three times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1: textbook, ~1.1%
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;licenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CardTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;license_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CardTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-4xl"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;$&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_usd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;allFeatures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BuyButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three identical cards. Long feature matrix. &lt;code&gt;.toFixed(2)&lt;/code&gt; on every price for "safety." A weak border on the middle card that wasn't visible in dark mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the data said:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;38% of sessions ended without a single &lt;code&gt;pricing_card_focus&lt;/code&gt; event&lt;/li&gt;
&lt;li&gt;Time spent disproportionately on the cheapest card&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;multiple&lt;/code&gt; tier — best per-developer economics — got the least attention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Classic decision-paralysis. Three identical options read as a comparison spreadsheet, not a recommendation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 2: anchor the middle tier, ~1.9%
&lt;/h2&gt;

&lt;p&gt;The fix was one boolean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getLicenseHighlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;multiple&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wired into the card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isHighlighted&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;border-primary shadow-lg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isHighlighted&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Badge&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"absolute -top-3 left-1/2 -translate-x-1/2"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Most Popular
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Badge&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  ...
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And reordered columns left-to-right by price: cheapest, recommended, premium. Buyer's eye now lands on the middle tier first; the others become reference points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; ~1.1% → ~1.9% over four weeks. Almost double from one boolean and a sort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 3: small fixes, ~3.4%
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix 1: drop trailing &lt;code&gt;.00&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The one-liner that lifted conversion the most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-4xl font-bold"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  $&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_usd&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the price is a whole dollar, render the integer. Otherwise two decimals. &lt;code&gt;$49.00 → $49&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This change, isolated for two weeks holding everything else constant, accounted for roughly half the v2→v3 lift. The most embarrassing finding in our analytics this year.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 2: replace the feature matrix with six bullets per card
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"space-y-3"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Check&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed_users&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`Up to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed_users&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; developers`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unlimited developers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Check&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed_projects&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed_projects&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; projects`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unlimited projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Check&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_commercial&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Commercial use included&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Personal use&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Check&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three claims from the license row, three from the product. Six bullets per card. The diff between Single and Team becomes immediately readable: &lt;em&gt;one developer, one project&lt;/em&gt; vs. &lt;em&gt;up to N developers, N projects&lt;/em&gt;. That's the comparison the buyer is making.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 3: refund/ownership line under the CTA
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CardFooter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BuyButton&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-xs text-muted-foreground mt-3"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    7-day refund · lifetime updates · own the code
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CardFooter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last objection happens at the click. The answer has to live there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Conversion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;V1&lt;/td&gt;
&lt;td&gt;Identical cards, feature matrix, &lt;code&gt;.toFixed(2)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;~1.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V2&lt;/td&gt;
&lt;td&gt;"Most Popular" badge on &lt;code&gt;multiple&lt;/code&gt;, price-sorted&lt;/td&gt;
&lt;td&gt;~1.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V3&lt;/td&gt;
&lt;td&gt;Drop &lt;code&gt;.00&lt;/code&gt;, 6 bullets, refund line under CTA&lt;/td&gt;
&lt;td&gt;~3.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The actual lesson for devs
&lt;/h2&gt;

&lt;p&gt;Most of what helped wasn't pricing strategy. It was deleting fussiness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layout fussiness&lt;/strong&gt; → anchor a tier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content fussiness&lt;/strong&gt; → cut to 6 bullets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Formatting fussiness&lt;/strong&gt; → drop trailing zeros&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three rewrites, three layers peeled. Ship them in order, not as one A/B test. Watch each for two weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to copy tonight
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Anchor the middle tier&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isHighlighted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;license_type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;multiple&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Smart price rendering&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_usd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Refund line lives by the CTA, not the page footer&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BuyButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-xs"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;7-day refund · lifetime updates&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the diff. The component is in &lt;code&gt;components/pricing-cards.tsx&lt;/code&gt;. The rest of the engineering — Supabase rows, Stripe Checkout, Edge Function license grants — didn't change once across all three rewrites.&lt;/p&gt;

&lt;p&gt;For more on the React Native + Expo stack behind this, the &lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;Expo docs on EAS Build&lt;/a&gt; and &lt;a href="https://stripe.com/resources/more/pricing-experiments" rel="noopener noreferrer"&gt;Stripe's pricing experiments guide&lt;/a&gt; are the two external reads worth the time.&lt;/p&gt;




&lt;p&gt;What's the smallest pricing-page change that moved a real number for you? Drop the diff in the comments — I'm collecting examples from indie devs and small SaaS teams.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>stripe</category>
    </item>
    <item>
      <title>React Native AI app cost in 2026, line by line</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Thu, 28 May 2026 13:17:02 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/react-native-ai-app-cost-in-2026-line-by-line-51l</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/react-native-ai-app-cost-in-2026-line-by-line-51l</guid>
      <description>&lt;p&gt;Every agency quote for a React Native AI app collapses into one fuzzy bracket: &lt;code&gt;$60K–$150K&lt;/code&gt;. That bracket is useless. Here's the actual line-item breakdown — hours, dollars, API bills — for what a &lt;strong&gt;React Native AI app cost&lt;/strong&gt; looks like in 2026.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Solo indie, from scratch:   436–832 hours = $32K–$62K
Agency, from scratch:                       $80K–$180K
Template starting point:    $79 + 20–60 hrs customization
Year-one ongoing (API + infra):             $3.6K–$30K
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The line items
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Discovery + IA + design — 60-100 hrs
&lt;/h3&gt;

&lt;p&gt;The part juniors think doesn't count. Data model. Routing graph. What an "AI session" means in your domain. Empty states. Permission denials. Offline behavior. &lt;strong&gt;Under-estimated by 4x in every indie project I've seen.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Auth + Supabase + RLS — 40-80 hrs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The 30-line policy file that takes a full week to get right&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users can only see their own transcripts"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;transcripts&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users can insert their own transcripts"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;transcripts&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ...repeat for every table, every action&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus schema migrations, OAuth wiring, edge functions for secrets. The &lt;a href="https://supabase.com/docs/guides/auth/row-level-security" rel="noopener noreferrer"&gt;Supabase RLS guide&lt;/a&gt; is excellent but policy &lt;em&gt;design&lt;/em&gt; is on you.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. AI integration — 80-160 hrs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AI shape&lt;/th&gt;
&lt;th&gt;Hours&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Image → JSON (vision)&lt;/td&gt;
&lt;td&gt;60–100&lt;/td&gt;
&lt;td&gt;Camera, compression, retry, structured parsing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio → transcript&lt;/td&gt;
&lt;td&gt;100–160&lt;/td&gt;
&lt;td&gt;Streaming chunks, partial results, background mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document → chat (RAG)&lt;/td&gt;
&lt;td&gt;120–200&lt;/td&gt;
&lt;td&gt;Chunking, embeddings, vector store, citations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4. UI screens — 120-240 hrs
&lt;/h3&gt;

&lt;p&gt;A real AI app is not one chat screen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;onboarding (4–6 screens)&lt;/li&gt;
&lt;li&gt;history list + detail&lt;/li&gt;
&lt;li&gt;settings&lt;/li&gt;
&lt;li&gt;paywall + manage-subscription&lt;/li&gt;
&lt;li&gt;empty / error / offline states&lt;/li&gt;
&lt;li&gt;accessibility labels on everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Twenty-plus production screens, each needing a dark mode, a tablet layout, and a VoiceOver label. This is the block that swallows half the budget on every project I've seen.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Stripe + license grants — 40-60 hrs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The 200-line webhook handler nobody writes a tutorial for&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout.session.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;grantLicense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer.subscription.deleted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;revokeLicense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...11 more cases for restore-purchases, refunds, disputes&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS IAP restore flow alone is two days.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Push notifications — 16-32 hrs
&lt;/h3&gt;

&lt;p&gt;Topic logic, do-not-disturb, deep links, permission UX, silent-push retries. Expo's push service is great but the &lt;em&gt;policy&lt;/em&gt; of when to send is yours.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. EAS Build + store submission — 20-40 hrs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eas build &lt;span class="nt"&gt;--platform&lt;/span&gt; all &lt;span class="nt"&gt;--profile&lt;/span&gt; production
eas submit &lt;span class="nt"&gt;-p&lt;/span&gt; ios &lt;span class="nt"&gt;--latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two commands. Forty hours of metadata, screenshots at every device size, privacy nutrition labels, ATT prompts, the "what does your AI actually do" review questions, one rejection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;EAS docs&lt;/a&gt; cover the build; nobody covers the rejection.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. QA + edge cases + accessibility — 60-120 hrs
&lt;/h3&gt;

&lt;p&gt;VoiceOver labels. RTL. Dark mode. Tablet. Tiny phones. 4,000-item scroll test. Apple genuinely checks accessibility now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recurring costs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPT-4o Vision:        ~$0.01 / image
OpenAI Whisper:       ~$0.006 / minute audio
Claude/GPT-4 chat:    $3–$15 / 1M input tokens
Supabase Pro:         $25 / month
EAS team plan:        $99 / month
pgvector (in Supabase):     $0
Pinecone (if you outgrow):  $70+ / month
Sentry developer:     $26 / month
Apple Dev Program:    $99 / year
Maintenance:          15–25% of initial build / year
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Year-one all-in for an indie React Native AI app: &lt;strong&gt;$45K–$78K&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison: scratch vs template
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line item&lt;/th&gt;
&lt;th&gt;From scratch&lt;/th&gt;
&lt;th&gt;Production template&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth + Supabase + RLS&lt;/td&gt;
&lt;td&gt;40–80 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI provider wiring (BYOK)&lt;/td&gt;
&lt;td&gt;80–160 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20+ screens&lt;/td&gt;
&lt;td&gt;120–240 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe + license grant&lt;/td&gt;
&lt;td&gt;40–60 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push notifications&lt;/td&gt;
&lt;td&gt;16–32 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAS Build config&lt;/td&gt;
&lt;td&gt;8–16 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;One-time cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$32K–$62K&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$79&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customization&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;20–60 hrs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to App Store&lt;/td&gt;
&lt;td&gt;4–7 months&lt;/td&gt;
&lt;td&gt;2–6 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The $79 isn't a discount on the engineering — it's the cost of buying a copy of architecture that already exists. If you want to see what a production template actually ships (RLS policies, BYOK AI wiring, Stripe license grants, 20+ screens), the &lt;a href="https://www.applighter.com/blog?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=honest-cost-react-native-ai-app-2026" rel="noopener noreferrer"&gt;Applighter blog&lt;/a&gt; has the full line-item breakdown and three real budget shapes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three real shapes, three real budgets
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Image → AI insight&lt;/strong&gt; (calorie scanner, plant ID, skincare): 380–620 hrs from scratch ($28K–$47K). Template: $79 + ~30 hrs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice → transcript + summary&lt;/strong&gt;: 480–740 hrs from scratch ($36K–$56K). Template: $79 + ~40 hrs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Document → chat (RAG)&lt;/strong&gt;: 520–820 hrs from scratch ($39K–$62K). Template: $79 + ~50 hrs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What free boilerplates miss
&lt;/h2&gt;

&lt;p&gt;A free Expo boilerplate gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expo Router setup&lt;/li&gt;
&lt;li&gt;A login screen&lt;/li&gt;
&lt;li&gt;Maybe a tab bar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A free boilerplate does &lt;em&gt;not&lt;/em&gt; give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI provider abstraction (BYOK pattern)&lt;/li&gt;
&lt;li&gt;RLS policies + migrations&lt;/li&gt;
&lt;li&gt;Stripe webhook handlers with signature verification&lt;/li&gt;
&lt;li&gt;License grant logic&lt;/li&gt;
&lt;li&gt;20+ designer-vetted screens&lt;/li&gt;
&lt;li&gt;Push notification topic logic&lt;/li&gt;
&lt;li&gt;Accessibility audit&lt;/li&gt;
&lt;li&gt;Dark mode tested across every screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paid UI kits from competitors like &lt;a href="https://reactnativebase.com" rel="nofollow noopener noreferrer"&gt;React Native Base&lt;/a&gt; ($99–$299) typically ship as &lt;em&gt;UI only&lt;/em&gt; — backend, AI, and licensing are still yours. That's where 70% of the hours live.&lt;/p&gt;

&lt;h2&gt;
  
  
  When build-from-scratch wins
&lt;/h2&gt;

&lt;p&gt;Three cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Genuinely novel on-device AI (ExecuTorch, custom-trained vision)&lt;/li&gt;
&lt;li&gt;HIPAA / FedRAMP / on-prem constraints a template's RLS can't satisfy&lt;/li&gt;
&lt;li&gt;Existing in-house React Native team with idle capacity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For everything else — solo indies, two-person teams, agencies shipping client demos fast — buy the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Do production AI templates include API keys?&lt;/strong&gt;&lt;br&gt;
A: The good ones don't — they use a BYOK pattern. Pick OpenAI, Anthropic, Whisper, Deepgram, AssemblyAI — your call. You pay the provider directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Year-one ongoing costs?&lt;/strong&gt;&lt;br&gt;
A: $3.6K–$30K. Mostly AI API spend. Supabase + EAS + Sentry are ~$2K/year combined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is React Native fast enough for streaming AI responses?&lt;/strong&gt;&lt;br&gt;
A: Yes. Reanimated v4 handles streaming text and audio waveforms at 60fps. See &lt;a href="https://reactnative.dev/docs/performance" rel="noopener noreferrer"&gt;React Native performance docs&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;What did your last React Native AI build actually cost — in hours or dollars? I'm collecting real numbers from indie devs in the comments, because the honest ones are almost impossible to find online.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>ai</category>
    </item>
    <item>
      <title>Mobile App Analytics for React Native — What to Track, What to Ignore</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 27 May 2026 07:42:39 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/mobile-app-analytics-for-react-native-what-to-track-what-to-ignore-4o0n</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/mobile-app-analytics-for-react-native-what-to-track-what-to-ignore-4o0n</guid>
      <description>&lt;p&gt;Stop tracking 60 events. Pick 6–10. Define your activation event before you write any tracking code. Watch the cohort retention curve, not total installs. One product analytics SDK + one crash tool. Defer attribution until you spend on ads.&lt;/p&gt;

&lt;p&gt;Most React Native apps don't die from bad code. They die from invisible problems — users who never finished onboarding, a checkout step that lost half the funnel, a feature nobody opened after week one. You can't fix what you can't see.&lt;/p&gt;

&lt;p&gt;The trap is that "set up analytics" gets pushed to next sprint, then the sprint after, then forever. By the time you actually look at your numbers, you've shipped three months of features blind. This post is the minimum viable analytics setup you should have running &lt;strong&gt;before&lt;/strong&gt; you push to TestFlight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five questions every mobile app has to answer
&lt;/h2&gt;

&lt;p&gt;AARRR — Acquisition, Activation, Retention, Referral, Revenue. Old framework, still right.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What you'd actually track&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Acquisition&lt;/td&gt;
&lt;td&gt;How do users find us?&lt;/td&gt;
&lt;td&gt;Installs by channel, CPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Activation&lt;/td&gt;
&lt;td&gt;Did the first session land?&lt;/td&gt;
&lt;td&gt;Onboarding complete, first key action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention&lt;/td&gt;
&lt;td&gt;Do they come back?&lt;/td&gt;
&lt;td&gt;D1/D7/D30, DAU/MAU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Referral&lt;/td&gt;
&lt;td&gt;Do they bring friends?&lt;/td&gt;
&lt;td&gt;Invite rate, viral coefficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue&lt;/td&gt;
&lt;td&gt;Does any of this pay?&lt;/td&gt;
&lt;td&gt;ARPU, LTV, conversion to paid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You don't need a metric in every row on day one. You need to know &lt;strong&gt;which row is leaking the worst&lt;/strong&gt;, and you can only know that if you can see all five.&lt;/p&gt;

&lt;h2&gt;
  
  
  The single most important event: activation
&lt;/h2&gt;

&lt;p&gt;Pick a behavior that strongly correlates with week-two retention. Examples from real apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dropbox: "one file in one folder on two devices"&lt;/li&gt;
&lt;li&gt;Facebook (early): "seven friends in ten days"&lt;/li&gt;
&lt;li&gt;Fitness app: "logged one workout in first 48h"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write this down before you write any tracking code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A user is activated when they ___ within ___ minutes/days of installing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the highest-leverage exercise in your first three months. Get this right and every other product decision becomes legible: &lt;em&gt;does this change move more new users to activation?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Track this in your React Native app
&lt;/h2&gt;

&lt;p&gt;A minimal event schema for any consumer mobile app. Pick a tool (Firebase, Amplitude, Mixpanel, or PostHog — they all have RN/Expo SDKs) and instrument these first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// events.ts — your single source of truth&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;APP_OPEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app_open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;SIGN_UP_COMPLETE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sign_up_complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ONBOARDING_COMPLETE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onboarding_complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ACTIVATION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;activation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// your one defined event&lt;/span&gt;
  &lt;span class="na"&gt;PURCHASE_COMPLETED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;purchase_completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;SHARE_INITIATED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;share_initiated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;PUSH_RECEIVED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push_received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;PUSH_OPENED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push_opened&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EventName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tracking helper that's tool-agnostic, so you can swap providers without a rewrite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// analytics.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Posthog&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posthog-react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__DEV__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[track]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;Posthog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// attach user properties at sign-up&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;identify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;traits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Posthog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;traits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call sites stay clean and typed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// in OnboardingScreen.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Events&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/analytics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleComplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveProfile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ONBOARDING_COMPLETE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup_v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;steps_skipped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;skippedSteps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things this does that ad-hoc SDK calls don't:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Snake_case event names enforced&lt;/strong&gt; via the const object — no &lt;code&gt;Purchase&lt;/code&gt; / &lt;code&gt;purchase&lt;/code&gt; / &lt;code&gt;purchasedItem&lt;/code&gt; drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single switch point&lt;/strong&gt; for swapping providers — change &lt;code&gt;analytics.ts&lt;/code&gt;, every call site keeps working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;DEV&lt;/strong&gt; logging&lt;/strong&gt; so you can verify events in the Metro console before they hit production.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  User properties (do this once, slice everything forever)
&lt;/h2&gt;

&lt;p&gt;The biggest analytics regret most teams have is not setting user properties on day one. Once they're attached, every event you've ever tracked becomes sliceable by them retroactively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// on first install&lt;/span&gt;
&lt;span class="nf"&gt;identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;install_source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;organic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// or attribution source&lt;/span&gt;
  &lt;span class="na"&gt;signup_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// 'ios' | 'android'&lt;/span&gt;
  &lt;span class="na"&gt;app_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DeviceInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVersion&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// on plan change&lt;/span&gt;
&lt;span class="nf"&gt;identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can ask "what's D7 retention for organic-install Pro users on iOS?" without touching tracking code again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chart that matters more than DAU
&lt;/h2&gt;

&lt;p&gt;The single most useful chart in mobile analytics is the &lt;strong&gt;cohort retention curve&lt;/strong&gt; — a triangle where each row is the install week and each column is the percentage of that cohort still active N weeks later.&lt;/p&gt;

&lt;p&gt;If the curve flattens around D30, you have something. If it keeps dropping toward zero, you don't — no matter what the install counter says. Most analytics tools (Amplitude, Mixpanel, PostHog) ship this view out of the box; Firebase requires a bit more setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vanity metrics to delete from your dashboard
&lt;/h2&gt;

&lt;p&gt;These always go up, which is exactly why they're useless for decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total installs&lt;/li&gt;
&lt;li&gt;Total registered users&lt;/li&gt;
&lt;li&gt;Total screen views without context&lt;/li&gt;
&lt;li&gt;Followers on social&lt;/li&gt;
&lt;li&gt;Average time in app, isolated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Replace each with a ratio or a cohort:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- Total installs (12,438)
&lt;/span&gt;&lt;span class="gi"&gt;+ 30-day retained users from last month's installs (1,247 / 9,800 = 12.7%)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- Page views (84,221)
&lt;/span&gt;&lt;span class="gi"&gt;+ Screen views per session for activated users (median 14)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- Average session length (4m 12s)
&lt;/span&gt;&lt;span class="gi"&gt;+ % of sessions where activation event fired (38%)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The discipline of converting absolute numbers to ratios is the difference between a dashboard and a decision-making tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day-one stack (don't overthink this)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ ONE product analytics SDK   → Firebase | Amplitude | Mixpanel | PostHog
✅ ONE crash tracker            → Sentry (or Crashlytics)
✅ 6–10 events                  → not 60
✅ Snake_case names             → enforced in a const object
✅ User properties attached     → on sign-up + on plan change

⏸️ Attribution (AppsFlyer/Adjust) → defer until you spend on paid acquisition
⏸️ Session replay               → defer until activation rate stabilizes
⏸️ A/B testing platform         → defer until you have a hypothesis worth testing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stack three analytics tools on launch day and you'll end up with three half-broken instrumentations and zero confidence in the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build–measure–learn loop, for real
&lt;/h2&gt;

&lt;p&gt;The teams that compound are the ones who decided, on day one, which question they were trying to answer — and built the tracking against that question first. They didn't install Mixpanel and then figure out what to track. They figured out the activation event first, then picked the tool, then built the screens.&lt;/p&gt;

&lt;p&gt;If you're earlier than that — still figuring out what the app even is — the bigger lever is &lt;strong&gt;speed of iteration&lt;/strong&gt;, not analytics tooling. The faster you can put a build in front of real users and watch what they do, the sooner you'll know what to instrument in the first place. &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=mobile-app-analytics-what-to-track" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates real React Native + Expo source from a prompt — meaning you ship, instrument with the snippets above, watch what happens, and iterate in hours instead of sprints. The code is yours, so swapping in PostHog or Amplitude is just a normal RN package install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How many events should a new app track?&lt;/strong&gt;&lt;br&gt;
6–10. Tracking 60 on day one almost guarantees none of them will be reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's a good D1 retention rate?&lt;/strong&gt;&lt;br&gt;
Rough rule: 25–40% for consumer apps. The &lt;em&gt;shape&lt;/em&gt; of the curve matters more than any single day — does it flatten, or keep falling?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a paid tool for an MVP?&lt;/strong&gt;&lt;br&gt;
No. Firebase is free, has solid RN/Expo SDKs, and covers events, funnels, and retention. Most MVPs don't outgrow it until they have meaningful revenue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mixpanel vs Amplitude vs PostHog for React Native?&lt;/strong&gt;&lt;br&gt;
All three have first-class RN SDKs. Amplitude has the strongest cohort UI for product-led growth teams. Mixpanel has mature reports and a generous free tier. PostHog is open-source-friendly and self-hostable if you care about data ownership. Firebase is the free default if integrated with the rest of Google's stack matters more than UI polish.&lt;/p&gt;




&lt;p&gt;What are you tracking in your React Native app right now — and which event do you wish you'd instrumented from day one? Drop a comment with your activation event; I'm collecting examples from the indie dev side.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>analytics</category>
      <category>startup</category>
    </item>
    <item>
      <title>3 Pricing Page Rewrites: What Finally Converted</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Tue, 19 May 2026 10:07:20 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/3-pricing-page-rewrites-what-finally-converted-1olm</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/3-pricing-page-rewrites-what-finally-converted-1olm</guid>
      <description>&lt;p&gt;We rewrote the Applighter pricing page three times in eleven months. Here is what each version was, why it failed (or worked), and the structure we kept.&lt;/p&gt;

&lt;p&gt;For context: we sell production-ready React Native templates. Each one is a full Expo app with a Supabase backend, NativeWind styling, AI integration where relevant, and a Stripe-powered license grant on purchase. Pricing is one-time, $79 per template, no subscription. That detail matters because two of our three rewrites pretended otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1: the SaaS-tier menu
&lt;/h2&gt;

&lt;p&gt;The first page was the one every founder builds in week one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Starter $79  | Pro $79      | Team $79     |
|--------------|--------------|--------------|
| ✓ Source     | ✓ Source     | ✓ Source     |
| ✓ Backend    | ✓ Backend    | ✓ Backend    |
|              | ✓ Most       |              |
|              |   Popular    |              |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three columns. "Most Popular" badge. Monthly/annual toggle (we don't sell monthly anything). It looked professional. It looked familiar.&lt;/p&gt;

&lt;p&gt;Conversion was bad. Support email volume was worse. The questions were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Is the $79 per template or for the whole library?"&lt;/li&gt;
&lt;li&gt;"Do I have to pay every month?"&lt;/li&gt;
&lt;li&gt;"If I pay $79, do I get the source code or just access to a dashboard?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The page was telling buyers a SaaS story. The product was a zip of TypeScript and a Supabase project. Mismatch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Don't borrow the pricing UX of a different business model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 2: the comparison wall
&lt;/h2&gt;

&lt;p&gt;When buyers are confused, every founder's reflex is &lt;em&gt;give them more information&lt;/em&gt;. We built a 32-row feature matrix comparing Applighter to "DIY," "UI templates," and "marketplace bundles."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;featureMatrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TypeScript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="na"&gt;applighter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;diy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;depends&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Supabase backend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="na"&gt;applighter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;diy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="na"&gt;applighter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;diy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;build it&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Commercial license&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;applighter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;diy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;n/a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="na"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ext&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ext&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 28 more rows&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Session recordings: median scroll-past in under three seconds.&lt;/p&gt;

&lt;p&gt;The table was answering questions nobody was asking. The questions buyers actually had — &lt;em&gt;can I use this commercially, what happens after I pay, what if it doesn't work for my project&lt;/em&gt; — were buried in a footer FAQ link nobody clicked.&lt;/p&gt;

&lt;p&gt;Worse, the comparison framed the product as defensive. Buyers don't pay $79 to validate your competitive position.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Read your support inbox before you read CRO articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 3: one product, three license sizes
&lt;/h2&gt;

&lt;p&gt;The current page is shorter than either previous version. Top to bottom:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hero with the product, not the price.&lt;/strong&gt; Screenshot of the template running on an iPhone. The price exists, but it isn't the headline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License picker, not a tier ladder.&lt;/strong&gt; Single / Multiple / Enterprise — described by &lt;em&gt;who is buying&lt;/em&gt;, not by features unlocked. Every license includes the same source code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's included, plainly.&lt;/strong&gt; Six bullets. No checkmarks. No comparisons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refund policy next to the buy button.&lt;/strong&gt; Seven-day, no-questions. The single biggest move on V3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FAQ above the fold of the second screen.&lt;/strong&gt; Pulled directly from real support tickets, in the buyer's own words.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One soft secondary path: browse other templates.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/buy/page.tsx (sketch)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BuyPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getLicenseTiers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Hero&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LicensePicker&lt;/span&gt; &lt;span class="na"&gt;tiers&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tiers&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RefundLine&lt;/span&gt; &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"7-day money-back"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BuyButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Faq&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;faqJson&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RelatedTemplates&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;License tiers are stored in Postgres and read at request time, so we can adjust per-template without a deploy. The FAQ is read from &lt;code&gt;data/faq.json&lt;/code&gt; and used on both the homepage and the buy page so the messaging stays consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What conversion actually moved
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pricing page&lt;/th&gt;
&lt;th&gt;Layout&lt;/th&gt;
&lt;th&gt;Optimized for&lt;/th&gt;
&lt;th&gt;What buyers did&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;V1: SaaS tier menu&lt;/td&gt;
&lt;td&gt;Starter / Pro / Team&lt;/td&gt;
&lt;td&gt;Looking like a "real" company&lt;/td&gt;
&lt;td&gt;Asked support if it was a subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V2: Comparison wall&lt;/td&gt;
&lt;td&gt;32-row feature matrix&lt;/td&gt;
&lt;td&gt;Defending against alternatives&lt;/td&gt;
&lt;td&gt;Scrolled past in 3 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V3: License picker&lt;/td&gt;
&lt;td&gt;Hero → license sizes → FAQ → refund&lt;/td&gt;
&lt;td&gt;Removing friction the buyer named&lt;/td&gt;
&lt;td&gt;Bought, or refunded inside 7 days (rarely)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;V1 → V2 made things &lt;strong&gt;worse&lt;/strong&gt;. More info to a confused buyer = more confusion.&lt;/li&gt;
&lt;li&gt;V2 → V3 was the largest jump. Refund line + license picker carried most of it.&lt;/li&gt;
&lt;li&gt;Refund rate dropped after V3, not V2. Refunds correlate with surprise.&lt;/li&gt;
&lt;li&gt;Support ticket volume on pricing questions fell roughly in half.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We did not run A/B tests. Traffic on a $79 product wasn't high enough for significance in a useful timeframe. We talked to buyers, watched email, and changed the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five lessons that survived
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Match the pricing UX to the business model.&lt;/strong&gt; A one-time digital product is not a SaaS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The buyer's questions are not your competitor's questions.&lt;/strong&gt; Read the inbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The refund policy is part of the offer.&lt;/strong&gt; Surface it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less ships faster than more.&lt;/strong&gt; Every rewrite removed something.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship a page, don't ship an A/B test.&lt;/strong&gt; At our volume, ten buyer conversations beat a multivariate framework.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What we would do differently
&lt;/h2&gt;

&lt;p&gt;Skip V1 and V2. Start at V3. The shape of the right page was sitting in the support inbox the whole time.&lt;/p&gt;

&lt;p&gt;If you want to see the structure in production, the &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=pricing-page-3-rewrites" rel="noopener noreferrer"&gt;Applighter template catalog&lt;/a&gt; is built on the V3 pattern — same hero-then-license-picker-then-refund-line shape on every product page.&lt;/p&gt;

&lt;p&gt;For the React Native + Supabase + Expo stack underneath, the relevant docs are &lt;a href="https://docs.expo.dev/" rel="noopener noreferrer"&gt;Expo&lt;/a&gt;, &lt;a href="https://supabase.com/docs" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;, and &lt;a href="https://docs.stripe.com/payments/checkout" rel="noopener noreferrer"&gt;Stripe Checkout&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you've rewritten your own pricing page and learned something the slow way — especially if you sell a one-time digital product — drop a comment with what moved the needle for you. Curious whether the "refund line next to CTA" pattern holds for other indie devs or whether it's specific to our buyer.&lt;/p&gt;

</description>
      <category>pricing</category>
      <category>conversion</category>
      <category>reactnative</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
