<?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: Famitha M A</title>
    <description>The latest articles on DEV Community by Famitha M A (@famitha_ma_b9c13ab1d324e).</description>
    <link>https://dev.to/famitha_ma_b9c13ab1d324e</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3845749%2Ff3dbd173-2bf0-4de1-82ad-bce78b883a05.png</url>
      <title>DEV Community: Famitha M A</title>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/famitha_ma_b9c13ab1d324e"/>
    <language>en</language>
    <item>
      <title>Delayed Push Permissions in React Native: Why the Third-Session Prompt Wins</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 16 Sep 2026 08:19:52 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/delayed-push-permissions-in-react-native-why-the-third-session-prompt-wins-1ola</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/delayed-push-permissions-in-react-native-why-the-third-session-prompt-wins-1ola</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asking for push permission on first launch is the single most common way apps burn their one clean shot at the iOS prompt&lt;/li&gt;
&lt;li&gt;Waiting until the third session roughly doubles your odds, because the user has now chosen to come back twice&lt;/li&gt;
&lt;li&gt;Session counting is a five-line AsyncStorage pattern&lt;/li&gt;
&lt;li&gt;Better still: trigger on a habit moment (first save, first completed workout, first message) instead of a raw session count&lt;/li&gt;
&lt;li&gt;Skip the custom "pre-permission" explainer modal unless you have data proving it helps. It often does not&lt;/li&gt;
&lt;li&gt;If you already shipped the install-time prompt, there is a recovery path via &lt;code&gt;Linking.openSettings()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The default cap
&lt;/h2&gt;

&lt;p&gt;On iOS you get one native permission prompt. Decline it and the OS will never show it again for your app. The user has to dig through Settings to reverse it, which almost nobody does.&lt;/p&gt;

&lt;p&gt;So the question is not "how do I ask." It is "when does the user have a reason to say yes."&lt;/p&gt;

&lt;p&gt;At install, they don't. They opened your app forty seconds ago. They have no idea what your notifications contain, whether they are useful, or how often they will fire. Blind install-time prompts get rejected constantly, and every rejection is permanent. You are spending an unrecoverable resource at the moment of lowest trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "the third session" actually means
&lt;/h2&gt;

&lt;p&gt;Not "day three." Not "72 hours after install." Session three: the third distinct time the user opens the app. By then they have voluntarily returned twice. They have context. The ask is no longer coming from a stranger.&lt;/p&gt;

&lt;p&gt;The mechanic is a counter that increments once per cold start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&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-async-storage/async-storage&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;SESSION_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session_count&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;bumpSession&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;raw&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SESSION_KEY&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SESSION_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&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;count&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 it once from your root component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;bumpSession&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;setSessionCount&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;Then gate the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;maybeAskForPush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionCount&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;sessionCount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&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="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;getPermissionsAsync&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undetermined&lt;/span&gt;&lt;span class="dl"&gt;'&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="c1"&gt;// already asked, never re-burn&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;undetermined&lt;/code&gt; check matters. It makes the function safe to call on every launch without ever re-triggering anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit moments beat session counts
&lt;/h2&gt;

&lt;p&gt;Session three is the floor, not the ceiling. A raw counter still fires the prompt at an arbitrary moment. The stronger pattern is tying the ask to the first action that notifications will actually serve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A task app asks right after the user sets their first due date&lt;/li&gt;
&lt;li&gt;A fitness app asks after the first completed workout&lt;/li&gt;
&lt;li&gt;A chat app asks when the user sends their first message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that moment the value proposition is self-evident. "Want a reminder when this is due?" needs no explanation. If your app has a moment like that, use it as the trigger and keep the session count as a fallback for users who never hit it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;askAtHabitMoment&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;getPermissionsAsync&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undetermined&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;requestPermissionsAsync&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;Same guard, different call site. That is the whole change.&lt;/p&gt;

&lt;p&gt;If you are prototyping this flow, this gating logic is exactly the kind of glue code worth generating rather than hand-writing. &lt;a href="https://rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=delayed-push-permissions" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; scaffolds React Native and Expo screens from a prompt, and wiring a permission flow like this into a generated onboarding is a five-minute job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pre-permission modal trap
&lt;/h2&gt;

&lt;p&gt;The popular advice says: show a custom in-app modal first ("We'd like to send you helpful reminders!"), and only fire the native prompt if the user taps yes.&lt;/p&gt;

&lt;p&gt;The theory is sound. In practice it is easy to get wrong, and plenty of teams have watched it hurt more than help. You are now showing two interruptions instead of one, and the custom modal gives users a free, zero-cost place to say no. Some users who would have shrugged and accepted the native prompt bounce off the softer one first.&lt;/p&gt;

&lt;p&gt;If you ask at a habit moment, the context does the explaining and the extra modal is redundant. My take: ship without it, and only add it if your own funnel data says otherwise. Do not cargo-cult it in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery for already-shipped apps
&lt;/h2&gt;

&lt;p&gt;Already burned the prompt on install? You cannot re-trigger it, but you can route users to Settings at a habit moment instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;Linking&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recoverPush&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="nx"&gt;canAskAgain&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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undetermined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;canAskAgain&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;requestPermissionsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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="c1"&gt;// one-tap jump to your app's own settings page&lt;/span&gt;
    &lt;span class="nx"&gt;Linking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openSettings&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;Pair &lt;code&gt;Linking.openSettings()&lt;/code&gt; with a single line of UI ("Turn on reminders in Settings") and fire it only at a moment where the user just tried to do something that needs notifications. Cold recovery rates are low, but warm ones, triggered by intent, are worth shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole thing
&lt;/h2&gt;

&lt;p&gt;Counter, guard, trigger, recovery. About 20 lines total, no library beyond AsyncStorage and expo-notifications, and it protects the one prompt you cannot get back.&lt;/p&gt;

&lt;p&gt;What trigger moment are you using in your app: session count, habit moment, or something weirder? Drop it in the comments.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>pushnotifications</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How to Add Payment Processing to Your React Native App (2026 Guide)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Fri, 11 Sep 2026 13:24:35 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/how-to-add-payment-processing-to-your-react-native-app-2026-guide-2b76</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/how-to-add-payment-processing-to-your-react-native-app-2026-guide-2b76</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decide first: digital goods (subscriptions, unlocks) or physical goods/services. That decision, not the SDK, determines what you're allowed to use.&lt;/li&gt;
&lt;li&gt;Physical goods or services: Stripe + Apple Pay + Google Pay via &lt;code&gt;@stripe/stripe-react-native&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Digital goods: native IAP through RevenueCat (&lt;code&gt;react-native-purchases&lt;/code&gt;). US external web checkout and EU alternative processors are now legal, but IAP is still the safe global default.&lt;/li&gt;
&lt;li&gt;The client integration is ~30% of the work. Webhooks, idempotency, and receipt validation are the other 70%.&lt;/li&gt;
&lt;li&gt;Never test on a live card. Stripe test mode, Apple sandbox testers, and Play license testers cover every failure path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every founder building a React Native app hits the same wall: the demo works, users love it, and then someone asks, "So how do people actually pay?" What sounds like a one-afternoon integration turns into a week of reading Apple's guidelines, comparing Stripe versus RevenueCat, and arguing about whether your feature counts as "digital goods." The stack is not the hard part. The rules around the stack are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision that comes before the code
&lt;/h2&gt;

&lt;p&gt;Before you touch an SDK, answer one question: is what your app sells a digital good or a physical good? This is not a UX distinction. It's the rule Apple and Google use to decide whether you're allowed to use Stripe at all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Digital goods&lt;/strong&gt;: subscriptions, premium unlocks, in-game currency, credits, cloud storage, ad-removal. Apple and Google have historically required these to go through their native in-app purchase (IAP) system and taken a 15–30% commission (30% for large developers on standard transactions and year-one subscriptions, 15% after year two or under the Small Business Program).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physical goods and real-world services&lt;/strong&gt;: t-shirts, meal kits, ride-sharing, bookings. These have always been allowed to use third-party processors like Stripe or PayPal at standard card fees (~2.9% + $0.30).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a long time, that was the whole story. Then Epic v. Apple in the US and the DMA in the EU cracked it open. As of late 2025, US developers can link out to an external web checkout for digital goods too (Apple still shows a warning sheet), and EU users on iOS can use fully alternative payment providers inside the app. Your practical options in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Physical/services app&lt;/strong&gt;: use Stripe (or PayPal, Adyen, Braintree) natively in-app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digital goods, US-only&lt;/strong&gt;: IAP is still simplest, but external web checkout via Stripe is now legal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digital goods, EU&lt;/strong&gt;: alternative in-app payment providers are allowed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digital goods, global&lt;/strong&gt;: use IAP for the safest cross-border coverage, or add a web-checkout fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick the answer before you pick an SDK. Everything downstream depends on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payment options compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Fees&lt;/th&gt;
&lt;th&gt;RN SDK&lt;/th&gt;
&lt;th&gt;App Store safe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stripe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Physical goods, services, B2B&lt;/td&gt;
&lt;td&gt;2.9% + $0.30&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@stripe/stripe-react-native&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes for physical, conditionally for digital&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RevenueCat (wraps IAP)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Subscriptions, digital goods&lt;/td&gt;
&lt;td&gt;Free &amp;lt; $2.5k MRR, then 1%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;react-native-purchases&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes (uses Apple/Google IAP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Apple Pay / Google Pay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast checkout on top of a processor&lt;/td&gt;
&lt;td&gt;Same as processor&lt;/td&gt;
&lt;td&gt;Built into Stripe SDK&lt;/td&gt;
&lt;td&gt;Yes for physical goods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PayPal / Braintree&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Markets where PayPal is preferred&lt;/td&gt;
&lt;td&gt;2.9% + fixed&lt;/td&gt;
&lt;td&gt;community SDKs&lt;/td&gt;
&lt;td&gt;Same rules as Stripe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Native IAP directly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Digital goods, no abstraction&lt;/td&gt;
&lt;td&gt;15–30% Apple/Google&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;expo-in-app-purchases&lt;/code&gt; / &lt;code&gt;react-native-iap&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes (required by default)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For 90% of React Native apps in 2026, the answer is one of two combos: &lt;strong&gt;Stripe + Apple/Google Pay&lt;/strong&gt; for physical goods or services, or &lt;strong&gt;RevenueCat on top of native IAP&lt;/strong&gt; for subscriptions and digital unlocks. Let's build both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Stripe
&lt;/h2&gt;

&lt;p&gt;Stripe's official SDK handles PCI compliance for you (card details never touch your server), supports Apple Pay and Google Pay out of the box, and has &lt;a href="https://docs.expo.dev/versions/latest/sdk/stripe/" rel="noopener noreferrer"&gt;first-class Expo support&lt;/a&gt; via a config plugin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: install.&lt;/strong&gt;&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;# Expo (recommended)&lt;/span&gt;
npx expo &lt;span class="nb"&gt;install&lt;/span&gt; @stripe/stripe-react-native

&lt;span class="c"&gt;# Bare React Native&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @stripe/stripe-react-native
&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;With Expo, add the plugin to &lt;code&gt;app.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"@stripe/stripe-react-native"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"merchantIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"merchant.com.yourapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"enableGooglePay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: wrap your app in &lt;code&gt;StripeProvider&lt;/code&gt;.&lt;/strong&gt;&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StripeProvider&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;@stripe/stripe-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;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StripeProvider&lt;/span&gt;
      &lt;span class="na"&gt;publishableKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;merchantIdentifier&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"merchant.com.yourapp"&lt;/span&gt;
      &lt;span class="na"&gt;urlScheme&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"yourapp"&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;RootNavigator&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;StripeProvider&lt;/span&gt;&lt;span class="p"&gt;&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;The publishable key is safe to ship in the bundle. The secret key never leaves your backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: create a Payment Intent on your server.&lt;/strong&gt; This is the piece a lot of tutorials skip. Stripe requires a server-side call so the amount is not manipulable from the client.&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;// Backend: /api/create-payment-intent&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&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;stripe&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;stripe&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;Stripe&lt;/span&gt;&lt;span class="p"&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;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&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;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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;usd&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentIntent&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;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;automatic_payment_methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&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="na"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;paymentIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client_secret&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;strong&gt;Step 4: present the Payment Sheet.&lt;/strong&gt; Stripe's pre-built, PCI-compliant UI supports cards, Apple Pay, Google Pay, and 40+ local payment methods depending on your country.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useStripe&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;@stripe/stripe-react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CheckoutScreen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;initPaymentSheet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;presentPaymentSheet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStripe&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;pay&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;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/create-payment-intent&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;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;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="nx"&gt;amount&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;clientSecret&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;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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;initPaymentSheet&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;merchantDisplayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your App&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;paymentIntentClientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;applePay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;merchantCountryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;googlePay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;merchantCountryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;testEnv&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="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;error&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="nf"&gt;presentPaymentSheet&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;error&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="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;Alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment complete&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Pay"&lt;/span&gt; &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pay&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;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a functioning payment flow. Cards, Apple Pay, and Google Pay all light up in the same sheet with no extra integration work. If you'd rather not hand-wire this, &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-to-add-payment-processing-to-your-react-native-app" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates this exact stack (checkout screen, provider, payment intent endpoint, webhook handler) from a plain-English prompt, and the code exports to your own repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling in-app purchases for digital goods
&lt;/h2&gt;

&lt;p&gt;Selling subscriptions, credits, or premium unlocks? You'll almost certainly go through StoreKit and Google Play Billing. Wiring these APIs directly is painful: different receipt formats, different renewal semantics, different edge cases for pauses, grace periods, refunds, and family sharing. Nearly every serious React Native app in 2026 uses &lt;strong&gt;RevenueCat&lt;/strong&gt; to abstract this away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: install.&lt;/strong&gt;&lt;br&gt;
&lt;/p&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;react-native-purchases
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: configure at app start.&lt;/strong&gt;&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Purchases&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-purchases&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;Purchases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&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;ios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&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;EXPO_PUBLIC_RC_IOS_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="p"&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;EXPO_PUBLIC_RC_ANDROID_KEY&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: fetch offerings and present a paywall.&lt;/strong&gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offerings&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;Purchases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOfferings&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;monthly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;offerings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;availablePackages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;packageType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MONTHLY&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;monthly&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;customerInfo&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;Purchases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;purchasePackage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;monthly&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;isPro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;customerInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entitlements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&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;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;RevenueCat handles receipt validation, cross-platform sync, subscription state, and analytics. You configure products in App Store Connect and Google Play Console, mirror them in RevenueCat, and let the SDK reconcile everything. The commission is still Apple's or Google's. RevenueCat itself is free below $2,500/month in tracked revenue, then 1%.&lt;/p&gt;

&lt;p&gt;One warning: do not route digital-goods subscriptions through Stripe just to dodge the 30% cut globally. Apple will reject the app. The narrow exception (US external web-checkout linking) requires the StoreKit External Purchase Link entitlement plus an explicit user warning sheet. Worth doing at scale, not worth the review risk for a first launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Apple Pay and Google Pay
&lt;/h2&gt;

&lt;p&gt;Good news: if you integrated the Payment Sheet above, Apple Pay and Google Pay are already working. The sheet auto-detects device capability and shows the right button.&lt;/p&gt;

&lt;p&gt;Two extra steps to get store-approved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Apple Pay&lt;/strong&gt;: enable the Apple Pay capability in your Apple Developer account, create a merchant ID (&lt;code&gt;merchant.com.yourapp&lt;/code&gt;), and add it to the Expo config plugin. Test on a real device: the simulator doesn't run Apple Pay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Pay&lt;/strong&gt;: enable Google Pay in the Google Pay Business Console, register your app, and turn on the &lt;code&gt;googlePay&lt;/code&gt; flag. Test on a real Android device with a saved card.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both are free on top of Stripe, and they dramatically improve conversion. &lt;a href="https://stripe.com/en-in/guides/mobile-checkout-optimization" rel="noopener noreferrer"&gt;Stripe's own data&lt;/a&gt; shows one-tap payments increase mobile checkout completion by 60–70%. Treat them as table stakes, not features.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backend half nobody talks about
&lt;/h2&gt;

&lt;p&gt;Client-side integration is maybe 30% of a real payment system. The other 70% lives on your server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks.&lt;/strong&gt; Stripe (and RevenueCat) push events for every state change: &lt;code&gt;payment_intent.succeeded&lt;/code&gt;, &lt;code&gt;invoice.payment_failed&lt;/code&gt;, &lt;code&gt;customer.subscription.updated&lt;/code&gt;. You need an endpoint that verifies signatures, reads the event, and updates your database. Skipping this is the number-one cause of "user paid but the app says they didn't" bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency.&lt;/strong&gt; Payment operations must be safe to retry. Use idempotency keys on Stripe requests and dedupe webhook events by ID. Networks fail; users retry; you cannot charge twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Receipt validation.&lt;/strong&gt; For IAP, Apple and Google receipts must be validated server-side. RevenueCat does this for you. Rolling your own? The &lt;a href="https://developer.apple.com/documentation/appstoreserverapi" rel="noopener noreferrer"&gt;App Store Server API docs&lt;/a&gt; are your source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscription lifecycle.&lt;/strong&gt; Grace periods, billing retries, pauses, refunds, and family sharing mean "active" is not a single boolean. Model it as state, not a flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance.&lt;/strong&gt; Even with Stripe absorbing PCI scope, you still need Strong Customer Authentication (SCA) in the EU/UK. The Payment Sheet handles the 3DS challenge automatically, but your backend must handle the &lt;code&gt;requires_action&lt;/code&gt; state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where most hand-rolled payment integrations quietly leak revenue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing payments safely
&lt;/h2&gt;

&lt;p&gt;Never run a first payment against a live card. Both Stripe and RevenueCat have first-class sandbox modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stripe test mode&lt;/strong&gt;: use test API keys and &lt;a href="https://docs.stripe.com/testing" rel="noopener noreferrer"&gt;Stripe's published test cards&lt;/a&gt;. &lt;code&gt;4242 4242 4242 4242&lt;/code&gt; for success, &lt;code&gt;4000 0000 0000 9995&lt;/code&gt; for insufficient funds, &lt;code&gt;4000 0025 0000 3155&lt;/code&gt; to trigger 3D Secure. Run every failure branch before switching to live keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple sandbox&lt;/strong&gt;: create a Sandbox Tester in App Store Connect, sign into it on your test device under Settings → App Store → Sandbox Account, then run TestFlight or a dev build. Sandbox subscriptions renew every 5 minutes instead of monthly, which is how you test renewals in a reasonable amount of time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Play&lt;/strong&gt;: add license testers in the Play Console, upload to an internal test track, and purchase without being charged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real devices&lt;/strong&gt;: Apple Pay and Google Pay don't work in simulators. Test the payment sheet the way an end user actually sees it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write end-to-end tests for the happy path, the SCA path, the network-failure path, the webhook-late path, and the cancellation path. If those five work, you have a payment system. If any of them fails silently, you have a support ticket queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship payments, not payment bugs
&lt;/h2&gt;

&lt;p&gt;Adding payment processing to a React Native app in 2026 is a solved problem, but only if you make the digital-vs-physical decision first, treat webhooks as first-class code, and test the failure paths as hard as the happy path. Stripe + Apple Pay + Google Pay covers most physical-goods and services apps. RevenueCat covers most subscription apps. The rest is discipline: idempotency, sandbox testing, and a webhook handler you actually trust.&lt;/p&gt;

&lt;p&gt;What are you wiring payments into right now? Drop a comment with your stack (and any App Store review horror stories).&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>stripe</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React Native OTA Updates: A Production Playbook (Rollback Discipline, Canaries, and Session-Safe Applies)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 09 Sep 2026 09:50:47 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/react-native-ota-updates-a-production-playbook-rollback-discipline-canaries-and-session-safe-144d</link>
      <guid>https://dev.to/rapidnative-ai/react-native-ota-updates-a-production-playbook-rollback-discipline-canaries-and-session-safe-144d</guid>
      <description>&lt;p&gt;OTA updates are the closest thing React Native has to a cheat code. You can ship a fix to every user in minutes without waiting on App Store review. And that is exactly why most teams get burned by them: the same speed that ships fixes fast also ships bugs fast, to everyone, at once.&lt;/p&gt;

&lt;p&gt;This is the playbook we settled on after enough OTA incidents to stop treating &lt;code&gt;eas update&lt;/code&gt; like a deploy button.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treat OTA pushes in three tiers: hotfix, canary, staged. Each has different rules.&lt;/li&gt;
&lt;li&gt;Write the rollback runbook before you push, not during the incident.&lt;/li&gt;
&lt;li&gt;Never apply an update mid-session. Download in the background, apply on next cold start or an explicit safe point.&lt;/li&gt;
&lt;li&gt;EAS Update channels are a rollout dimension, not just an environment label. Use them like one.&lt;/li&gt;
&lt;li&gt;OTA is for JS-only changes. Anything touching native code goes through the store, no exceptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why OTA is a superpower most teams misuse
&lt;/h2&gt;

&lt;p&gt;The failure pattern is almost always the same: a team discovers OTA, loves it, and starts using it as their default release path. No staged rollout, no rollback plan, updates applying whenever the client feels like it. It works fine for weeks. Then one bad bundle lands on 100% of users simultaneously, and there is no "roll back the servers" because the broken code is already on the devices.&lt;/p&gt;

&lt;p&gt;The fix is not to avoid OTA. It is to give it the same discipline you give backend deploys.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three update tiers
&lt;/h2&gt;

&lt;p&gt;Not every OTA push is the same kind of event. We label every update as one of three tiers before it goes out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 1: Hotfix.&lt;/strong&gt; A crash or a broken critical path. Goes to 100% as fast as possible. This is the only tier allowed to skip a canary, and it requires two people to sign off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 2: Canary.&lt;/strong&gt; Behavior changes, refactors, anything with real surface area. Ships to a small channel first, soaks, then promotes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 3: Staged.&lt;/strong&gt; Larger feature work that happens to be JS-only. Rolls out gradually with explicit checkpoints.&lt;/p&gt;

&lt;p&gt;Naming the tier forces the conversation: "is this really a hotfix, or are you just impatient?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollback discipline: write the runbook before you push
&lt;/h2&gt;

&lt;p&gt;The rule that has saved us the most: no update ships without a rollback line already written. Concretely, the PR description for an OTA push includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rollback plan:
- Known-good update group: &amp;lt;id of the current production update&amp;gt;
- Command: eas update:republish --group &amp;lt;id&amp;gt; --channel production
- Owner if this goes wrong after hours: &amp;lt;name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Republishing a known-good group is the core move. You are not reverting git and rebuilding under pressure. You are re-pointing the channel at a bundle that was already live and healthy:&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;# find the last known-good update group&lt;/span&gt;
eas update:list &lt;span class="nt"&gt;--branch&lt;/span&gt; production

&lt;span class="c"&gt;# re-point production at it&lt;/span&gt;
eas update:republish &lt;span class="nt"&gt;--group&lt;/span&gt; &amp;lt;update-group-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two habits make this actually work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Record the current production group id in the release checklist before every push. During an incident you do not want to be scrolling &lt;code&gt;update:list&lt;/code&gt; output trying to remember which one was fine.&lt;/li&gt;
&lt;li&gt;Rehearse it once per quarter on a staging channel. A runbook nobody has executed is a rumor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat to internalize: rollback is not instant for users. Devices pick up the republished bundle on their next check, so your real recovery time is rollback plus the client's update-check cadence. That number should be written down somewhere your team can see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session-safe apply: don't swap the bundle mid-scroll
&lt;/h2&gt;

&lt;p&gt;The default temptation is to call &lt;code&gt;Updates.reloadAsync()&lt;/code&gt; the moment a download finishes. From the user's side, that is the app blinking and eating their half-written message.&lt;/p&gt;

&lt;p&gt;The pattern we use: fetch eagerly, apply lazily, at safe points only.&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="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;Updates&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-updates&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&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&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;AppState&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useSafeOtaApply&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;pendingUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&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="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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&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;update&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;Updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkForUpdateAsync&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;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isAvailable&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;Updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchUpdateAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="nx"&gt;pendingUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// downloaded, NOT applied&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// never let update plumbing crash the app&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// apply only when the app goes to background: user is gone anyway&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&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="nx"&gt;state&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;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;background&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;pendingUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reloadAsync&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="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&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;Safe points, in order of preference:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Next cold start (the default: set the update policy to apply on launch and do nothing clever).&lt;/li&gt;
&lt;li&gt;App backgrounded for more than a few seconds.&lt;/li&gt;
&lt;li&gt;An explicit "restart to update" prompt, reserved for Tier 1 hotfixes where you cannot wait.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The one thing you never do is reload while a form, checkout, or upload is in flight. If your app has flows like that, gate the reload behind a "is anything critical in progress" check.&lt;/p&gt;

&lt;h2&gt;
  
  
  EAS channels as a first-class rollout dimension
&lt;/h2&gt;

&lt;p&gt;Most teams use channels as environment labels: &lt;code&gt;development&lt;/code&gt;, &lt;code&gt;preview&lt;/code&gt;, &lt;code&gt;production&lt;/code&gt;. That works, but it leaves the interesting part on the table. Channels are also how you slice your user base for rollout.&lt;/p&gt;

&lt;p&gt;Our channel layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;production        &amp;lt;- everyone on the store build
production-canary &amp;lt;- internal team + opted-in users
staging           &amp;lt;- QA builds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The canary build is the same runtime as production (same native code, same runtime version), just pointed at a different channel. A Tier 2 update flows like this:&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;# 1. ship to canary&lt;/span&gt;
eas update &lt;span class="nt"&gt;--channel&lt;/span&gt; production-canary &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"fix: cart total rounding"&lt;/span&gt;

&lt;span class="c"&gt;# 2. soak. watch crash rate and the specific flow you touched.&lt;/span&gt;

&lt;span class="c"&gt;# 3. promote the same update to production&lt;/span&gt;
eas channel:edit production &lt;span class="nt"&gt;--branch&lt;/span&gt; &amp;lt;canary-branch&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Promoting the branch rather than publishing twice means production gets the exact bytes the canary soaked on. No "it worked on canary because we rebuilt it" mysteries.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;runtimeVersion&lt;/code&gt; fences which builds can receive which updates, keep canary and production builds on the same runtime version policy or your canary stops being representative.&lt;/p&gt;

&lt;h2&gt;
  
  
  When OTA vs. a store release
&lt;/h2&gt;

&lt;p&gt;The boundary is mechanical, not judgment-based:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OTA:&lt;/strong&gt; JS, assets, config that lives in JS. Copy changes, logic fixes, style fixes, feature flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store release:&lt;/strong&gt; any native dependency change, permissions changes, SDK upgrades, anything touching the native project. Also anything Apple would consider a significant change in app behavior; OTA is for fixes and iterations, not for shipping a different app than the one that was reviewed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also run a soft rule: if an OTA diff is big enough that you would want a changelog, it probably deserves the store pipeline and its slower, safer cadence anyway.&lt;/p&gt;

&lt;p&gt;This calculus changes a bit depending on how the app was built. A lot of the apps we see coming out of &lt;a href="https://rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-ota-production-playbook" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; are Expo projects where the entire product surface is JS, which makes almost everything OTA-eligible. That makes the discipline above more important, not less: when every change &lt;em&gt;can&lt;/em&gt; go OTA, the tiering and rollback rules are the only thing standing between you and pushing straight to 100%.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shortest reliable loop we've settled on
&lt;/h2&gt;

&lt;p&gt;For a Tier 2 change, end to end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. PR merged with rollback plan in the description
2. Note current production update group id
3. eas update --channel production-canary
4. Soak on canary (crash rate + touched flow)
5. Promote canary branch to production channel
6. Watch dashboards through one full update-check cycle
7. Log the new known-good group id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven steps, and five of them are copy-paste. The whole loop is still dramatically faster than a store release, which is the point: the discipline does not slow you down enough to notice, but it converts "OTA incident" from an outage into a non-event.&lt;/p&gt;

&lt;p&gt;What does your OTA setup look like: are you applying on cold start, on background, or (be honest) reloading the second the download finishes? Drop your setup in the comments, especially if you have a rollback story.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>mobile</category>
      <category>devops</category>
    </item>
    <item>
      <title>From Figma to Expo Router — turning wireframes into RN scaffolding in an afternoon</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 02 Sep 2026 10:27:17 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/from-figma-to-expo-router-turning-wireframes-into-rn-scaffolding-in-an-afternoon-4g51</link>
      <guid>https://dev.to/rapidnative-ai/from-figma-to-expo-router-turning-wireframes-into-rn-scaffolding-in-an-afternoon-4g51</guid>
      <description>&lt;p&gt;Designer-dev handoff on RN apps has a specific waste: I finish wireframes, hand them to dev, and then wait a day or two while someone types out &lt;code&gt;Stack.Screen name="Foo" component={FooScreen}&lt;/code&gt; for every one of my screens. It's mechanical work, but it blocks me from clicking through my own design in a real device.&lt;/p&gt;

&lt;p&gt;Expo Router's file-based routing changed that for me. As a designer, I can now scaffold the entire route structure of a new prototype in an afternoon — no dev required until I want interactivity beyond navigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading a wireframe as a folder tree
&lt;/h2&gt;

&lt;p&gt;Every well-organized Figma file already has an implicit tree: pages → frames → sub-flows. Turning that into an Expo Router &lt;code&gt;app/&lt;/code&gt; folder is a mechanical mapping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top-level pages in Figma → top-level files in &lt;code&gt;app/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sub-flows within a page → nested folders&lt;/li&gt;
&lt;li&gt;Modal flows → files at the top level marked with &lt;code&gt;presentation: 'modal'&lt;/code&gt; in the parent &lt;code&gt;_layout.tsx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Auth-gated flows → wrap in &lt;code&gt;(auth)/&lt;/code&gt; group&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: a 12-screen social-app prototype with home feed, explore, profile, and auth flow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
├── _layout.tsx
├── (tabs)/
│   ├── _layout.tsx
│   ├── index.tsx        # Feed
│   ├── explore.tsx
│   └── profile.tsx
├── (auth)/
│   ├── _layout.tsx
│   ├── login.tsx
│   ├── signup.tsx
│   └── forgot.tsx
├── post/[id].tsx        # Post detail
├── user/[id].tsx        # User profile detail
├── new-post.tsx         # Modal
└── settings/
    ├── _layout.tsx
    ├── index.tsx
    ├── notifications.tsx
    └── account.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each file gets a placeholder component with the screen name, a text &lt;code&gt;You are on: Feed&lt;/code&gt;, and a couple of &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; components pointing at the next screens per the wireframe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scaffolding script
&lt;/h2&gt;

&lt;p&gt;I wrote a tiny script — you copy a JSON that mirrors your Figma page tree, and it generates the folder + file structure with placeholder components. Nothing fancy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scaffold.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&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;fs&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;path&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;path&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;tree&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./ia.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&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="s2"&gt;`import { View, Text } from 'react-native';
export default function &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;() {
  return &amp;lt;View&amp;gt;&amp;lt;Text&amp;gt;You are on: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/Text&amp;gt;&amp;lt;/View&amp;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;function&lt;/span&gt; &lt;span class="nf"&gt;scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&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="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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&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="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&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;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tsx`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&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="nf"&gt;scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tree&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&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;With &lt;code&gt;ia.json&lt;/code&gt; derived from your Figma page tree (I copy-paste layer names), you get a runnable Expo Router app in seconds. &lt;code&gt;npx expo start&lt;/code&gt;, click through your own design, feel the flow before writing a line of real UI code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What still needs a dev
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;_layout.tsx&lt;/code&gt; files: tab bar styling, header options, auth gating, transitions.&lt;/li&gt;
&lt;li&gt;Any real data fetching or state.&lt;/li&gt;
&lt;li&gt;Custom transitions (modal presentation, shared element).&lt;/li&gt;
&lt;li&gt;Anything that touches native modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But structure? That's mine now. And structure is what determines whether the design feels right in-hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case study
&lt;/h2&gt;

&lt;p&gt;Last month we prototyped a 12-screen fitness-tracker concept. Old workflow: hand Figma to dev, wait 2 days for a clickable prototype. New workflow: I scaffolded the routes in an afternoon, walked through it on my own phone the same evening, caught two flow issues before dev started real work, and by the time engineering opened the ticket the IA was already validated.&lt;/p&gt;

&lt;p&gt;Designer-authored scaffolding won't replace engineering. It moves the design-validation gate 2–3 days earlier — which for us has been the single biggest reduction in "we shipped the wrong flow" incidents.&lt;/p&gt;




&lt;p&gt;If you're a design lead on an RN team and you're still handing wireframes to dev to translate into a nav tree, try scaffolding once. You'll never go back.&lt;/p&gt;

&lt;p&gt;If you want to go beyond scaffolding and generate a complete mobile app from your designs and requirements, you can also try RapidNative: &lt;a href="https://www.rapidnative.com/?utm_medium=blog&amp;amp;utm_campaign=mobile-app-analytics-what-to-track&amp;amp;utm_source=chatgpt.com" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>design</category>
      <category>workflow</category>
    </item>
    <item>
      <title>The Design System of a Trustworthy React Native Checkout Screen</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:59:52 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/the-design-system-of-a-trustworthy-react-native-checkout-screen-3p33</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/the-design-system-of-a-trustworthy-react-native-checkout-screen-3p33</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Checkout is the most emotionally loaded screen in your app. Design it like it.&lt;/li&gt;
&lt;li&gt;Five zones: header, amount, line items, payment method, sticky Pay button.&lt;/li&gt;
&lt;li&gt;Two type sizes. Not three. Not five.&lt;/li&gt;
&lt;li&gt;No custom fonts on this screen. They delay the render of the total.&lt;/li&gt;
&lt;li&gt;Never green on the Pay button. Never red outside errors.&lt;/li&gt;
&lt;li&gt;The three states everyone skips: loading, error, success. Specs below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most React Native checkout screens I audit are functional and forgettable. A stack of &lt;code&gt;&amp;lt;View&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt; in the app's default theme. The user taps through and either succeeds or bounces.&lt;/p&gt;

&lt;p&gt;But this is where the user hands you money. Every pixel choice either builds trust or leaks it.&lt;/p&gt;

&lt;p&gt;Here's the whole system, with code.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────┐
│  ← Back        Order summary │  Zone 1: Header (32pt height)
├─────────────────────────────┤
│                             │
│  Total                      │
│  $42.87 USD                 │  Zone 2: Amount (dominant type)
│                             │
├─────────────────────────────┤
│  Item 1              $30    │
│  Item 2              $10    │  Zone 3: Line items (collapsed)
│  Tax                 $2.87  │
├─────────────────────────────┤
│                             │
│  [ Apple Pay ]              │  Zone 4: Payment method
│  [ Card ending 4242 ]       │
│  [ + Add payment method ]   │
│                             │
├─────────────────────────────┤
│  [    Pay $42.87 USD    ]   │  Zone 5: Pay button (sticky)
└─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zone 5 stays sticky above the keyboard. Zone 2 collapses when the keyboard opens so the Pay button never leaves the viewport.&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;KeyboardAvoidingView&lt;/span&gt;
  &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;ios&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;padding&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;height&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&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;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ScrollView&lt;/span&gt; &lt;span class="na"&gt;contentContainerStyle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flexGrow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&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;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AmountBlock&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;keyboardVisible&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;LineItems&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&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;PaymentMethods&lt;/span&gt; &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;methods&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onSelect&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setMethod&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;ScrollView&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;PayButton&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handlePay&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;KeyboardAvoidingView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Typography: two sizes, that's it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;600&lt;/span&gt;&lt;span class="dl"&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="na"&gt;letterSpacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.5&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;400&lt;/span&gt;&lt;span class="dl"&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="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;Platform system font only. SF Pro Display on iOS, Roboto on Android. Skip &lt;code&gt;expo-font&lt;/code&gt; entirely on this screen. Custom font loading is real work on a cold boot, and anything that delays the render of the total is worth cutting. If you want a number for your own app, profile it instead of trusting a blog post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Color tokens
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;PlatformColor&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;background&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PlatformColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;systemBackground&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FFFFFF&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;text&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PlatformColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;label&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#111111&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="c1"&gt;// Brand primary only if it clears 4.5:1 on white. Otherwise black.&lt;/span&gt;
  &lt;span class="na"&gt;payButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;contrastRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brandPrimary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FFFFFF&lt;/span&gt;&lt;span class="dl"&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="mf"&gt;4.5&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;brandPrimary&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#000000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;errorBg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FFF4F4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;errorText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#B00020&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;Never use red for anything except errors. Never use green on the Pay button. Users associate green with "Buy" but not with "you're about to spend money," which is a subtle difference that still shows up in testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three states everyone forgets
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Loading.&lt;/strong&gt; Swap the label, disable the button, and escalate the copy on a timer. An indeterminate spinner alone reads as a frozen app.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePayingLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processing&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="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;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setElapsed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="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="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;processing&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;setElapsed&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setElapsed&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1000&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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;processing&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;processing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This is taking longer than usual, please wait.&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;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Contacting your bank...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// button label already says "Processing..."&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;Error.&lt;/strong&gt; Full-width bar above the Pay button. One human sentence, one recovery action. Never the raw API code.&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;{&lt;/span&gt;&lt;span class="nx"&gt;error&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;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorBg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&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;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&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;gt;&lt;/span&gt;
      Your card was declined by your bank.
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&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;Pressable&lt;/span&gt; &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;resetToPaymentMethod&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;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;600&lt;/span&gt;&lt;span class="dl"&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;gt;&lt;/span&gt;Try again&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&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;Pressable&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;View&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;&lt;strong&gt;Success.&lt;/strong&gt; Full-screen replacement: large checkmark, order ID, amount, timestamp, email receipt button. Hold 3 seconds minimum before any auto-dismiss so the user feels the transaction land.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two components that carry the screen
&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;// Pay button&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;marginHorizontal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handlePay&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;processing&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;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;600&lt;/span&gt;&lt;span class="dl"&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;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;processing&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Processing...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Pay &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&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;Text&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;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Amount block&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paddingHorizontal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;paddingVertical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&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;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;marginBottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&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;gt;&lt;/span&gt;Total&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&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;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&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="nf"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subtle&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;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currency&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;Text&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;Text&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;View&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;Note the 24pt of vertical padding above the total and the 16pt horizontal margin on the button. Those are the two anchor values on the 8pt grid. Density signals cheapness. Whitespace signals trust.&lt;/p&gt;

&lt;p&gt;If you're scaffolding a fresh app rather than retrofitting one, &lt;a href="https://rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-checkout-design-system" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates the screen structure and theme tokens up front, so you're wiring Stripe on day one instead of rebuilding the amount block by hand again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship it in a sprint
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 1:&lt;/strong&gt; Audit the current checkout against the five zones. Find what's missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 2:&lt;/strong&gt; Rebuild the amount zone and the Pay button. Do these first, they carry most of the perceived trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 3:&lt;/strong&gt; Build the three states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 4:&lt;/strong&gt; Test with five people. Watch where they pause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 5:&lt;/strong&gt; Ship.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest is plumbing to Stripe or Razorpay.&lt;/p&gt;

&lt;p&gt;What does your checkout look like right now? Drop a comment with the ugliest part of yours and I'll tell you which zone it belongs to.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>ux</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How AI Is Making Mobile Accessibility Easier Than Ever</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Mon, 17 Aug 2026 08:27:47 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-ai-is-making-mobile-accessibility-easier-than-ever-25cl</link>
      <guid>https://dev.to/rapidnative-ai/how-ai-is-making-mobile-accessibility-easier-than-ever-25cl</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;95.9% of the top million sites have detectable a11y failures. Mobile apps are not better, they're just harder to audit.&lt;/li&gt;
&lt;li&gt;Four things changed: on-device assistive AI, ML-assisted testing, generated alt text and captions, and AI-assisted code generation.&lt;/li&gt;
&lt;li&gt;On-device AI raises the floor, not the ceiling. VoiceOver guessing "Button, likely a heart" is not a label.&lt;/li&gt;
&lt;li&gt;Rule-based tools catch 30 to 40% of WCAG failures. AI pushes that up, nowhere near 100%.&lt;/li&gt;
&lt;li&gt;The real fix is at generation time: if the model writes the JSX, it can write &lt;code&gt;accessibilityLabel&lt;/code&gt; in the same pass, for free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between apps shipping and apps shipping &lt;em&gt;accessibly&lt;/em&gt; has been widening for a decade. What changed recently is that AI shows up on both sides of the shipping fence: in the runtime layer (the phone), in the tooling layer (audits), and at the code-generation layer, where the app is actually being written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why mobile a11y is harder than the web
&lt;/h2&gt;

&lt;p&gt;Native apps don't share a common semantic model. On the web you have HTML, and HTML has a built-in accessibility contract. On mobile you have UIAccessibility on iOS, AccessibilityNodeInfo on Android, two screen readers with different gesture conventions, and a framework translation layer (React Native, Flutter, SwiftUI) sitting on top of both.&lt;/p&gt;

&lt;p&gt;That means a label that works on one platform can be silently dropped on the other. Here's the version everyone writes first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ships. Announces as "Button". Useless.&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt; &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggleFavorite&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;Icon&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isFavorite&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heart-filled&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;heart&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="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&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 the version that actually works with VoiceOver and TalkBack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;
  &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggleFavorite&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityRole&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Add to favorites"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityHint&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Saves this item to your favorites list"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityState&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isFavorite&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;hitSlop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;12&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;Icon&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isFavorite&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heart-filled&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;heart&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="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&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;Four extra props. Nobody argues they're hard. They just never get written, because they're invisible in the simulator and they're the last-day task that gets cut.&lt;/p&gt;

&lt;p&gt;Regulation is closing in anyway: the European Accessibility Act took effect for consumer digital products in June 2025, and ADA Title II requires state and local government apps to hit WCAG 2.1 AA by April 2026. Regulation without automation is a punishment. AI is what makes the automation possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 1: the OS started covering for you
&lt;/h2&gt;

&lt;p&gt;iOS runs an on-device model that generates image descriptions for photos and unlabeled UI elements. Ship a button with no label and VoiceOver will guess: "Button, likely a play triangle." Android's TalkBack does the same with Gemini Nano, including for images inside third-party apps that never set &lt;code&gt;contentDescription&lt;/code&gt;. Both platforms now generate Live Captions for any audio playing on the device. Voice Control on iOS understands intent, so "tap the little heart" resolves even when your label is &lt;code&gt;favoriteButton&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The floor is rising. The ceiling is not. A guessed description loses to a real one every time, and the guess is silently wrong often enough to matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 2: audits got cheap
&lt;/h2&gt;

&lt;p&gt;Three categories worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rule engines plus ML&lt;/strong&gt; (axe DevTools Mobile, Google's Accessibility Scanner). These now catch labels that exist but are meaningless, like &lt;code&gt;contentDescription="image1.jpg"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual regression plus ML&lt;/strong&gt; (Applause, BrowserStack, testRigor). Diffs screens against known-good corpora and flags contrast ratios, small tap targets, low-legibility fonts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM audits.&lt;/strong&gt; Feed in screenshots plus an accessibility tree dump, get back findings in plain English: "The 'Sign in with Apple' button has no accessibility label; VoiceOver will announce it as 'Button'."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can wire the cheap version of the third one into CI yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// jest + @testing-library/react-native&lt;/span&gt;
&lt;span class="c1"&gt;// Fails the build when an interactive element ships unlabeled.&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;render&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="s2"&gt;@testing-library/react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all buttons are labeled&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="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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UNSAFE_root&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProductScreen&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;buttons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UNSAFE_root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&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;accessibilityRole&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buttons&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="nf"&gt;toBeGreaterThan&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="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&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;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&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;WebAIM's own numbers say automated tooling catches 30 to 40% of WCAG failures. AI raises it, not to 100%. Keep a human in the loop, ideally one who uses the tech.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 3: the missing content is now generated
&lt;/h2&gt;

&lt;p&gt;Alt text from vision models is table stakes. Whisper-class models made captions good enough for accessibility use in most languages. LLMs are producing plain-language versions of dense content on the fly, which is a genuine breakthrough for cognitive accessibility. Live cross-language captioning is built into the call stack on both platforms.&lt;/p&gt;

&lt;p&gt;The catch: generated descriptions are only &lt;em&gt;usually&lt;/em&gt; right, and a screen reader confidently reading a subtly wrong description is worse than one that says nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 4: fix it at generation time
&lt;/h2&gt;

&lt;p&gt;This is the one that matters. When an LLM writes the screen, the accessibility props are not extra work, they're the same tokens. That flips the default from "no labels, ship it" to "labels came with the generation."&lt;/p&gt;

&lt;p&gt;This is the bet &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-ai-is-making-mobile-accessibility-easier" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; is built on: prompt in, real React Native and Expo code out, with &lt;code&gt;accessibilityLabel&lt;/code&gt;, &lt;code&gt;accessibilityRole&lt;/code&gt;, &lt;code&gt;accessibilityHint&lt;/code&gt;, and &lt;code&gt;accessibilityState&lt;/code&gt; attached at generation rather than bolted on later. An image comes out as &lt;code&gt;accessibilityLabel="Sunset over the ocean"&lt;/code&gt; instead of a nameless &lt;code&gt;&amp;lt;Image /&amp;gt;&lt;/code&gt; someone will theoretically label next sprint.&lt;/p&gt;

&lt;p&gt;None of this is exclusive to one tool. A well-prompted Copilot, Cursor, or Claude does the same thing in a hand-written codebase. Put it in the repo rules and it applies to every generation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- .cursorrules / CLAUDE.md / copilot-instructions.md --&amp;gt;&lt;/span&gt;
Every interactive element must have accessibilityRole and accessibilityLabel.
Every Image must have accessibilityLabel or accessible={false}.
Touch targets must be &amp;gt;= 44x44 (use hitSlop when the visual is smaller).
Never use placeholder text as the only label for a TextInput.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What AI still can't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It tells you labels exist, not that the flow is &lt;strong&gt;usable&lt;/strong&gt;. Reading order that jumps around passes every automated check.&lt;/li&gt;
&lt;li&gt;Generated alt text is generic. "A person" is compliant. "The founder of the org you're about to donate to" is what a sighted user sees.&lt;/li&gt;
&lt;li&gt;Cognitive accessibility is barely automatable. The fix is design work, not a tag.&lt;/li&gt;
&lt;li&gt;Live captioning fails contextually: wrong medication name, wrong price, wrong date. Plausible-but-wrong is the worst failure mode for exactly the users you were helping.&lt;/li&gt;
&lt;li&gt;Motor accessibility is under-covered because it doesn't map cleanly onto structured tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat AI as the intern who catches the obvious stuff at 10x speed. It makes a specialist's time higher-leverage. It doesn't replace them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist I'd run on any AI-generated app
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Every interactive element has &lt;code&gt;accessibilityLabel&lt;/code&gt;, plus &lt;code&gt;accessibilityRole&lt;/code&gt; and &lt;code&gt;accessibilityHint&lt;/code&gt; where useful.&lt;/li&gt;
&lt;li&gt;Images are labeled or explicitly marked decorative with &lt;code&gt;accessible={false}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Contrast passes WCAG 2.2 AA (4.5:1 body, 3:1 large). Re-check after any palette tweak.&lt;/li&gt;
&lt;li&gt;Font scaling works. Test at 200%.&lt;/li&gt;
&lt;li&gt;Touch targets are 44x44 minimum. Generators over-index on clean small icons, so this is the most-violated rule in generated code.&lt;/li&gt;
&lt;li&gt;Forms have labels, not just placeholders.&lt;/li&gt;
&lt;li&gt;Video has captions.&lt;/li&gt;
&lt;li&gt;You personally turned on VoiceOver and TalkBack and completed the primary flow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Point 4 is worth a snippet, since it's the one people get wrong quietly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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;useWindowDimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PixelRatio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&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="s2"&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="c1"&gt;// Bad: fixed height locks text out of scaling&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&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;gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Continue&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Better: let the row grow with the user's font scale&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PixelRatio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFontScale&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;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&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;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;maxFontSizeMultiplier&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Continue&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&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;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;WCAG 3.0 moves toward outcome-based scoring instead of binary pass/fail, which is far better suited to LLM evaluation. EAA enforcement in Europe starts producing fines and case law in 2026, which changes the internal politics of a11y work fast. And expect compliance tooling that lives inside the AI development pipeline: reviewing PRs, gating deploys, producing an audit trail.&lt;/p&gt;

&lt;p&gt;The direction of travel is that accessibility becomes a property of how the app was built, not a coat of paint applied afterward.&lt;/p&gt;

&lt;p&gt;What's the a11y bug that has burned you hardest in a shipped app? Drop it in the comments. I'm especially interested in the ones no automated audit caught.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>a11y</category>
      <category>ai</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The State of Mobile AI in 2026 — From Code Generation to Full App Creation</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Thu, 06 Aug 2026 07:55:07 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/the-state-of-mobile-ai-in-2026-from-code-generation-to-full-app-creation-37ig</link>
      <guid>https://dev.to/rapidnative-ai/the-state-of-mobile-ai-in-2026-from-code-generation-to-full-app-creation-37ig</guid>
      <description>&lt;p&gt;In 2023, AI could complete a line of code. In 2026, AI can build the entire mobile app — screens, backend, auth, camera access, and a signed IPA ready for the App Store.&lt;/p&gt;

&lt;p&gt;That's not marketing copy, it's a specific technical shift, and it happened faster than most of us in mobile expected. This post walks through the four generations of AI coding tools, why mobile lagged the web by three years, and what specifically broke open in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four generations, briefly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gen 1 (2021-2023): Autocomplete.&lt;/strong&gt; Copilot. AI completes the next line. Human owns the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gen 2 (2023-2024): Single-file generation.&lt;/strong&gt; Cursor, Claude Code. AI generates entire files. Human is the integrator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gen 3 (2024-2025): Web full-stack.&lt;/strong&gt; Lovable, v0, Bolt. Prompt to deployed web app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gen 4 (2026): Mobile end-to-end.&lt;/strong&gt; Prompt to submission-ready native React Native + Expo app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every generation compressed the space between "I have an idea" and "users are running my app." Gen 4 is the first one where a non-engineer can complete that arc entirely for native mobile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why mobile lagged the web
&lt;/h2&gt;

&lt;p&gt;Web AI moved fast because the web is forgiving. Mobile is not. Each of the following cost the ecosystem roughly a year:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two platforms, not one.&lt;/strong&gt; React Native shares ~80% of code across iOS and Android. The other 20% is where the pain lives: permissions, keyboard behavior, safe areas, gestures, notches. A model has to reason about platform divergence explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native APIs are gated.&lt;/strong&gt; Camera on web is &lt;code&gt;getUserMedia&lt;/code&gt;. Camera on mobile is &lt;code&gt;expo-camera&lt;/code&gt; + permission strings in &lt;code&gt;app.json&lt;/code&gt; + runtime prompts + a config plugin. Any wrong step and it doesn't build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preview is genuinely hard.&lt;/strong&gt; Web preview is an iframe. Mobile preview needs Metro, a matching JS engine (Hermes), and streaming updates without a full rebuild.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The App Store is a wall.&lt;/strong&gt; Apple's Guideline 4.2 rejects "wrapped websites." Web-first tools with a "mobile export" button hit this and bounced. Gen 4 had to output real native code from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fullstack means fullstack.&lt;/strong&gt; Real mobile means auth that survives app suspend/resume, secure token storage in the OS keychain, offline sync, and a data layer that handles being genuinely offline. Gen 3 tools papered over most of this — they targeted the web, which rarely needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "full app creation" actually produces in 2026
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Screens&lt;/td&gt;
&lt;td&gt;React Native + navigation, gestures, safe areas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Redux / Zustand / Context, chosen by complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Provisioned Supabase project + schema + RLS + types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Keychain-backed session, deep links, sign-in flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;Migrations, seed data, offline-first where relevant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;Camera, GPS, push, biometrics with correct config plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview&lt;/td&gt;
&lt;td&gt;QR-code preview on real iOS and Android devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export&lt;/td&gt;
&lt;td&gt;Clean React Native + Expo source, runs anywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ship&lt;/td&gt;
&lt;td&gt;Signed IPA/AAB, submitted to App Store + Play&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row above is a place a Gen 3 "mobile export" tool fails. Every row is now Gen 4 table stakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four unlocks that made 2026 different
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Frontier models got good at multi-file reasoning.&lt;/strong&gt; The current generation of frontier models can hold an entire codebase in context and reason about change propagation. Prerequisite for full app gen.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Streaming output made iteration feel instant.&lt;/strong&gt; SSE + progressive component mounting turns "AI generation" from a batch job into a conversation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LLM-native tooling replaced ported IDEs.&lt;/strong&gt; Virtual filesystems the AI writes into, sandboxed runners that typecheck and lint before feedback loops back to the model, multi-step LLM pipelines. The model is the CPU; the surrounding system is the OS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Someone had to actually build the mobile-specific infrastructure.&lt;/strong&gt; This is the moat. Producing an Expo project that installs, builds, previews on a real device, submits to a store, and comes back with a valid receipt is a stack of ten problems requiring ten specialists.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Checklist: is a tool actually Generation 4?
&lt;/h2&gt;

&lt;p&gt;If you're evaluating tools in 2026, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the output run on a real phone via QR code, or just in a browser iframe?&lt;/li&gt;
&lt;li&gt;Does the tool generate backend + auth in the same prompt as the frontend?&lt;/li&gt;
&lt;li&gt;Can you export the code and run it locally without the vendor?&lt;/li&gt;
&lt;li&gt;Does the tool handle App Store submission or stop at "here's your code"?&lt;/li&gt;
&lt;li&gt;Does the tool support device features — camera, push, GPS, biometrics — out of the box?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most tools calling themselves "mobile AI app builders" in 2026 fail at least two. The ones that pass are the ones defining what AI mobile app development means for the rest of the decade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for developers
&lt;/h2&gt;

&lt;p&gt;Senior mobile engineers have not been displaced — the ceiling of what you can build solo has moved up, not down. The mechanical work (screen scaffolding, auth boilerplate, Expo config, navigation setup) collapses to a prompt. The architectural work (data modeling, performance profiling, complex state machines, offline sync semantics) is still where craftsmanship compounds.&lt;/p&gt;

&lt;p&gt;The developers who ship the most in 2026 are the ones who treat AI as a code CPU and spend their time on the parts that matter.&lt;/p&gt;




&lt;p&gt;Try it out: &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=mobile-ai-2026-the-shift-from-code-generation-to-full-app-creation" rel="noopener noreferrer"&gt;rapidnative.com&lt;/a&gt; — describe an app in plain English, get a real React Native + Expo app on your phone in minutes. Free tier is 20 credits, no credit card.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ai</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Complete Guide to Offline Storage in React Native (2026)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:28:13 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/the-complete-guide-to-offline-storage-in-react-native-2026-1n7j</link>
      <guid>https://dev.to/rapidnative-ai/the-complete-guide-to-offline-storage-in-react-native-2026-1n7j</guid>
      <description>&lt;p&gt;Most React Native apps treat the network as an always-on dependency, and it shows the moment a request stalls. Offline storage isn't one library — it's a stack: key-value stores for preferences, secure enclaves for tokens, structured DBs for domain data, and query caches for server state. This post walks through the whole stack and the offline-first pattern that ties it together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five categories of RN offline storage
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Key-value&lt;/strong&gt; — AsyncStorage (compat) or MMKV (default). MMKV is ~30x faster and supports sync reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure&lt;/strong&gt; — &lt;code&gt;expo-secure-store&lt;/code&gt; or &lt;code&gt;react-native-keychain&lt;/code&gt;. Backed by iOS Keychain / Android Keystore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured&lt;/strong&gt; — &lt;code&gt;expo-sqlite&lt;/code&gt;, &lt;code&gt;op-sqlite&lt;/code&gt; (JSI, synchronous), WatermelonDB (reactive + sync), Realm (Atlas Device Sync).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File system&lt;/strong&gt; — &lt;code&gt;expo-file-system&lt;/code&gt; for media. Never base64 blobs into KV storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-state cache&lt;/strong&gt; — TanStack Query + &lt;code&gt;persistQueryClient&lt;/code&gt;, backed by MMKV.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Perf&lt;/th&gt;
&lt;th&gt;Encryption&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AsyncStorage&lt;/td&gt;
&lt;td&gt;KV&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;td&gt;Community fork&lt;/td&gt;
&lt;td&gt;Legacy/compat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MMKV&lt;/td&gt;
&lt;td&gt;KV&lt;/td&gt;
&lt;td&gt;~30x&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;New default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expo-secure-store&lt;/td&gt;
&lt;td&gt;Secure KV&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;OS keychain&lt;/td&gt;
&lt;td&gt;Tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;op-sqlite&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;JSI&lt;/td&gt;
&lt;td&gt;SQLCipher&lt;/td&gt;
&lt;td&gt;Structured data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WatermelonDB&lt;/td&gt;
&lt;td&gt;Reactive DB&lt;/td&gt;
&lt;td&gt;Lazy&lt;/td&gt;
&lt;td&gt;SQLCipher&lt;/td&gt;
&lt;td&gt;Large offline lists + sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realm&lt;/td&gt;
&lt;td&gt;Object DB&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Atlas Device Sync&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The offline-first pattern
&lt;/h2&gt;

&lt;p&gt;Four moving parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local-first reads&lt;/strong&gt; — UI always reads from local store. Network populates store; store renders UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic writes&lt;/strong&gt; — UI updates immediately, before the server acks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutation queue&lt;/strong&gt; — Durable list (SQLite or MMKV) of pending server-side changes with client IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync engine&lt;/strong&gt; — Background worker drains the queue with exponential backoff, reconciles conflicts.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markMessageAsRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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;db&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="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;readAt&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="na"&gt;pendingSync&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mutationQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message.markRead&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;readAt&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="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;syncEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wake&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;h2&gt;
  
  
  Conflict resolution
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LWW&lt;/strong&gt; — timestamps, newest wins. Fine for most consumer apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-authoritative&lt;/strong&gt; — reject stale writes with a version mismatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CRDTs&lt;/strong&gt; — Yjs, Automerge. Overkill unless you're building a collaborative editor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision cheat sheet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prefs / JWT? → MMKV + expo-secure-store&lt;/li&gt;
&lt;li&gt;Instant-open cached lists? → TanStack Query + &lt;code&gt;persistQueryClient&lt;/code&gt; on MMKV&lt;/li&gt;
&lt;li&gt;Thousands of offline records? → op-sqlite (or WatermelonDB for reactive)&lt;/li&gt;
&lt;li&gt;Cross-device sync? → WatermelonDB or Realm&lt;/li&gt;
&lt;li&gt;Media? → expo-file-system + metadata in SQLite&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pitfalls to skip
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Blocking JS thread with async AsyncStorage at startup&lt;/li&gt;
&lt;li&gt;Blobs in KV stores&lt;/li&gt;
&lt;li&gt;No cache-wipe on logout&lt;/li&gt;
&lt;li&gt;Infinite retry loops in the mutation queue&lt;/li&gt;
&lt;li&gt;Persisting Redux state without a schema version&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Offline-first isn't a defensive posture — it's a performance strategy. Reading from local storage beats a round-trip every time, and a good sync engine hides the network from your users. Pick each layer deliberately and this becomes a solved problem, not a recurring one.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=complete-guide-offline-storage-react-native" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, an AI mobile app builder that generates production-ready React Native code you can extend with any of these libraries. Would love feedback from anyone using MMKV + TanStack Query in production.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why 77% of Your App's Users Are Gone in 3 Days (and the real fix isn't another tactic)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:19:50 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/why-77-of-your-apps-users-are-gone-in-3-days-and-the-real-fix-isnt-another-tactic-201n</link>
      <guid>https://dev.to/rapidnative-ai/why-77-of-your-apps-users-are-gone-in-3-days-and-the-real-fix-isnt-another-tactic-201n</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;77% of users who install your app stop using it within 72 hours. Most of that churn happens &lt;em&gt;inside the first session&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;2026 cross-industry medians: Day-1 = 26%, Day-7 = 11%, Day-30 = 5.4%. Subscription apps retain ~2.5x better at D30.&lt;/li&gt;
&lt;li&gt;The five real churn causes: no "aha moment", ad/product mismatch, no external trigger, crashes/slow loads, and one-and-done intent (which is fine).&lt;/li&gt;
&lt;li&gt;The uncomfortable part: you probably already know the tactics. The real bottleneck is that your experiment cycle takes 3 weeks while your cohort churns in 3 days.&lt;/li&gt;
&lt;li&gt;Fix the cycle time, not the tactics list.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;77% of the users who install your app stop using it within 72 hours.&lt;/strong&gt; Not a month. Not a week. Three days.&lt;/p&gt;

&lt;p&gt;That stat traces back to Andrew Chen's analysis at a16z, and every benchmark report since (Adjust, AppsFlyer, Sensor Tower) lands in the same neighborhood. The 2026 numbers, by vertical:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vertical&lt;/th&gt;
&lt;th&gt;Day 1&lt;/th&gt;
&lt;th&gt;Day 7&lt;/th&gt;
&lt;th&gt;Day 30&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Social &amp;amp; Communication&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gaming (Casual)&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finance &amp;amp; Fintech&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health &amp;amp; Fitness&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-commerce &amp;amp; Retail&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Productivity&lt;/td&gt;
&lt;td&gt;29%&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscription Apps (all)&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-category median&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things worth noticing: subscription apps retain 2.5x better at Day 30 (paywalls are a commitment device), and fintech beats casual gaming 3x at D30 (utility beats entertainment on retention, almost always).&lt;/p&gt;

&lt;h2&gt;
  
  
  Make sure you're measuring the same thing as the benchmarks
&lt;/h2&gt;

&lt;p&gt;Retention has three variants, and teams routinely fool themselves by measuring one internally and comparing against another:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Classic&lt;/strong&gt; — user came back on &lt;em&gt;exactly&lt;/em&gt; day N. Punitive, but it's what the benchmarks above use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling&lt;/strong&gt; — user came back on day N &lt;em&gt;or later&lt;/em&gt;. More forgiving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bracket&lt;/strong&gt; — user came back anywhere in days N–M. Useful for weekly cohorts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to sanity-check your own numbers, classic Day-N retention is one query away:&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="c1"&gt;-- Classic Day-N retention per install cohort&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;installs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&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="n"&gt;install_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cohort_day&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;app_installs&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;user_id&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="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active_day&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;app_events&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cohort_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&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;AS&lt;/span&gt; &lt;span class="n"&gt;cohort_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cohort_day&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&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;AS&lt;/span&gt; &lt;span class="n"&gt;d1_retention&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cohort_day&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 day'&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&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;AS&lt;/span&gt; &lt;span class="n"&gt;d7_retention&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;installs&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="n"&gt;a&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;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Segment this by acquisition channel before you conclude anything. A cohort with 15% D7 overall might be 30% from organic and 4% from one bad Facebook creative. That's not a retention problem — that's an ad-relevance problem wearing a retention costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the 77% actually dies: the first session
&lt;/h2&gt;

&lt;p&gt;The churn isn't spread evenly across 72 hours. Roughly 60–70% of it happens &lt;strong&gt;inside the first session&lt;/strong&gt;. Users don't drift away over three opens — they open once, hit friction, close, never return.&lt;/p&gt;

&lt;p&gt;The consistent offenders, with rough measured costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signup wall before showing value: &lt;strong&gt;–30-40%&lt;/strong&gt; of installers on that screen alone&lt;/li&gt;
&lt;li&gt;Phone verification: another &lt;strong&gt;–15-20%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Push permission prompt on Day 0 (before any value): &lt;strong&gt;~4x lower&lt;/strong&gt; opt-in vs. asking after value&lt;/li&gt;
&lt;li&gt;More than five onboarding screens: &lt;strong&gt;~20% more&lt;/strong&gt; churn vs. three or fewer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cheapest fix in mobile is deferring everything. Guest mode instead of a signup wall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instead of gating the app behind auth, persist a local guest identity&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&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-async-storage/async-storage&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;v4&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;uuid&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;uuid&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;getOrCreateGuestId&lt;/span&gt;&lt;span class="p"&gt;()&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;id&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`guest_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;uuid&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&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;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Later, when the user actually signs up, merge the guest state&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;upgradeGuestToUser&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guestId&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&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;guestId&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mergeAccounts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;guestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same logic for push permissions — ask &lt;em&gt;after&lt;/em&gt; the user has experienced value, not on launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't request on app open. Trigger contextually, e.g. after first "aha" action&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;requestPushAfterValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&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;alreadyAsked&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&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_prompted&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;alreadyAsked&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first_workout_completed&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="c1"&gt;// Pre-permission soft prompt first — protects your one real OS prompt&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wants&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;showSoftPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Want a reminder before your streak breaks?&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;wants&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="nf"&gt;requestNotificationPermission&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&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_prompted&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;true&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The five real reasons users churn
&lt;/h2&gt;

&lt;p&gt;Skip the twenty-tactic listicles. Churn comes from a small set of root causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;They never hit the "aha moment."&lt;/strong&gt; Duolingo: first lesson done. Instagram: first personalized feed. Fintech: first balance after bank link. Users who reach it retain at 3-5x the rate of users who don't. Your entire first session should be a speedrun to that moment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ad promised something the app isn't.&lt;/strong&gt; Invisible in aggregate, obvious when you segment by channel (see the SQL above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No external trigger.&lt;/strong&gt; Apps that survive past D7 almost always reach out — a push tied to a &lt;em&gt;real&lt;/em&gt; event (someone messaged you, your streak's about to break), not "hi, we miss you." Apps that rely on being remembered lose to apps that reach out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crashes and slow screens.&lt;/strong&gt; Users don't file bug reports; they uninstall. And crashes disproportionately hit your heaviest users, because they hit more code paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They got what they came for.&lt;/strong&gt; One-time intent (check a flight, convert a currency once) isn't a retention failure. Segment churn by intent before you panic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The part nobody writes: your bottleneck is cycle time, not tactics
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth: you already knew most of the above. Everyone's read the same posts. Everyone has the Notion doc of retention experiments. So why is the median D30 still 5.4%?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because most teams can't ship a retention experiment in under three weeks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Week 1: spec + Figma + design review. Week 2: implementation + new tracking event + QA. Week 3: ship to 20%, wait for the cohort to season, analyze. By the time you know whether the shorter signup form moved D1, the cohort you wanted to save has been gone for two and a half weeks. You're permanently fighting the last war — with eighteen more experiments queued behind this one.&lt;/p&gt;

&lt;p&gt;Duolingo runs hundreds of A/B tests a year. Netflix ships micro-experiments continuously. The gap isn't ideas — it's that they ship an experiment in a day and you ship one a month.&lt;/p&gt;

&lt;p&gt;This is where tooling actually changed the math. Building in an AI-native stack like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=mobile-app-retention-why-users-churn" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; means the guest-mode experiment above is a prompt — "make signup optional, add continue-as-guest, persist state locally" — with a working build on a real device via QR code in under a minute, instead of a sprint. When iteration cost collapses that far, the strategy flips: you stop hoarding experiments and start running them in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  A weekly experiment cadence that actually compounds
&lt;/h2&gt;

&lt;p&gt;If your iteration cost is low enough, this rhythm works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Monday AM&lt;/strong&gt; — pull last week's cohorts, segmented by channel and onboarding variant. Find the biggest first-session drop-off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monday PM&lt;/strong&gt; — one hypothesis, one line: "changing X to Y improves D1 by Z% for cohort A."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuesday&lt;/strong&gt; — build the variant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wednesday&lt;/strong&gt; — ship to 50% of new installs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next Monday&lt;/strong&gt; — cohort's seasoned. Ship the winner or kill it. Next experiment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's ~5 experiments a month, 60 a year. Even at a 1-in-5 hit rate you're compounding at a pace nobody on a three-week cycle can touch.&lt;/p&gt;

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

&lt;p&gt;Retention in 2026 isn't a knowledge problem. The benchmarks are public and every founder can recite why users churn. The teams that win are the ones shipping retention experiments &lt;strong&gt;faster than their cohorts churn&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Stop asking "which tactic should we prioritize?" Start asking "what's the smallest experiment we can put in front of real users this week?"&lt;/p&gt;

&lt;p&gt;What's your team's actual experiment cycle time — days or weeks? And what's the one onboarding change you've been meaning to test forever? Drop it in the comments, I'm curious what's stuck in everyone's backlog. 👇&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>startup</category>
      <category>analytics</category>
    </item>
    <item>
      <title>How to Build a Grocery Delivery App with React Native + Expo (2026)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Fri, 17 Jul 2026 07:00:25 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-to-build-a-grocery-delivery-app-with-react-native-expo-2026-f2d</link>
      <guid>https://dev.to/rapidnative-ai/how-to-build-a-grocery-delivery-app-with-react-native-expo-2026-f2d</guid>
      <description>&lt;h1&gt;
  
  
  How to Build a Grocery Delivery App with React Native + Expo (2026)
&lt;/h1&gt;

&lt;p&gt;If you've ever tried to build an Instacart-style app from scratch, you know the pain: three apps to ship (customer, driver, admin), a real-time tracking layer, payments, dispatch, and the ever-fun "handle a driver going offline mid-delivery."&lt;/p&gt;

&lt;p&gt;Here's a practical stack and a build sequence that keeps the moving parts to a minimum.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-app reality
&lt;/h2&gt;

&lt;p&gt;A grocery delivery product is three apps sharing one backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Customer app&lt;/strong&gt; — browse, cart, checkout, track&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Driver app&lt;/strong&gt; — accept orders, navigate, capture proof of delivery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin dashboard&lt;/strong&gt; — inventory, dispatch, disputes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combine them into one and you'll spend the next quarter untangling permission logic. Keep them separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack that converges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mobile:&lt;/strong&gt; React Native + Expo (one codebase, iOS + Android, OTA updates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Supabase (Postgres, auth, realtime, RLS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments:&lt;/strong&gt; Stripe Payment Intents + Stripe Connect for driver payouts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maps:&lt;/strong&gt; Google Maps SDK + Directions API + Distance Matrix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push:&lt;/strong&gt; Expo Push → Twilio SMS for critical alerts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realtime:&lt;/strong&gt; Supabase realtime channels (no custom WebSocket server)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data model — the parts that matter
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The critical detail: capture price at purchase time&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price_at_purchase&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- never join back to products.price_cents&lt;/span&gt;
  &lt;span class="n"&gt;substitution_preference&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;substitution_preference&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;'allow'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'refund'&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="c1"&gt;-- Append-only audit log = free source of truth for status timeline&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;order_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&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 invariants worth burning into your brain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Never resolve &lt;code&gt;order_items.price&lt;/code&gt; by joining back to &lt;code&gt;products&lt;/code&gt;. Prices change; receipts must not.&lt;/li&gt;
&lt;li&gt;Never trust the client's clock for &lt;code&gt;delivered_at&lt;/code&gt;. Use &lt;code&gt;now()&lt;/code&gt; in Postgres. You will thank yourself the first time a dispute goes to your inbox.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Real-time tracking without pain
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Driver side:&lt;/strong&gt; capture location every 5–15 seconds, back off when stationary.&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Location&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-location&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;supabase&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;./supabase&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="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startLocationUpdatesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;driver-location&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;accuracy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Accuracy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Balanced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;distanceInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;foregroundService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;notificationTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Delivery in progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;notificationBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sharing your location with the customer&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Customer side:&lt;/strong&gt; subscribe to the driver row.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`driver:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;driverId&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres_changes&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drivers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`id=eq.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;driverId&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="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;driver&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;setDriverPosition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_lng&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="nf"&gt;subscribe&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&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="nx"&gt;driverId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refresh ETA every 30 seconds via Distance Matrix — not on every location ping, or you'll blow through your rate limit before lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payments — the substitution problem
&lt;/h2&gt;

&lt;p&gt;Stripe Payment Intents flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First order: create a Stripe Customer, attach a &lt;code&gt;SetupIntent&lt;/code&gt; to save the card off-session.&lt;/li&gt;
&lt;li&gt;Checkout: create a &lt;code&gt;PaymentIntent&lt;/code&gt; with &lt;code&gt;confirm: false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only confirm after the order is packed.&lt;/strong&gt; Never authorize before you know items are available.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The substitution policy question you must answer on day one: pre-authorize subtotal × 1.15 to cover swaps (Instacart's approach) or refund the diff (better UX, more work). Either works; pick one and communicate it at checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  The screens
&lt;/h2&gt;

&lt;p&gt;Nine screens ship a customer app: Onboarding + Address, Home, Category Listing, Product Detail, Cart, Checkout, Order Tracking, Order History, Profile.&lt;/p&gt;

&lt;p&gt;Writing these by hand from scratch is a 6–8 week grind. If you want to skip the JSX, prompts to an AI mobile app builder generate them in an afternoon and export clean React Native you can extend by hand. I've been using &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-grocery-delivery-app" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; for this and shipping the boring UI in a day so I can spend the rest of the sprint on dispatch logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The subtle bugs to avoid
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client-side timestamps.&lt;/strong&gt; Always &lt;code&gt;now()&lt;/code&gt; in the DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting the driver's app to update order status.&lt;/strong&gt; Use Postgres triggers on &lt;code&gt;order_events&lt;/code&gt; to update &lt;code&gt;orders.status&lt;/code&gt; atomically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading inventory from &lt;code&gt;products.inventory_count&lt;/code&gt; at checkout.&lt;/strong&gt; Race conditions. Use a serializable transaction or &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendering the map on every location ping.&lt;/strong&gt; Debounce marker updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap
&lt;/h2&gt;

&lt;p&gt;React Native + Expo + Supabase + Stripe + Google Maps is boring, well-documented, and consolidated for a reason. The wins in this space come from operational density and freshness, not from the tech stack. Ship the MVP, get it in front of ten customers, iterate.&lt;/p&gt;

&lt;p&gt;If you want the whole architecture in one place (data model, screen list, prompt sequence), the &lt;a href="https://www.rapidnative.com/blogs/how-to-build-a-grocery-delivery-app?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-grocery-delivery-app" rel="noopener noreferrer"&gt;full walkthrough is on our blog&lt;/a&gt;.&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;Best time: Tuesday–Thursday, 8–10 AM ET&lt;/li&gt;
&lt;li&gt;Community: Also cross-post to &lt;code&gt;#reactnative&lt;/code&gt; and &lt;code&gt;#tutorial&lt;/code&gt; Dev.to tags&lt;/li&gt;
&lt;li&gt;Engagement: Reply to every comment in the first 24 hours; Dev.to's algorithm weights early engagement&lt;/li&gt;
&lt;li&gt;Cover image: Use the Unsplash URL in frontmatter or upload a screenshot of the finished app&lt;/li&gt;
&lt;li&gt;After posting: Share in &lt;code&gt;r/reactnative&lt;/code&gt; and &lt;code&gt;r/expo&lt;/code&gt; with the Dev.to link (Reddit tolerates Dev.to links better than direct-to-marketing links)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>tutorial</category>
      <category>mobile</category>
    </item>
    <item>
      <title>7 Lessons Shipping React Native Templates</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Fri, 17 Jul 2026 06:51:33 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/7-lessons-shipping-react-native-templates-1b21</link>
      <guid>https://dev.to/rapidnative-ai/7-lessons-shipping-react-native-templates-1b21</guid>
      <description>&lt;p&gt;Seven React Native templates in one year, all on Expo SDK 54 + Supabase + NativeWind. This is the technical retrospective — seven lessons with code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The templates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Weather ($49, 5 screens)&lt;/li&gt;
&lt;li&gt;Fitness ($89, 12+ screens)&lt;/li&gt;
&lt;li&gt;LearnHub ($89, 9 screens)&lt;/li&gt;
&lt;li&gt;RideNow ($99, 8 screens)&lt;/li&gt;
&lt;li&gt;Food Delivery ($79, 5 screens)&lt;/li&gt;
&lt;li&gt;AI Voice Notes ($79, 16+ screens) — Whisper&lt;/li&gt;
&lt;li&gt;Chat with PDF ($79, 16+ screens) — RAG&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All share one substrate. That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1 — Build the substrate before the first template
&lt;/h2&gt;

&lt;p&gt;Weather was our first template. Five screens, no auth, no DB. It didn't need Supabase. We built it against Supabase anyway.&lt;/p&gt;

&lt;p&gt;Rationale: template two would need auth, RLS, edge functions, and a marketing page contract. Building that infra for a template that didn't need it looked wasteful for two weeks. It paid for itself by template three.&lt;/p&gt;

&lt;p&gt;If you want to build a template business, spend the first sprint on infra you won't use. It's the least glamorous advice I have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2 — API keys never live in the RN bundle
&lt;/h2&gt;

&lt;p&gt;Both AI templates route through Supabase Edge Functions. The customer's OpenAI or Anthropic key sits at the edge. Client calls the function; function holds the key; RLS enforces per-user rate limits.&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;// Bad: key in the client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&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;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EXPO_PUBLIC_OPENAI_KEY&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Good: client calls edge function&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;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/functions/v1/ai-chat`&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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&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="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="nx"&gt;messages&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;EXPO_PUBLIC_*&lt;/code&gt; bakes into the bundle. Anyone can &lt;code&gt;strings&lt;/code&gt; your &lt;code&gt;.ipa&lt;/code&gt; and pull it out. This is not theoretical — it happens weekly to indie apps.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 3 — One data contract, all templates
&lt;/h2&gt;

&lt;p&gt;Every template's marketing page on our site renders from the same TypeScript interface:&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;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ProductData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;tagline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;screenshots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Screenshot&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;pricing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PricingTier&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;testimonials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Testimonial&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;faqs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FAQ&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;techStack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TechItem&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;Sixty-three lines. Every template's config satisfies it. Every marketing component consumes it. Zero forks.&lt;/p&gt;

&lt;p&gt;Before this contract, we had five copies of the pricing card. A fix on one wasn't a fix on all. After the contract: one component, one bug fix, seven templates updated.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 4 — Ship CLAUDE.md, AGENTS.md, .cursorrules
&lt;/h2&gt;

&lt;p&gt;Every template ships with agent-readiness files. Short, opinionated, points to &lt;code&gt;lib/data/types.ts&lt;/code&gt; and lists invariants.&lt;/p&gt;

&lt;p&gt;Result: support tickets about "the agent broke something in your template" dropped noticeably. Not because agents got smarter — because they had a map.&lt;/p&gt;

&lt;p&gt;Structure that works:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# CLAUDE.md&lt;/span&gt;
&lt;span class="gu"&gt;## Invariants&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Do not touch &lt;span class="sb"&gt;`lib/data/types.ts`&lt;/span&gt; — it's the shared contract
&lt;span class="p"&gt;-&lt;/span&gt; Every new screen goes in &lt;span class="sb"&gt;`app/(tabs)/`&lt;/span&gt;, wired via Expo Router
&lt;span class="p"&gt;-&lt;/span&gt; Edge functions in &lt;span class="sb"&gt;`supabase/functions/`&lt;/span&gt;, never in the client

&lt;span class="gu"&gt;## File map&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; app/apps/config/&lt;span class="err"&gt;*&lt;/span&gt;.config.ts — template configs
&lt;span class="p"&gt;-&lt;/span&gt; app/apps/components/ — shared marketing UI
&lt;span class="p"&gt;-&lt;/span&gt; app/apps/lib/data/ — data layer

&lt;span class="gu"&gt;## When adding a feature&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Update the config type first
&lt;span class="p"&gt;2.&lt;/span&gt; Then update the config value
&lt;span class="p"&gt;3.&lt;/span&gt; Then update the UI that reads it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Lesson 5 — Keep the entry price low
&lt;/h2&gt;

&lt;p&gt;Templates range $49–$99. Temptation: raise everything to $99. Support-per-dollar is worse at $49.&lt;/p&gt;

&lt;p&gt;Reality: $49 buyers come back for $79–$99 templates. The cheap template is a paid trial. Kill it and the funnel collapses.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 6 — Docs convert, not screenshots
&lt;/h2&gt;

&lt;p&gt;Analytics: converting buyers spend more time in &lt;code&gt;/docs&lt;/code&gt; than on the marketing page. Specifically the "how do I swap the AI provider" sections.&lt;/p&gt;

&lt;p&gt;Writing docs first also forces you to design the escape hatch before the feature. If you can't write the "how do I swap this" section, the feature isn't done.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 7 — Stack review
&lt;/h2&gt;

&lt;p&gt;Would pick again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expo SDK 54, RN 0.81 — new arch stable&lt;/li&gt;
&lt;li&gt;NativeWind v4 — kills styling drift across templates&lt;/li&gt;
&lt;li&gt;Supabase — auth, Postgres, edge, storage, one bill&lt;/li&gt;
&lt;li&gt;TanStack Query v5 — replaced Redux everywhere&lt;/li&gt;
&lt;li&gt;Expo Router v6 — file-based routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Would skip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zustand for state that TanStack Query handles. Two templates had Zustand stores that were purely query-cache reinventions. Removing them cost more than not adding them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  For year two
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Config file exists before any RN code does&lt;/li&gt;
&lt;li&gt;Docs written before the feature&lt;/li&gt;
&lt;li&gt;CLAUDE.md written during design&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.applighter.com/blog/streaming-ai-responses-to-react-native-the-complete-guide" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdnimg.co%2F62e8b74c-52a4-4089-b3d3-fd82483028c1%2Fe462e274-b3ea-483b-b3fe-039834185114%2Fstreaming-ai-responses-to-react-native-the-complete-guide-abstract-illustration.jpg" height="449" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.applighter.com/blog/streaming-ai-responses-to-react-native-the-complete-guide" rel="noopener noreferrer" class="c-link"&gt;
            Streaming AI Responses to React Native: The Complete Guide
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Streaming AI Responses to React Native: The Complete Guide. Build fluid, conversational UIs with SSE, edge functions, &amp;amp; robust client-side rendering.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.applighter.com%2Ficon.svg%3Ficon.f2cf9ab1.svg" width="32" height="32"&gt;
          applighter.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.applighter.com/blog/react-native-supabase-template-auth-rls-storage" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1517694712202-14dd9538aa97%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" height="1067" class="m-0" width="1600"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.applighter.com/blog/react-native-supabase-template-auth-rls-storage" rel="noopener noreferrer" class="c-link"&gt;
            React Native Supabase Template: Auth, RLS, Storage
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            How Applighter ships React Native templates with Supabase auth, RLS policies, and Storage already wired — clone, run migrations, build features.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.applighter.com%2Ficon.svg%3Ficon.f2cf9ab1.svg" width="32" height="32"&gt;
          applighter.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>applighter</category>
    </item>
    <item>
      <title>Wiring Designers Into Your EAS Preview Loop</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 08 Jul 2026 08:15:28 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/wiring-designers-into-your-eas-preview-loop-1hil</link>
      <guid>https://dev.to/rapidnative-ai/wiring-designers-into-your-eas-preview-loop-1hil</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Figma reviews miss everything that's a function of the real device — touch targets, scroll momentum, keyboard behavior, font substitution, dark-mode transitions.&lt;/li&gt;
&lt;li&gt;Wrap &lt;code&gt;eas build --profile designer-preview&lt;/code&gt; in a Slack slash command so designers trigger their own preview builds without engineering.&lt;/li&gt;
&lt;li&gt;Use an EAS Update &lt;code&gt;designer-preview&lt;/code&gt; channel to push JS-only changes to an already-installed build — no reinstall, no rebuild.&lt;/li&gt;
&lt;li&gt;Remove three friction points first: the install step, the "which build is this?" question, and the "is this update live yet?" question.&lt;/li&gt;
&lt;li&gt;Measure &lt;strong&gt;review-loop time&lt;/strong&gt;, not build time.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Our mobile design reviews used to happen in Figma. Then a build would ship, the designer would see it on their phone a day later, and we'd realize the touch target was 8dp too small or the sheet animation felt wrong on Android. The fix was obvious in hindsight: put designers directly into the EAS preview loop so they see the change on a real device before signing off.&lt;/p&gt;

&lt;p&gt;Here's how we wired it up and what actually moved the needle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why design reviews on Figma miss real device behavior
&lt;/h2&gt;

&lt;p&gt;Figma reviews catch layout, spacing, and color drift. They miss everything that's a function of the device:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Touch target size and reachability on the specific phone the designer holds every day&lt;/li&gt;
&lt;li&gt;Momentum and rubber-banding on real ScrollViews&lt;/li&gt;
&lt;li&gt;Keyboard behavior (especially on iOS Safe Area vs Android IME resize)&lt;/li&gt;
&lt;li&gt;Actual font rendering — system font substitution differs by OS version&lt;/li&gt;
&lt;li&gt;Dark-mode transitions in a real app vs mocked screens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these has bitten us. And every one of them is invisible until a designer scrolls, taps, and feels the app on their own device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-Slack-command pattern that unblocks designers
&lt;/h2&gt;

&lt;p&gt;The unlock was making it possible for a designer to trigger their own preview build without asking engineering. We wrapped &lt;code&gt;eas build --profile designer-preview --platform ios&lt;/code&gt; in a Slack slash command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/preview branch feat/new-onboarding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command dispatches a GitHub Action that runs &lt;code&gt;eas build&lt;/code&gt; against the named branch, then posts the QR code + install link back into the Slack thread when it completes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/designer-preview.yml (excerpt)&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;designer-preview&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;inputs.branch&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;expo/expo-github-action@v8&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;expo-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;latest&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;secrets.EXPO_TOKEN&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eas build --profile designer-preview --platform ios --non-interactive --no-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical detail: &lt;code&gt;--profile designer-preview&lt;/code&gt; in &lt;code&gt;eas.json&lt;/code&gt; should be internal distribution, not TestFlight — TestFlight has an approval delay that breaks the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  EAS Update channels for designer-only preview builds
&lt;/h2&gt;

&lt;p&gt;Once you have preview builds working, the second unlock is EAS Update for JS-only changes. Configure a &lt;code&gt;designer-preview&lt;/code&gt; channel in &lt;code&gt;eas.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"designer-preview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"designer-preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"distribution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"EXPO_PUBLIC_ENV"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"designer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any JS/TSX change (which is 80% of what designers care about — spacing, colors, typography, animation curves) can now be pushed to their existing installed build with &lt;code&gt;eas update --channel designer-preview --branch feat/spacing-fix&lt;/code&gt;. No new install, no new build. The designer opens the app, pulls to refresh, sees the change.&lt;/p&gt;

&lt;p&gt;This compressed our design-iteration loop from ~1 build/day to ~4-6 JS pushes/hour when we're in an active review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three friction points to remove before you invite designers
&lt;/h2&gt;

&lt;p&gt;Before rolling this out, remove these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The install step.&lt;/strong&gt; Designers should not have to install Expo Dev Client from scratch every time. Ship one Dev Client build a week, keep it stable, and use EAS Update on top of it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "which build is this?" question.&lt;/strong&gt; Add a corner label to your app in the designer env showing the branch name + short commit hash. A &lt;code&gt;useConstants()&lt;/code&gt; hook + a semi-transparent &lt;code&gt;Text&lt;/code&gt; component is enough:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;Constants&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-constants&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt; &lt;span class="p"&gt;}&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="c1"&gt;// In your root layout:&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;watermark&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;manifest&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;branch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; · &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;manifest&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;commit&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;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The "is this update live yet?" question.&lt;/strong&gt; Show the last-updated timestamp somewhere the designer can find (a debug menu, a settings screen). Nothing kills trust in the loop like reviewing an old version.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion: measure the review-loop time, not build time
&lt;/h2&gt;

&lt;p&gt;After wiring this up, our design-to-shipped-mobile-build time dropped roughly 40%. But the metric that actually mattered wasn't build time — it was &lt;strong&gt;review-loop time&lt;/strong&gt;: how long between "designer flags a change" and "engineer knows if the fix is right."&lt;/p&gt;

&lt;p&gt;Measure that instead of build time and you'll instrument the right optimizations. Faster builds are useful; a designer who can review on-device without waiting for engineering is transformative.&lt;/p&gt;




&lt;p&gt;Are you running designers through your EAS loop, or still bouncing builds over Slack manually? Drop a comment with how your review loop works — especially if you've found a cleaner way to handle the "which build is this?" problem.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>designops</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
