<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[SpiroKit's Blog - React Native &amp; Figma UI Library - Build industry-level mobile apps in no time]]></title><description><![CDATA[SpiroKit is a React Native &amp; Figma UI Kit that contains a collection of carefully crafted components and design principles to transform an idea into a product.]]></description><link>https://blog.spirokit.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1666215624521/2KbaicY_D.png</url><title>SpiroKit&apos;s Blog - React Native &amp;amp; Figma UI Library - Build industry-level mobile apps in no time</title><link>https://blog.spirokit.com</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 21 May 2026 16:47:00 GMT</lastBuildDate><atom:link href="https://blog.spirokit.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Collecting payments with React Native & RevenueCat (Part 4): Building a Paywall screen]]></title><description><![CDATA[💡
This is the third part of the "Collecting payments" series. Before proceeding, make sure to read Part 1, Part 2 & Part 3

We've finally reached the last part of the series! With all the tedious aspects taken care of, we just need to connect everyt...]]></description><link>https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-4</link><guid isPermaLink="true">https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-4</guid><category><![CDATA[React Native]]></category><category><![CDATA[RevenueCat]]></category><category><![CDATA[Expo]]></category><category><![CDATA[payment]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Thu, 22 Feb 2024 23:44:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1708641739502/b0204fd5-5135-4ae9-a781-e0fc3482ee9e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><div data-node-type="callout"><p></p>
<p></p><div data-node-type="callout-emoji">💡</div><p></p>
<p></p><div data-node-type="callout-text">This is the third part of the "Collecting payments" series. Before proceeding, make sure to read <a target="_blank" href="https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-1">Part 1</a>, <a target="_blank" href="https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-2">Part 2</a> &amp; <a target="_blank" href="https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-3">Part 3</a></div>
</div>
We've finally reached the last part of the series! With all the tedious aspects taken care of, we just need to connect everything within the React Native app.<p></p>
<p>We'll add a React Provider wrapping the entire app, granting us easy access to various actions and information, such as purchasing a subscription, obtaining details about our offerings, and more!</p>
<hr />
<h1 id="heading-spirokit-for-saas">SpiroKit for SaaS</h1>
<p>In case you want to save weeks of painful work &amp; research on your next React Native app, checkout SpiroKit for SaaS. It's a starter template that comes with Purchases, Push Notifications, Authentication, Analytics, Error Reporting and more. It also includes a Notion template to guide you through the entire process. If you get a licence, you won't need to follow the rest of this guide ;)</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://spirokit.com">https://spirokit.com</a></div>
<p> </p>
<hr />
<h1 id="heading-building-a-development-client">Building a development client</h1>
<p>Before we can begin working on the app, we need to run it locally to test our changes. Since <code>react-native-purchases</code> is a native package, we require a development build for our app. Execute the following command to obtain a dev-client with EAS.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Replace platform based on your needs</span>
eas build --platform [ios|android] --profile development
</code></pre>
<p>Once the development build is ready, you'll need to install it on an actual device, as the RevenueCat package cannot be tested on a simulator.</p>
<h1 id="heading-adding-a-purchases-provider-to-your-app">Adding a Purchases provider to your app</h1>
<p>We could drop all the necessary logic to retrieve our offerings information directly into a component. However, I believe it would be even better to have a provider with all this logic encapsulated, wrapping the entire app, so you can easily access the information from any component. Let's begin by creating a few files:</p>
<pre><code class="lang-bash">mkdir context
touch context/PurchasesProvider.tsx
touch context/PurchasesContext.tsx
</code></pre>
<p>Copy &amp; paste the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// context/PurchasesContext.tsx</span>
<span class="hljs-keyword">import</span> { createContext } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> {
  CustomerInfo,
  MakePurchaseResult,
  PurchasesOffering,
  PurchasesPackage,
  PurchasesStoreTransaction,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-purchases"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> PurchasesContextProps = {
  currentOffering: PurchasesOffering | <span class="hljs-literal">null</span>;
  purchasePackage: <span class="hljs-function">(<span class="hljs-params">
    packageToPurchase: PurchasesPackage
  </span>) =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;MakePurchaseResult&gt;;
  customerInfo?: CustomerInfo;
  isSubscribed: <span class="hljs-built_in">boolean</span>;
  getNonSubscriptionPurchase: <span class="hljs-function">(<span class="hljs-params">
    identifier: <span class="hljs-built_in">string</span>
  </span>) =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;PurchasesStoreTransaction | <span class="hljs-literal">null</span> | <span class="hljs-literal">undefined</span>&gt;;
};

<span class="hljs-keyword">const</span> PurchasesContext = createContext&lt;PurchasesContextProps | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> PurchasesContext;
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// context/PurchasesProvider.tsx</span>
<span class="hljs-keyword">import</span> React, { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> PurchasesContext <span class="hljs-keyword">from</span> <span class="hljs-string">"./PurchasesContext"</span>;
<span class="hljs-keyword">import</span> Purchases, {
  CustomerInfo,
  LOG_LEVEL,
  PurchasesOffering,
  PurchasesPackage,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-purchases"</span>;
<span class="hljs-keyword">import</span> { Platform } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">type</span> PurchasesProviderProps = {
  children: JSX.Element | JSX.Element[];
};

<span class="hljs-keyword">const</span> androidApiKey = process.env.EXPO_PUBLIC_REVENUECAT_API_KEY_ANDROID!;
<span class="hljs-keyword">const</span> iosApiKey = process.env.EXPO_PUBLIC_REVENUECAT_API_KEY_IOS!;

<span class="hljs-keyword">const</span> PurchasesProvider: React.FC&lt;PurchasesProviderProps&gt; = <span class="hljs-function">(<span class="hljs-params">{ children }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> [initialized, setInitialized] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [offering, setOffering] = useState&lt;PurchasesOffering | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [isSubscribed, setIsSubscribed] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [customerInfo, setCustomerInfo] = useState&lt;CustomerInfo&gt;();

  <span class="hljs-keyword">const</span> init = <span class="hljs-keyword">async</span> () =&gt; {
    Purchases.configure({
      apiKey: Platform.OS === <span class="hljs-string">"android"</span> ? androidApiKey : iosApiKey,
    });

    <span class="hljs-keyword">if</span> (__DEV__) {
      Purchases.setLogLevel(LOG_LEVEL.DEBUG);
    }

    <span class="hljs-keyword">await</span> getOfferings();

    <span class="hljs-comment">// Add a listener to update the customerInfo state when the customer info changes, like when a user subscribes</span>
    Purchases.addCustomerInfoUpdateListener(<span class="hljs-function">(<span class="hljs-params">customerInfo</span>) =&gt;</span> {
      setCustomerInfo(customerInfo);
    });

    setInitialized(<span class="hljs-literal">true</span>);
  };

  <span class="hljs-comment">/**
   * Fetch the current offerings from RevenueCat
   */</span>
  <span class="hljs-keyword">const</span> getOfferings = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> offerings = <span class="hljs-keyword">await</span> Purchases.getOfferings();
    <span class="hljs-keyword">const</span> currentOffering = offerings.current;
    setOffering(currentOffering);
  };

  <span class="hljs-comment">/**
   *
   * @param purchasedPackage The package to purchase
   * @returns The result of the purchase
   */</span>
  <span class="hljs-keyword">const</span> purchasePackage = <span class="hljs-keyword">async</span> (purchasedPackage: PurchasesPackage) =&gt; {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> Purchases.purchasePackage(purchasedPackage);
    <span class="hljs-keyword">return</span> result;
  };

  <span class="hljs-comment">/**
   * Fetch the customer info from RevenueCat
   */</span>
  <span class="hljs-keyword">const</span> getCustomerInfo = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> customerInfo = <span class="hljs-keyword">await</span> Purchases.getCustomerInfo();
    setCustomerInfo(customerInfo);
  };

  <span class="hljs-comment">/**
   * Check if the user is subscribed to any offering
   * @returns True if the user is subscribed to any offering
   */</span>
  <span class="hljs-keyword">const</span> checkIfUserIsSubscribed = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!initialized || !customerInfo) <span class="hljs-keyword">return</span>;
    <span class="hljs-keyword">const</span> isPro = customerInfo.activeSubscriptions.length &gt; <span class="hljs-number">0</span>;
    setIsSubscribed(isPro);
  };

  <span class="hljs-comment">/**
   * Get the non-subscription purchase with the given identifier
   * @param identifier The identifier of the product to fetch
   * @returns The non-subscription purchase with the given identifier
   */</span>
  <span class="hljs-keyword">const</span> getNonSubscriptionPurchase = <span class="hljs-keyword">async</span> (identifier: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">if</span> (!initialized || !customerInfo) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;

    <span class="hljs-keyword">const</span> item = customerInfo.nonSubscriptionTransactions.find(
      <span class="hljs-function">(<span class="hljs-params">t</span>) =&gt;</span> t.productIdentifier === identifier
    );

    <span class="hljs-keyword">return</span> item;
  };

  useEffect(<span class="hljs-function">() =&gt;</span> {
    init();
    getCustomerInfo();
  }, []);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-comment">// Check if the user is subscribed to any offering after the customer info changes</span>
    checkIfUserIsSubscribed();
  }, [initialized, customerInfo]);

  <span class="hljs-comment">// If the Purchases SDK is not initialized, return null</span>
  <span class="hljs-keyword">if</span> (!initialized) {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

  <span class="hljs-keyword">return</span> (
    &lt;PurchasesContext.Provider
      value={{
        currentOffering: offering,
        purchasePackage,
        customerInfo,
        isSubscribed,
        getNonSubscriptionPurchase,
      }}
    &gt;
      {children}
    &lt;/PurchasesContext.Provider&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> PurchasesProvider;
</code></pre>
<h1 id="heading-using-a-custom-hook-for-better-developer-experience">Using a custom hook for better developer experience</h1>
<p>We'll need a custom hook to easily access all the exported members of our provider. Let's start by creating a new file for this.</p>
<pre><code class="lang-bash">mkdir hooks
touch hooks/usePurchases.tsx
</code></pre>
<p>Update the <code>usePurchases.tsx</code> with the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// hooks/usePurchases.tsx</span>
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> PurchasesContext, {
  PurchasesContextProps,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@/context/PurchasesContext"</span>;

<span class="hljs-comment">/**
 * Custom hook for managing purchases with RevenueCat.
 * @returns An object containing the following properties:
 *  - currentOffering - The current offering
 *  - purchasePackage - Purchase a package
 *  - customerInfo - The customer info
 *  - isSubscribed - Flag that indicates if the user is subscribed to any offering
 *  - getNonSubscriptionPurchase - Get the non-subscription purchase by identifier
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> usePurchases = <span class="hljs-function">() =&gt;</span>
  React.useContext(PurchasesContext <span class="hljs-keyword">as</span> React.Context&lt;PurchasesContextProps&gt;);
</code></pre>
<h1 id="heading-wrapping-the-app-with-the-new-provider">Wrapping the app with the new provider</h1>
<p>Go to <code>app/_layout.tsx</code> and update the file to wrap the app with the <code>PurchasesProvider</code>.</p>
<pre><code class="lang-diff">// app/_layout.tsx

...
<span class="hljs-addition">+ import PurchasesProvider from "@/context/PurchasesProvider";</span>

...

export default function RootLayout() {
  ...
  return &lt;RootLayoutNav /&gt;;
}

function RootLayoutNav() {
  const colorScheme = useColorScheme();

  return (
<span class="hljs-addition">+   &lt;PurchasesProvider&gt;</span>
      &lt;ThemeProvider value={colorScheme <span class="hljs-comment">=== "dark" ? DarkTheme : DefaultTheme}&gt;</span>
        &lt;Stack&gt;
          &lt;Stack.Screen name="(tabs)" options={{ headerShown: false }} /&gt;
          &lt;Stack.Screen name="modal" options={{ presentation: "modal" }} /&gt;
<span class="hljs-addition">+         &lt;Stack.Screen</span>
<span class="hljs-addition">+           name="subscriptions-paywall"</span>
<span class="hljs-addition">+           options={{ headerShown: false }}</span>
<span class="hljs-addition">+         /&gt;</span>
        &lt;/Stack&gt;
      &lt;/ThemeProvider&gt;
<span class="hljs-addition">+   &lt;/PurchasesProvider&gt;</span>
  );
}
</code></pre>
<h1 id="heading-adding-a-paywall-route-to-show-our-products">Adding a "Paywall" route to show our products</h1>
<p>This is where the magic happens! We need to create a new route for our Paywall screen.</p>
<pre><code class="lang-typescript">touch app/subscriptions-paywall.tsx
</code></pre>
<p>With the following code, we render a horizontal <code>FlatList</code> component that displays all available subscription options. Feel free to modify this to suit your needs.</p>
<p>Additionally, we use the offering metadata to enhance the information and display the number of months for each product. This is also optional, and you can remove it if desired.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//app/subscriptions-paywall.tsx</span>

<span class="hljs-keyword">import</span> { usePurchases } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/hooks/usePurchases"</span>;
<span class="hljs-keyword">import</span> { router } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> React, { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> {
  Dimensions,
  Platform,
  View,
  Image,
  ScrollView,
  Text,
  Button,
  Pressable,
  FlatList,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { PRODUCT_CATEGORY, PurchasesPackage } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-purchases"</span>;

<span class="hljs-keyword">const</span> screenHeight = Dimensions.get(<span class="hljs-string">"window"</span>).height;
<span class="hljs-keyword">const</span> screenWidth = Dimensions.get(<span class="hljs-string">"window"</span>).width;

<span class="hljs-keyword">const</span> SubscriptionsPaywall = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> { currentOffering, purchasePackage } = usePurchases();

  <span class="hljs-comment">// Filter out non-subscription products from RevenueCat</span>
  <span class="hljs-keyword">const</span> filteredPackages = currentOffering?.availablePackages.filter(
    <span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.product.productCategory === PRODUCT_CATEGORY.SUBSCRIPTION
  );

  <span class="hljs-keyword">const</span> [selectedPackage, setSelectedPackage] =
    useState&lt;PurchasesPackage | <span class="hljs-literal">null</span>&gt;(filteredPackages?.[<span class="hljs-number">0</span>] || <span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> [isLoading, setIsLoading] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> handleContinue = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!selectedPackage) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">try</span> {
      setIsLoading(<span class="hljs-literal">true</span>);
      <span class="hljs-keyword">await</span> purchasePackage(selectedPackage);
      router.back();
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    } <span class="hljs-keyword">finally</span> {
      setIsLoading(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;View style={{ gap: <span class="hljs-number">16</span>, flex: <span class="hljs-number">1</span>, backgroundColor: <span class="hljs-string">"white"</span> }}&gt;
      &lt;Image
        source={{
          uri: <span class="hljs-string">"https://i.imgur.com/qUZLBVT.jpg"</span>,
        }}
        height={screenHeight * <span class="hljs-number">0.3</span>}
        alt=<span class="hljs-string">"Background image showing food"</span>
        resizeMode=<span class="hljs-string">"cover"</span>
      &gt;&lt;/Image&gt;

      &lt;ScrollView contentContainerStyle={{ flexGrow: <span class="hljs-number">1</span> }}&gt;
        &lt;View style={{ flex: <span class="hljs-number">1</span>, alignItems: <span class="hljs-string">"center"</span>, gap: <span class="hljs-number">24</span>, padding: <span class="hljs-number">24</span> }}&gt;
          &lt;Text style={{ textAlign: <span class="hljs-string">"center"</span> }}&gt;
            {<span class="hljs-string">`Get access to our premium features`</span>}
          &lt;/Text&gt;
          &lt;PackagesCarousel
            packages={filteredPackages}
            onSelectPackage={<span class="hljs-function">(<span class="hljs-params">packageToSelect</span>) =&gt;</span>
              setSelectedPackage(packageToSelect)
            }
            selectedPackage={selectedPackage}
            metadata={currentOffering?.metadata <span class="hljs-keyword">as</span> PackageMetadata}
          &gt;&lt;/PackagesCarousel&gt;
        &lt;/View&gt;
        &lt;View style={{ padding: <span class="hljs-number">24</span>, gap: <span class="hljs-number">8</span>, marginBottom: <span class="hljs-number">24</span> }}&gt;
          &lt;Text style={{ textAlign: <span class="hljs-string">"center"</span> }}&gt;
            {<span class="hljs-string">`All Subscriptions include a 7-Day Free Trial`</span>}
          &lt;/Text&gt;
          &lt;Text style={{ textAlign: <span class="hljs-string">"center"</span> }}&gt;
            {<span class="hljs-string">`You can cancel at any time before the 7 day trial ends, and you won't be changed any amount`</span>}
          &lt;/Text&gt;
          &lt;Button
            disabled={isLoading}
            title={isLoading ? <span class="hljs-string">"Processing..."</span> : <span class="hljs-string">"Try it free for 7 days"</span>}
            onPress={handleContinue}
          /&gt;
        &lt;/View&gt;
      &lt;/ScrollView&gt;
    &lt;/View&gt;
  );
};

<span class="hljs-keyword">type</span> Subscription = {
  identifier: <span class="hljs-built_in">string</span>;
  cycles: <span class="hljs-built_in">number</span>;
  discount?: <span class="hljs-built_in">string</span>;
};

<span class="hljs-keyword">type</span> PackageMetadata = {
  subscriptions: {
    ios: Subscription[];
    android: Subscription[];
  };
};

<span class="hljs-keyword">type</span> PackagesCarouselProps = {
  onSelectPackage: <span class="hljs-function">(<span class="hljs-params">packageToSelect: PurchasesPackage</span>) =&gt;</span> <span class="hljs-built_in">void</span>;
  packages?: PurchasesPackage[];
  metadata: PackageMetadata;
  selectedPackage: PurchasesPackage | <span class="hljs-literal">null</span>;
};

<span class="hljs-keyword">const</span> PackagesCarousel: React.FC&lt;PackagesCarouselProps&gt; = <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> { packages, onSelectPackage, selectedPackage, metadata } = props;
  <span class="hljs-keyword">const</span> subscriptions =
    metadata.subscriptions[Platform.OS === <span class="hljs-string">"ios"</span> ? <span class="hljs-string">"ios"</span> : <span class="hljs-string">"android"</span>];

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"subscriptions"</span>, subscriptions);

  <span class="hljs-keyword">const</span> RenderItem = <span class="hljs-function">(<span class="hljs-params">item: PurchasesPackage</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (!selectedPackage) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    <span class="hljs-keyword">const</span> isSelected = selectedPackage.identifier === item.identifier;
    <span class="hljs-keyword">const</span> packageMetadata = subscriptions.find(
      <span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> s.identifier === item.product.identifier
    );

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"packageMetadata"</span>, packageMetadata);

    <span class="hljs-keyword">if</span> (!packageMetadata) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    <span class="hljs-keyword">const</span> discount = packageMetadata.discount;
    <span class="hljs-keyword">const</span> amountOfMonths = packageMetadata.cycles;

    <span class="hljs-keyword">return</span> (
      &lt;Pressable
        onPress={<span class="hljs-function">() =&gt;</span> {
          onSelectPackage(item);
        }}
      &gt;
        &lt;View
          style={{
            marginRight: <span class="hljs-number">16</span>,
            borderColor: isSelected ? <span class="hljs-string">"red"</span> : <span class="hljs-string">"black"</span>,
            borderWidth: <span class="hljs-number">3</span>,
            borderRadius: <span class="hljs-number">6</span>,
            overflow: <span class="hljs-string">"hidden"</span>,
            minWidth: screenWidth * <span class="hljs-number">0.4</span>,
          }}
        &gt;
          {discount &amp;&amp; (
            &lt;Text
              style={{
                position: <span class="hljs-string">"absolute"</span>,
                width: <span class="hljs-string">"100%"</span>,
                color: <span class="hljs-string">"black"</span>,
                textAlign: <span class="hljs-string">"center"</span>,
                padding: <span class="hljs-number">4</span>,
                backgroundColor: <span class="hljs-string">"yellow"</span>,
              }}
            &gt;
              {discount}
            &lt;/Text&gt;
          )}
          &lt;View
            style={{
              marginTop: <span class="hljs-number">16</span>,
              padding: <span class="hljs-number">16</span>,
              alignItems: <span class="hljs-string">"center"</span>,
            }}
          &gt;
            &lt;Text
              style={{
                fontSize: <span class="hljs-number">70</span>,
              }}
            &gt;
              {amountOfMonths}
            &lt;/Text&gt;
            &lt;Text&gt;{amountOfMonths &gt; <span class="hljs-number">1</span> ? <span class="hljs-string">"months"</span> : <span class="hljs-string">"month"</span>}&lt;/Text&gt;
            &lt;Text&gt;{item.product.priceString}&lt;/Text&gt;
          &lt;/View&gt;
        &lt;/View&gt;
      &lt;/Pressable&gt;
    );
  };

  <span class="hljs-keyword">if</span> (!packages) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;

  <span class="hljs-keyword">return</span> (
    &lt;FlatList
      contentContainerStyle={{
        paddingBottom: <span class="hljs-number">4</span>,
        marginRight: <span class="hljs-number">-16</span>,
        overflow: <span class="hljs-string">"hidden"</span>,
      }}
      horizontal={<span class="hljs-literal">true</span>}
      data={packages}
      pagingEnabled={<span class="hljs-literal">false</span>}
      keyExtractor={<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.identifier}
      renderItem={<span class="hljs-function">(<span class="hljs-params">{ item }</span>) =&gt;</span> &lt;RenderItem {...item}&gt;&lt;/RenderItem&gt;}
    &gt;&lt;/FlatList&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> SubscriptionsPaywall;
</code></pre>
<p>Finally, we need a way to navigate to our Paywall screen. Update the <code>app/(tabs)/index.tsx</code> like this:</p>
<pre><code class="lang-diff">// app/(tabs)/index.tsx

import { Button, StyleSheet } from "react-native";

import EditScreenInfo from "@/components/EditScreenInfo";
import { Text, View } from "@/components/Themed";
<span class="hljs-addition">+ import { router } from "expo-router";</span>

export default function TabOneScreen() {
  return (
    &lt;View style={styles.container}&gt;
      ...
<span class="hljs-addition">+     &lt;Button</span>
<span class="hljs-addition">+       title="Navigate to Paywall"</span>
<span class="hljs-addition">+       onPress={() =&gt; {</span>
<span class="hljs-addition">+         router.push("/subscriptions-paywall");</span>
<span class="hljs-addition">+       }}</span>
<span class="hljs-addition">+     /&gt;</span>
    &lt;/View&gt;
  );
}

const styles = StyleSheet.create({
  ...
});
</code></pre>
<h1 id="heading-ready-to-test">Ready to test?</h1>
<p>Run the following command to start your app using the development client:</p>
<pre><code class="lang-bash">npx expo start --dev-client
</code></pre>
<p>If everything went as expected, you'll see a home screen with a button to navigate to the Paywall route you created before.</p>
<p>Select any of the subscription options and confirm to purchase the package.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708643226070/7046ab12-1efa-44ca-94ba-f3078545f417.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-whats-next">What's next?</h1>
<p>With this solid foundation, you can now start working on your app to check customer information and grant or deny access to specific features based on their subscription status.</p>
<p>You can also create a dedicated screen to offer other products, such as a one-time purchase to remove ads. Explore the <code>PurchasesProvider</code>; it's filled with helpful methods.</p>
<hr />
<h1 id="heading-final-words">Final words</h1>
<p>I know this has been a long series, but I hope you find it useful (please let me know in the comments). To be honest, I chose to share all this information because I spent countless hours trying to understand all the different configurations involved in this process.</p>
<p>Moreover, These kinds of difficult tasks are the main reason so many apps fail before being released to the stores. Now, focus on your idea instead of how to add Payments 😉</p>
<p>Happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Collecting payments with React Native & RevenueCat (Part 3): Configuring Entitlements & Oferings in RevenueCat]]></title><description><![CDATA[💡
This is the third part of the "Collecting payments" series. Before proceeding, make sure to read Part 1 and Part 2


Before diving into this third part, I highly recommend watching this video to become familiar with some essential RevenueCat conce...]]></description><link>https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-3</link><guid isPermaLink="true">https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-3</guid><category><![CDATA[React Native]]></category><category><![CDATA[Expo]]></category><category><![CDATA[payment]]></category><category><![CDATA[RevenueCat]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Thu, 22 Feb 2024 23:41:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1708382042698/d38b98cf-2a42-4c44-8d58-6af4b2b86736.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This is the third part of the "Collecting payments" series. Before proceeding, make sure to read <a target="_blank" href="https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-1">Part 1</a> and <a target="_blank" href="https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-2">Part 2</a></div>
</div>

<p>Before diving into this third part, I highly recommend watching this video to become familiar with some essential RevenueCat concepts like Entitlements, Products, Packages, and Offerings.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=QxHeZiW4KCA">https://www.youtube.com/watch?v=QxHeZiW4KCA</a></div>
<p> </p>
<p>Following with the example we presented on the previous post, we now need to set up our subscriptions and one-time payment in RevenueCat.</p>
<h3 id="heading-scenario">Scenario</h3>
<p>Our example scenario includes:</p>
<ul>
<li><p>A monthly subscription with 7-day free trial</p>
</li>
<li><p>A annual subscription with 7-day free trial</p>
</li>
<li><p>A one-time purchase to remove ads from the app</p>
</li>
</ul>
<h3 id="heading-the-process">The process</h3>
<ol>
<li><p>Create 2 entitlements: “Pro access” (used by the subscriptions) and “Ad free” (used by those who paid to remove the ads)</p>
</li>
<li><p>Create 6 products: “Pro monthly”, “Pro annual”, and “Ad free” (both for Android and iOS).</p>
</li>
<li><p>Associate products with entitlements</p>
<ol>
<li><p>The “Pro access” entitlement will include the “Pro monthly” and “Pro annual” products for both platforms.</p>
</li>
<li><p>The “Ad free” entitlement will include the “Add free” product for both platforms.</p>
</li>
</ol>
</li>
<li><p>Create 3 “packages”</p>
<ol>
<li><p>The “Pro monthly” package will include the “Pro monthly” products for both platforms</p>
</li>
<li><p>The “Pro annual” package will include the “Pro annual” products for both platforms</p>
</li>
<li><p>The “Ad free” package will include the “Ad free” products for both platforms.</p>
</li>
</ol>
</li>
<li><p>Create 1 “offering” that will display the 3 packages mentioned before.</p>
</li>
</ol>
<p>I know… take a 5 minute break to digest all these new concepts 😄</p>
<p>I’ll guide you through the entire process below 💪</p>
<hr />
<h1 id="heading-spirokit-for-saas">SpiroKit for SaaS</h1>
<p>In case you want to save weeks of painful hours of work &amp; research on your next React Native app, checkout SpiroKit for SaaS. It's a starter template that comes with Purchases, Push Notifications, Authentication, Analytics, Error Reporting and more. It also includes a Notion template to guide you through the entire process</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://spirokit.com">https://spirokit.com</a></div>
<p> </p>
<hr />
<h3 id="heading-creating-the-entitlements">Creating the Entitlements</h3>
<p>The first step is to create 2 entitlements: “Pro access” and “Ad free” (feel free to adapt this to your needs)</p>
<ol>
<li><p>Visit <a target="_blank" href="https://app.revenuecat.com/">https://app.revenuecat.com/</a> and navigate to your project dashboard.</p>
</li>
<li><p>In the sidebar, under the “Product catalog” section, click on “Entitlements”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F2476ac08-7366-4172-849c-33a259d80fcb%2FUntitled.png?table=block&amp;id=54f2668d-a69e-4820-ad77-f644b682affe&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>On the top right corner, click on “New”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F0d15c14b-520c-48e6-aebc-fc90695a2862%2FUntitled.png?table=block&amp;id=31a38cf2-1b7c-47a2-a3c0-171e387bb780&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose an identifier and a description for your new entitlement</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fcc3d5976-70c4-4378-a393-621575348314%2FUntitled.png?table=block&amp;id=12aed534-5a2a-4173-901a-f454384274b0&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Repeat step 3 and 4 for the “Ad free” entitlement (feel free to adapt this to your needs)</p>
</li>
</ol>
<hr />
<h3 id="heading-creating-the-products">Creating the Products</h3>
<p>We’ll need to create 6 products: “Pro monthly”, “Pro annual”, and “Ad free” (both for Android and iOS).</p>
<h4 id="heading-automatically-importing-android-products">Automatically importing Android products</h4>
<ol>
<li><p>In the sidebar, under the “Product catalog” section, click on “Products”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fa18eac5c-2ae5-4fff-aa3f-64d4adb1337e%2FUntitled.png?table=block&amp;id=566caa89-e719-4083-b21f-2f179fd462fd&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>On the top right corner, click on “New”</p>
</li>
<li><p>Choose your Android App</p>
</li>
<li><p>Click on “Import products”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F0359d908-5e95-4cdf-85e3-6ab75836d89a%2FUntitled.png?table=block&amp;id=5742d59a-ec0c-4cff-a675-43dddb80cdb6&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>If everything went as expected, you should see something like this</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F8367ab10-c13f-4a75-a494-9738712aa8d5%2FUntitled.png?table=block&amp;id=93f62627-a54a-4600-ab9e-4982e05cfe58&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Select all the products you want to import and click “Import” to confirm.</p>
</li>
<li><p>Repeat steps 2 to 4 for the iOS app. But there’s a small difference with Android. You’ll need to setup your App Store Connect API key</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F194ff02f-04be-47c9-bae9-18a4db48c8fc%2FUntitled.png?table=block&amp;id=18fd2c57-f342-432b-be0f-9a9e68e7fd67&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h4 id="heading-automatically-importing-ios-products">Automatically importing iOS products</h4>
<p>Before you can automatically import the iOS products, you’ll need to setup your <strong>App Store Connect API key.</strong> In order to do that, follow these steps:</p>
<ol>
<li><p>Visit the <a target="_blank" href="https://appstoreconnect.apple.com/">App Store Connect</a></p>
</li>
<li><p>Click on “Users and access”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fbbd794f3-3c97-4111-bb70-ab3d552635e7%2FUntitled.png?table=block&amp;id=325efe8a-15f9-4715-b51c-2988e541d064&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on the “Keys” tab, and then click “+”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fd8a29638-b2b9-41db-9bda-c66ac81b987c%2FUntitled.png?table=block&amp;id=0d4e7dbe-06ad-435f-ac9e-8b913fd40d4f&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Assign a descriptive name, and assign the “App Manager” role that will be required by RevenueCat to import the products. Then, click on “Generate” to confirm</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F3f7834f2-30fd-4849-9df8-2c686d69b2f1%2FUntitled.png?table=block&amp;id=a2143db1-f0ed-472e-895f-95acda13701f&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>In the right corner, click on “Download”. Make sure to store this file in a safe place, as you won’t be able to download it again.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fb8dc90c0-8055-46e4-ae3b-05d55ecacd5b%2FUntitled.png?table=block&amp;id=c7d12581-1a3f-432f-b2b9-6d75dd1dc8e8&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Once you downloaded the file, go back to your RevenueCat dashboard</p>
</li>
<li><p>In the sidebar, click on your iOS app</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F6f54b42d-6df0-4827-a444-5473043a38df%2FUntitled.png?table=block&amp;id=200ee034-20c3-4e10-85e7-b113107e34bb&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Scroll to the “<strong>App Store Connect API</strong>” section, and drop the file you just downloaded.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F47ee7324-b93b-4094-b770-a5ebbbf98530%2FUntitled.png?table=block&amp;id=9bfc3025-4f53-435c-af50-d77f204f61a8&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>After that, you’ll be requested to complete two additional fields</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fae6af265-3eb5-45cb-b2d9-f21053bfac50%2FUntitled.png?table=block&amp;id=c6b8e169-3420-4591-9c6f-003d643c5f0f&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>You can get the Issuer ID in the App Store Connect, in the same page you just downloaded your API key</p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F6b4f6675-4c14-4a22-b7af-030aef6fffd4%2FUntitled.png?table=block&amp;id=6cd2f7eb-12e7-4a5d-b05e-543c5e2101b2&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>For the Vendor number, visit the “<strong>Payments and Financial Reports</strong>” section in the App Store Connect. You should see your vendor id in the top left corner</p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F6903b24b-14e6-4565-a95b-fc3eef8049ea%2FUntitled.png?table=block&amp;id=43717de7-3428-4c95-9548-eff70c14acc0&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “<strong>Save changes</strong>” to confirm</p>
</li>
<li><p>Now you can import your iOS products by following the same steps required for Android.</p>
</li>
<li><p>Make sure to select all the products and click “Import”</p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F459a24a4-ec2e-42c4-a748-fb4cd2596d4c%2FUntitled.png?table=block&amp;id=70a6a2bc-3a12-4630-b311-25d99b20bd40&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<p>If everything went as expected, your “<strong>Products</strong>” section should look like this:</p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fe27c5fc8-1617-4ba6-869a-b942f03b928e%2FUntitled.png?table=block&amp;id=6a281eed-d776-477f-a38b-9f1a3dae59bf&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<hr />
<h3 id="heading-attaching-products-to-entitlements">Attaching Products to Entitlements</h3>
<ol>
<li><p>Go back to “<strong>Entitlements</strong>” in the sidebar</p>
</li>
<li><p>Click on the “<strong>Pro access</strong>” entitlement</p>
</li>
<li><p>In the “<strong>Associated Products</strong>” section, click on “<strong>Attach</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fb2e571ef-82bf-4d80-92d5-8e38700e3ba3%2FUntitled.png?table=block&amp;id=a3c0ee90-65f2-40a3-ac19-43a353f15644&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>You’ll need to add the 4 products related to “Pro access” which are:</p>
<ol>
<li><p>Monthly subscription (iOS / Android)</p>
</li>
<li><p>Annual subscription (iOS / Android)</p>
</li>
</ol>
</li>
<li><p>After adding all the products, it should look like this</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fbf886464-1b6a-49e8-94dc-956fa0329261%2FUntitled.png?table=block&amp;id=8062b03a-56f6-4309-9342-8ffd07e91e53&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Repeat steps 2 and 3 for the “<strong>Add free</strong>” entitlement. This time, make sure to only add the “Ad free” products (iOS / Android). It should look like this</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F505e01ff-b1c6-4d10-a3b5-ed79ce2a3611%2FUntitled.png?table=block&amp;id=7078dfa8-0566-4bc5-b400-c804e8b1f73e&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<hr />
<h3 id="heading-creating-offerings">Creating Offerings</h3>
<ol>
<li><p>In the sidebar, click on “<strong>Offerings</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F555a3ca2-a9b3-453b-b5a5-bc28ea575725%2FUntitled.png?table=block&amp;id=348dc46f-9cca-43e2-b7fb-fb1b05d2a2bf&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>In the top right corner, click on “<strong>New</strong>”</p>
</li>
<li><p>You’ll need to provide an “<strong>Identifier</strong>” and a “<strong>Description</strong>” for the offering.</p>
</li>
<li><p>In the “Metadata” section, add the following json structure. Make sure to provide your product identifiers, and set the discount based on your pricing. This information will be used later in the starter template, to show a “50% OFF” label in the annual subscription</p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"subscriptions"</span>: {
     <span class="hljs-attr">"android"</span>: [
       {
         <span class="hljs-attr">"cycles"</span>: <span class="hljs-number">12</span>,
         <span class="hljs-attr">"discount"</span>: <span class="hljs-string">"50%"</span>,
         <span class="hljs-attr">"identifier"</span>: <span class="hljs-string">"[YOUR_ANDROID_ANNUAL_PRODUCT_ID]"</span>
       },
       {
         <span class="hljs-attr">"cycles"</span>: <span class="hljs-number">1</span>,
         <span class="hljs-attr">"identifier"</span>: <span class="hljs-string">"[YOUR_ANDROID_MONTHLY_PRODUCT_ID]"</span>
       }
     ],
     <span class="hljs-attr">"ios"</span>: [
       {
         <span class="hljs-attr">"cycles"</span>: <span class="hljs-number">12</span>,
         <span class="hljs-attr">"discount"</span>: <span class="hljs-string">"50%"</span>,
         <span class="hljs-attr">"identifier"</span>: <span class="hljs-string">"[YOUR_IOS_ANNUAL_PRODUCT_ID]"</span>
       },
       {
         <span class="hljs-attr">"cycles"</span>: <span class="hljs-number">1</span>,
         <span class="hljs-attr">"identifier"</span>: <span class="hljs-string">"[YOUR_IOS_MONTHLY_PRODUCT_ID]"</span>
       }
     ]
   }
 }
</code></pre>
</li>
<li><p>Click “<strong>Save</strong>” to finish</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F45dd02c5-ecc7-49be-a8e9-fbf054a18694%2FUntitled.png?table=block&amp;id=18be8b6d-8f9c-4a13-b311-fa8e109f9a36&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<blockquote>
<p>In order to start using this Offering, you’ll need to add at least one package to it in the next section.</p>
</blockquote>
<hr />
<h3 id="heading-adding-packages-to-offerings">Adding Packages to Offerings</h3>
<p>We’ll need to create 3 packages:</p>
<ul>
<li><p>The “Pro monthly” package will include the “Pro monthly” products for both platforms</p>
</li>
<li><p>The “Pro annual” package will include the “Pro annual” products for both platforms</p>
</li>
<li><p>The “Ad free” package will include the “Ad free” products for both platforms.</p>
</li>
</ul>
<ol>
<li><p>In the sidebar, go to “<strong>Offerings</strong>”</p>
</li>
<li><p>Click on the offering you just created in the previous section</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F3274ae81-8e69-41f2-afad-e39a8e4fd199%2FUntitled.png?table=block&amp;id=76af2678-153f-4676-bebe-346d3dc1e427&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>In the “<strong>Packages</strong>” section, click on “<strong>New</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F0a0ddeaf-e510-4556-98ed-71c732208c8d%2FUntitled.png?table=block&amp;id=1c5cc3ac-324c-4901-b8f8-118346829330&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>For the identifier, you can choose between a list of “standard identifiers” or choose “Custom” to create your own identifier. I decided to use the standards for monthly and annual subscriptions, but a custom identifier for the “Ad free” package.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fd8797d28-6112-4375-81a5-510dead9a1e2%2FUntitled.png?table=block&amp;id=ffce12e9-84b8-43d8-a8b2-e617d68557b8&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “Add” to confirm.</p>
</li>
<li><p>Now, click on the new package, and then click on “<strong>Attach</strong>” in the “<strong>Products</strong>” section.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F6f5d6c51-aa3f-4232-9aa5-47a88d401014%2FUntitled.png?table=block&amp;id=ca00650c-83e2-46a0-bce0-ae597b362580&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Select the relevant product for each platform. In this case, I’m adding the monthly products like this:</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fdd73f066-5149-4e79-b349-8facfe0dbe77%2FUntitled.png?table=block&amp;id=f3caca83-5e4e-42e8-9d85-ea74c29d62a6&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click “<strong>Attach</strong>” to confirm</p>
</li>
<li><p>The first package is complete with the products associated. Now, go back to the offering and repeat steps 3 to 8 for the “<strong>Annual</strong>” package and for the “<strong>Ad free</strong>” package.</p>
</li>
<li><p>If everything went as expected, your offering should look like this:</p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F7ed7f73a-18bd-46f9-a5f6-25f5337dc02f%2FUntitled.png?table=block&amp;id=86bb10a4-33e7-48f1-a959-ff53d2dcc029&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<p>Congrats! You are done with all the boring part! Now, it's time to code 🥳</p>
<h1 id="heading-whats-next">What's next?</h1>
<p>In the next and final article of these series, I'll focus on creating a new React Native project and wiring everything together to create a Paywall screen where users can purchase your products.</p>
<p>You can read the next part of the series here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-4">https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-4</a></div>
]]></content:encoded></item><item><title><![CDATA[Collecting payments with React Native & RevenueCat (Part 2): Configuring Subscriptions in the Stores]]></title><description><![CDATA[💡
This is the third part of the "Collecting payments" series. Before proceeding, make sure to read Part 1



SpiroKit for SaaS
In case you want to save weeks of painful hours of work & research on your next React Native app, checkout SpiroKit for Sa...]]></description><link>https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-2</link><guid isPermaLink="true">https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-2</guid><category><![CDATA[React Native]]></category><category><![CDATA[Expo]]></category><category><![CDATA[payment]]></category><category><![CDATA[RevenueCat]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Thu, 22 Feb 2024 23:38:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1708382057558/49bb924f-4e37-4916-9116-59dbcdccb1ce.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This is the third part of the "Collecting payments" series. Before proceeding, make sure to read <a target="_blank" href="https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-1">Part 1</a></div>
</div>

<hr />
<h1 id="heading-spirokit-for-saas">SpiroKit for SaaS</h1>
<p>In case you want to save weeks of painful hours of work &amp; research on your next React Native app, checkout SpiroKit for SaaS. It's a starter template that comes with Purchases, Push Notifications, Authentication, Analytics, Error Reporting and more. It also includes a Notion template to guide you through the entire process</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://spirokit.com">https://spirokit.com</a></div>
<p> </p>
<hr />
<h1 id="heading-app-store-connect-setup-ios">App Store Connect Setup (iOS)</h1>
<p>Before we can offer subscriptions and/or in-app purchases on our iOS app, we need to setup in-app purchases in App Store Connect.</p>
<h2 id="heading-creating-a-subscription-group-app-store-connect">Creating a Subscription Group (App Store Connect)</h2>
<ol>
<li><p>Visit App Store Connect. Then click on “My Apps”, and choose your app.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fb0da1975-ad12-46ab-ad6e-89b0c54e93f2%2FUntitled.png?table=block&amp;id=b3628113-0e15-40c0-8c9c-d38e134a5ddc&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fe8badb54-2bd2-4a51-8a87-e68077e3352e%2FUntitled.png?table=block&amp;id=c7baabed-1501-4dbc-b2d4-717845e04800&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<p> Click on “Subscriptions”, within the “<strong>Features</strong>” section in the sidebar</p>
</li>
<li><p>Under “<strong>Subscription Groups</strong>”, click on “<strong>Create</strong>”. As mentioned in the App Store Connect, All Subscriptions must be a part of a group. Subscription Groups are ways to organize your products in App Store Connect so users are able to switch between products.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F46b98433-bbc3-4e0e-996b-e0d4432d47e8%2FUntitled.png?table=block&amp;id=d5f9611b-532e-4d27-b450-cdd876b2c0ec&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose a “<strong>Reference Name</strong>” for the Subscription Group, and click on “<strong>Create</strong>” to confirm.</p>
<blockquote>
<p>The Reference Name is not user-facing. It’s just for you.</p>
</blockquote>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F3248c4b4-3b72-424b-acfc-c755dc9a7e5f%2FUntitled.png?table=block&amp;id=cc1c3b85-0087-471f-9983-af984d232922&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h2 id="heading-adding-a-new-product-to-the-subscription-group-app-store-connect">Adding a new product to the subscription group (App Store Connect)</h2>
<p>Before we start adding new products to our app, I would like to define a useful example so you can follow along and tweak it based on your needs.</p>
<p>Let’s say we have an App that offers both a monthly and an annual subscription. Both come with a 7-day trial period, and give users access to premium features that are not available in the free version.</p>
<p>Besides the available subscriptions, we also want to offer a one-time in-app payment so our users can remove ads from the free version.</p>
<h3 id="heading-annual-subscription">Annual Subscription</h3>
<ol>
<li><p>After creating the subscription group, click “Create” to add your first product.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Feeb02aaf-e846-43bf-8ddf-8e7d31d230c4%2FUntitled.png?table=block&amp;id=4e1fdee3-799d-483b-9fab-f392c414ad3c&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>You’ll need to provide:</p>
<ul>
<li><p><strong>Reference Name</strong>: The reference name will be used on App Store Connect and in Sales and Trends reports from Apple. It won't be displayed to your users on the App Store. We recommend using a human readable description of the purchase you plan to set up. The name can't be longer than 64 characters.</p>
</li>
<li><p><strong>Product ID</strong>: The product Id is a unique alphanumeric ID that is used for accessing your product in development and syncing with RevenueCat. After you use a Product ID for one product in App Store Connect, it can’t be used again across any of your apps, even if the product is deleted. It helps to be a little organized here from the beginning. RevenueCat recommend using a consistent naming scheme across all of your product identifiers such as: <code>&lt;app&gt;_&lt;price&gt;_&lt;duration&gt;_&lt;intro duration&gt;&lt;intro price&gt;</code></p>
<ul>
<li><p><strong>App:</strong> Some prefix that will be unique to your app, since the same product Id cannot but used in any future apps you create.</p>
</li>
<li><p><strong>Price:</strong> The price you plan to charge for the product in your default currency.</p>
</li>
<li><p><strong>Duration:</strong> The duration of the normal subscription period.</p>
</li>
<li><p><strong>Intro duration:</strong> The duration of the introductory period, if any.</p>
</li>
<li><p><strong>Intro price:</strong> The price of the introductory period in your default currency, if any.</p>
</li>
</ul>
</li>
<li><p>Based on this information, I’ll use the following product id for my example:</p>
<ul>
<li><p><code>saasstarterdemo1_3999_1y_1w0</code></p>
<ul>
<li><p>Name of my app: saas starter demo 1</p>
</li>
<li><p>Price: $39.99</p>
</li>
<li><p>Duration: 1 year</p>
</li>
<li><p>Intro/trial duration: 1 week</p>
</li>
<li><p>Intro/trial price: $0.00</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>Click on “<strong>Create</strong>” to confirm</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fa98e7b63-6fc8-4dac-8c41-8052357d9732%2FUntitled.png?table=block&amp;id=7e6395ba-8cc9-4ffd-8531-244fb3e32bff&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h4 id="heading-setting-duration">Setting duration</h4>
<p>After creating the new product, you’ll need to select a duration for the subscription. Choose “1 year” in the dropdown menu, and click “<strong>Save</strong>”</p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fd342df58-1ee0-4ea9-8680-3259c94a318a%2FUntitled.png?table=block&amp;id=94d69f04-4fbf-4cad-88a8-e3468442ebf9&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<h4 id="heading-setting-availability">Setting availability</h4>
<ol>
<li><p>Scroll to the “Availability” section, and click “Set up Availability”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fbbded214-f1fc-434d-a239-991e377251ff%2FUntitled.png?table=block&amp;id=be42e113-e400-4c61-a28f-83519d9e3fe1&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose the countries you want to offer the subscriptions, and click “<strong>Done</strong>” to finish.</p>
</li>
<li><p>Don’t forget to click “Save” in the top right corner</p>
</li>
</ol>
<h4 id="heading-setting-price">Setting price</h4>
<ol>
<li><p>You’ll need to set the subscription price. Scroll to the “<strong>Subscription Prices</strong>” section, and click “<strong>Add Subscription Price</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F53287056-1207-4c1a-b2e5-9581bb96c6bd%2FUntitled.png?table=block&amp;id=376d1fda-88fc-46c9-9425-244c1fe13a06&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose your Country or Region, and select $39.99 for the price. Click on “Next”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F68afd00a-9163-4a2b-b742-3059a58320eb%2FUntitled.png?table=block&amp;id=64497930-be2f-4802-868e-d8d4c7e03028&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>You can optionally change the price for specific countries. Once you are done with that, click “<strong>Next</strong>” again and “<strong>Confirm</strong>” to finish.</p>
</li>
</ol>
<h4 id="heading-setting-introductory-offers-optional">Setting Introductory Offers (Optional)</h4>
<ol>
<li><p>You’ll need to setup the 7-day trial period. Scroll to the “<strong>Subscription Prices</strong>” section, and click on “<strong>View all Subscription Pricing</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fe8b7ae10-3d9f-42ec-a96c-d6ca77bea1af%2FUntitled.png?table=block&amp;id=162b0b08-ad85-4f84-afb0-017a59c1248b&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on the “<strong>Introductory Offers</strong>” tab, and click on “<strong>Set up Introductory Offer</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F1c7a0d70-62a5-41f8-a0bd-b58ec877c881%2FUntitled.png?table=block&amp;id=0002b657-1de3-414d-9065-fe4524ca6cfc&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F56dd5d60-c5d3-4ddc-b372-455c3310afda%2FUntitled.png?table=block&amp;id=9d43db8f-0457-41bd-b9fe-9c508b8cf35d&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose which countries you want to offer the free trial, and click “<strong>Next</strong>”.</p>
</li>
<li><p>For the Start/End date, I went with:</p>
<ol>
<li><p>Start Date: Current Date</p>
</li>
<li><p>End Date: “No end date”</p>
</li>
</ol>
</li>
</ol>
<p>    Click “Next” to continue</p>
<p>    <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F95de66a8-61a9-49cb-b04a-d03b526c4c69%2FUntitled.png?table=block&amp;id=0cf9ff6c-5853-43f3-b952-e54d5397c098&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<ol>
<li><p>Choose “<strong>Free</strong>” as Type, and “<strong>1 Week</strong>” for duration. Click “<strong>Next</strong>” to continue, and finally “<strong>Confirm</strong>” to finish.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F6db38145-a804-43b2-868e-c3861bee03e6%2FUntitled.png?table=block&amp;id=93560658-1630-4329-a850-4879d8251651&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h4 id="heading-adding-localizations">Adding Localizations</h4>
<p>You’ll need to provide a name and description for the in-app purchase that the user will see.</p>
<ol>
<li><p>From the “<strong>Subscription Pricing</strong>” page, go back to the home page for your subscription.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F89bed097-9f0b-4fad-a379-dbf483b2457e%2FUntitled.png?table=block&amp;id=5bc7c981-6323-4f80-97f8-0b4bc1508d19&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Scroll to the “<strong>App Store Localization</strong>” section. Click “<strong>Add Localization</strong>”.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F5cb07ce1-bf28-4eba-85a4-3b54641d4272%2FUntitled.png?table=block&amp;id=67445826-641c-4e00-995d-8a34090ccb23&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Add the required information for at least one language.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F06a2aee6-75e4-468b-a753-ae83d5af6a0c%2FUntitled.png?table=block&amp;id=268a65da-11a7-4ba6-8191-0582ff7d4bf0&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>If you scroll up, you may see the following message:</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Ffc0b8333-247a-4e8d-95b2-e6522ea4d472%2FUntitled.png?table=block&amp;id=6be3fb5d-6b4b-4563-8155-d6e5d24a2b14&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Make sure to visit that page and setup localizations for the Subscription Group you created earlier.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fdcadd8b9-02eb-49e1-b5dc-5f79d9f5d88e%2FUntitled.png?table=block&amp;id=1ab48cfd-65fd-46ce-90ea-3d8e29ebf3b8&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Ffea1b72e-6688-4f74-af7c-777d0abbff80%2FUntitled.png?table=block&amp;id=f28ea96a-d5d5-4831-a796-b6ecac2a74c4&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h4 id="heading-adding-review-information">Adding Review Information</h4>
<p>You’ll be asked to upload a screenshot of the paywall for an Apple reviewer. You also need to provide review notes to make the review process smoother.</p>
<ol>
<li><p>Navigate to the “<strong>Review Information</strong>” section and complete the requested information. You can upload the screenshot later once you've build the paywall screen on your app.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F81617859-2f6f-41b8-bf90-d656cbdc5290%2FUntitled.png?table=block&amp;id=77548a74-3af3-4075-8d99-8bf9577c60fd&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<blockquote>
<p>For the screenshot, you’ll need to upload an image with one of the following sizes: <a target="_blank" href="https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications">https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications</a></p>
</blockquote>
</li>
<li><p>Finally, click “<strong>Save</strong>” in the top right corner</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F52ec530f-664b-4297-890c-d95a67bbcf21%2FUntitled.png?table=block&amp;id=5c49cdff-23a9-4190-b3c5-08d274dbeb78&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h3 id="heading-monthly-subscription">Monthly Subscription</h3>
<p>For the monthly subscription, you need to follow the exact same steps mentioned in the “<strong>Annual Subscription”</strong> section above.</p>
<p>Here’s the information I’m using for my example app:</p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F8ccf8406-ed69-4976-b829-d80810149177%2FUntitled.png?table=block&amp;id=dc8ce198-6235-43ad-8fbf-5123118d9198&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F6ad3cac8-c07f-4e69-9f23-f5e8432ab3fd%2FUntitled.png?table=block&amp;id=48d6014d-4119-4a63-a1d4-a936b2bbc643&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fcca84619-3292-4168-8db3-4c4a2013650e%2FUntitled.png?table=block&amp;id=d5e91bfe-6a1d-4a76-9884-304c6780d7d0&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F1fbc97c8-a2d6-4675-9a67-59a664b4929a%2FUntitled.png?table=block&amp;id=c1eba6fe-81d2-427d-a97f-8112c9b770c1&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Ff6af61f4-99d1-416e-beef-69cde1eb571f%2FUntitled.png?table=block&amp;id=99adcc6f-5254-407d-8b33-2591b46e9d06&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<h3 id="heading-one-time-payment-to-remove-ads">One-time payment to remove ads</h3>
<ol>
<li><p>For one-time payments, you’ll need to go back to you app page in App Store Connect, and click on “<strong>In-App Purchases</strong>” in the sidebar (under the “<strong>Features</strong>” section). Then, click on “<strong>Create</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F7937036d-bc64-4a41-8dd5-5e7da1a5a99c%2FUntitled.png?table=block&amp;id=8875be33-73c7-4add-8826-c0234da43463&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>For “<strong>Type</strong>,” choose “<strong>Non-Consumable</strong>” because the “Remove ads” feature we’ll offer in our example app does not expire. Add your “<strong>Reference Name</strong>” and “<strong>Product ID</strong>” following a clear convention like you did before. Here’s what I used as example</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F00cb622c-9255-46d4-ad87-f6e67dd114bc%2FUntitled.png?table=block&amp;id=c6b5244f-1ec3-4140-b831-d623b23bf80b&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>As you did with the subscriptions before, you’ll need to setup “<strong>Availability</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Ff9ee417e-efc0-4dbb-8b26-c614e65b3f36%2FUntitled.png?table=block&amp;id=151bf1c2-0614-4e60-8370-913aa7d53d0f&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Next, you’ll need to provide a price. Scroll to the “Price Schedule” section and click on “<strong>Add Pricing</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Ff5df4dbc-9f16-4dfb-8bd5-12ecdf9a09f5%2FUntitled.png?table=block&amp;id=d1b0d244-f8ce-4c7c-8272-4df6e07864e8&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose a country and a price, then click “<strong>Next</strong>”. Review the prices for different countries if you like, then click “<strong>Confirm</strong>” to finish.</p>
</li>
<li><p>Scroll to the “<strong>App Store Localization</strong>” section, and choose a “<strong>Display Name</strong>” and “<strong>Description</strong>” as you did before in the subscriptions. Then, click on “<strong>Create</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fad052983-59c4-41cb-a1fd-5ac7e595bf21%2FUntitled.png?table=block&amp;id=720dfeb4-9907-486b-957e-c3313e2d60d6&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Finally, complete the “Review Information” section with the screenshot and notes as before.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F0986446b-1cb4-4156-8aaa-057145e4412c%2FUntitled.png?table=block&amp;id=ad275822-6d62-4a32-8806-bbe66475cf27&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<hr />
<h1 id="heading-google-play-billing-setup-android">Google Play Billing Setup (Android)</h1>
<p>To set up products for Android devices, start by logging into Google Play Console.</p>
<h2 id="heading-set-up-a-google-payments-merchant-account">Set up a Google Payments merchant account</h2>
<p>This step is only required if you are setting up payments in Google Play Console for the first time.</p>
<ol>
<li><p>Select your app in Google Play Console.</p>
</li>
<li><p>In the sidebar, scroll to the “<strong>Monetize</strong>” section and click on “<strong>Subscription</strong>”.</p>
</li>
<li><p>If you don’t see this message, feel free to skip the rest of this step.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F145a72d2-c616-4937-a219-fb9d17706a75%2FUntitled.png?table=block&amp;id=b3e93c84-e584-4130-a1cf-23cd15c821a6&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “<strong>Set up a merchant account</strong>”, then click on “<strong>Create payments profile</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fd60126d6-552c-46ce-8511-4b8a7caba0bc%2FUntitled.png?table=block&amp;id=e2f5fb5b-8deb-48e4-90da-59f027b6167d&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>You’ll see a list of Payments Profiles. In my case, I already had a profile generated, but you can select “<strong>Create payments profile</strong>.” In any case, you’ll need to provide additional information.</p>
</li>
<li><p>Complete the form with the requested information, and click “Submit” to finish.</p>
</li>
</ol>
<h2 id="heading-introduction">Introduction</h2>
<p>I’ll guide you through the process to replicate the same subscriptions and in-app purchases I described above for iOS. This includes:</p>
<ul>
<li><p>An annual subscription ($39.99) - 1 week free trial</p>
</li>
<li><p>A monthly subscription ($4.99) - 1 week trial</p>
</li>
<li><p>A one-time purchase to remove ads ($2.99)</p>
</li>
</ul>
<p>On Android, there are a few differences with the keywords I used for the iOS setup. On Android, You’ll only need to create one subscription that will serve both the annual and the monthly plans (called “Base plans” here). Then, each plan can have one or more “Offers” associated. We’ll add offers for the free trial.</p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F14b0745e-1673-43a5-b6fc-b55bf056ee1e%2FUntitled.png?table=block&amp;id=4b2cdc0a-aa58-4968-8d9b-9de10f5b061a&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<ul>
<li><p><strong>Subscription</strong>: Represents a user benefit, such as “Access to Premium features.” Independent of how it’s paid.</p>
</li>
<li><p><strong>Base Plans</strong>: Specify the duration, price, availability (countries), etc. You can create pre-paid plans (non-renewing) too.</p>
</li>
<li><p><strong>Offers</strong>: Allow eligible users to access a subscription at a discounted price. You can target offers at different user segments for acquisition and retention, or to incentivize users to upgrade.</p>
</li>
</ul>
<h2 id="heading-creating-a-new-subscription-for-the-annual-and-monthly-base-plans">Creating a new Subscription for the Annual and Monthly base plans</h2>
<ol>
<li><p>Once you logged in into Google Play Console, select your app from the app list.</p>
</li>
<li><p>In the sidebar, scroll to the “<strong>Monetize</strong>” section, and click on “<strong>Subscription</strong>”.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fc7c11d9c-35c6-44ce-bce9-5052dada2085%2FUntitled.png?table=block&amp;id=71224245-0771-4d43-93ed-8a246506ed7e&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “<strong>Create subscription</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F5205a8f4-6f24-4034-887a-6aa9cd95f529%2FUntitled.png?table=block&amp;id=5ebaea3a-370f-4e40-b833-25f056ac572d&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>You’ll need to provide a “Product ID” and a “Name”. For the Product ID, RevenueCat has a set of recommendations you can find <a target="_blank" href="https://www.revenuecat.com/docs/getting-started/entitlements/android-products#tips-for-creating-robust-product-ids">in this link</a>. I’m sharing an example below using the following structure: <code>appname_subscriptioninfo_version</code></p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F8c099495-14e6-4fdf-8c9f-0f1124792972%2FUntitled.png?table=block&amp;id=964ee5fa-28a6-4e25-982d-3c8d90b295d3&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click “<strong>Create</strong>” to proceed.</p>
</li>
<li><p>To complete the set up for the subscription, you’ll need to add at least a base plan. Feel free to also add the subscription benefits, which is recommended. We’ll cover the steps 2 and 3 in the following sections</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fcefbffb2-1cda-4f1d-adb1-a44babeeb911%2FUntitled.png?table=block&amp;id=f637f841-2cc0-4e2f-822e-21e7c3e16cee&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h2 id="heading-adding-the-annual-base-plan">Adding the Annual Base Plan</h2>
<ol>
<li><p>In the “<strong>Set up the subscription</strong>” section, click on “<strong>Add a base plan</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F20c6a050-1441-45f9-8a8d-9bc6fae508b7%2FUntitled.png?table=block&amp;id=f28acecc-be07-4e92-bbdf-e41d5be9244b&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>For the “Base Plan ID”, I’m using the structure: <code>duration_type_price</code></p>
</li>
<li><p>In the “Type” section, select “Auto-renewing”, and choose a yearly billing period. Complete the other information based on your preferences. Here’s an example:</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F49e3e8a2-6c54-4687-bd06-eaa809f72650%2FUntitled.png?table=block&amp;id=aead79d3-ca68-4c15-8ab8-bf1c0ba3173b&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Next, you’ll need set prices and availability. Click the “Set prices” button on the right</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F715b7113-429f-4aec-9ec9-95cbb7909f22%2FUntitled.png?table=block&amp;id=c71207bf-d39a-41b6-a848-b63c6646743e&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Use the checkbox at the top of the countries table to select all the countries, and then click “Set Price” in the button below</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F00de1d23-1157-4120-a4be-a0cb2913d450%2FUntitled.png?table=block&amp;id=f57f5ee1-5019-475c-9c70-7b2c9aa15353&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose a price and click “Update”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F65ab1af3-3897-4416-984f-62bff6ac7735%2FUntitled.png?table=block&amp;id=c7241665-25f5-4044-be12-f972a8db1fdd&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>In the bottom right corner, click “<strong>Save</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Ff590dced-885e-4ace-9e90-471ee14ef2a6%2FUntitled.png?table=block&amp;id=cda7298f-e22d-4eb5-8003-e7b814325a48&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Finally, click “<strong>Activate</strong>” so you can start using this base plan</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fdd19c817-26f9-4f78-8e3e-35eb57c03901%2FUntitled.png?table=block&amp;id=b6f742cc-accb-4616-943b-c043b86cef9c&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h3 id="heading-adding-the-free-trial-for-the-annual-base-plan">Adding the free trial for the annual base plan</h3>
<ol>
<li><p>From the base plan, go back to the subscription page</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F70ad8150-afa0-47ef-8dbd-0d0dc10c85f8%2FUntitled.png?table=block&amp;id=c22b038e-cc6b-4d2e-b385-98069a6aaa7c&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>In the “<strong>Base plans and offers</strong>” section, click on “<strong>Add offer</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F2b645982-023d-4779-95a0-567adb787573%2FUntitled.png?table=block&amp;id=433a1031-c539-4c17-a365-67904bfe3d44&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose the annual base plan you just created, and click “<strong>Add offer</strong>”</p>
</li>
<li><p>Choose a unique Offer ID, like <code>free-trial-for-annual-subscription</code>.</p>
</li>
<li><p>For “<strong>Eligibility Criteria</strong>”, select “<strong>New customer acquisition</strong>”</p>
</li>
<li><p>Scroll to the “<strong>Phases</strong>” section, and click on “<strong>Add phase</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F6013d045-96b2-4715-84bc-8cd5e2459ce3%2FUntitled.png?table=block&amp;id=1ee909b9-6c1a-431b-b5d6-4bf260273eb5&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose “Free Trial” and 1 week for the duration. Click “<strong>Apply</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F3b31a28d-af45-42c2-bd40-19878b4ec673%2FUntitled.png?table=block&amp;id=72790a38-948f-43aa-b657-0d91f5ea4976&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Finally, click “<strong>Save</strong>”, and “<strong>Activate</strong>” to enable the free trial for the annual plan.</p>
</li>
</ol>
<h2 id="heading-adding-the-monthly-base-plan">Adding the Monthly base plan</h2>
<p>Repeat the steps mentioned above to setup the monthly base plan. Remember to also add the “Offer” for the free trial.</p>
<p>If everything went as expected, your subscription should look like this:</p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F5b7a1b09-0c1d-4cdd-9f75-f87c66775cbb%2FUntitled.png?table=block&amp;id=78d54534-7611-4341-9025-ebe7b829540e&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<h2 id="heading-adding-in-app-products-to-remove-ads">Adding in-app products to Remove Ads</h2>
<ol>
<li><p>Go back to your app dashboard. Scroll to the “<strong>Monetize</strong>” section in the sidebar, and click “<strong>In-app purchases</strong>”.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F1c0ab672-8b94-45ad-b7e0-979a7077d5c8%2FUntitled.png?table=block&amp;id=e57d054a-b48f-472a-baf1-10e15a74b53c&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “<strong>Create product</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F9072f38c-e5e1-4f36-92c7-e93aa07c7646%2FUntitled.png?table=block&amp;id=df4eb709-5a1f-4667-8bbe-a046ae9c00c3&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>In the “<strong>Create in-app product</strong>” screen, choose a Product ID like you did for the subscriptions. You can use the following structure: <code>appname_description_price</code></p>
</li>
<li><p>Choose a “<strong>Name</strong>”, “<strong>Description</strong>”, and “<strong>Price</strong>” for the product.</p>
</li>
<li><p>Click “<strong>Save</strong>” and then “<strong>Activate</strong>” to finish.</p>
</li>
</ol>
<p>Congrats! You are done with the product configuration for Android!</p>
<p>The next step is to set up everything on the RevenueCat side of things.</p>
<hr />
<p>Congrats! You are done with the second part!</p>
<h1 id="heading-whats-next">What's next?</h1>
<p>In the next part of the series, I'll guide you through everything you need to setup in RevenueCat to import all the products you just created in both stores.</p>
<p>You can read the next part of the series here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-3">https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-3</a></div>
<p> </p>
<p>Happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Collecting payments with React Native & RevenueCat (Part 1): Project & Apps setup]]></title><description><![CDATA[Introduction
Creating a mobile app involves numerous challenges and time-consuming tasks that must be completed before we can release our app in the stores.
In-app purchases and subscriptions are especially difficult examples of this. They require co...]]></description><link>https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-1</link><guid isPermaLink="true">https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-1</guid><category><![CDATA[RevenueCat]]></category><category><![CDATA[React Native]]></category><category><![CDATA[Expo]]></category><category><![CDATA[payment]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Thu, 22 Feb 2024 23:37:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1708381961749/0d9a1660-d18d-434b-8826-f408535b112c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Creating a mobile app involves numerous challenges and time-consuming tasks that must be completed before we can release our app in the stores.</p>
<p>In-app purchases and subscriptions are especially difficult examples of this. They require completing multiple steps in various locations, such as the Google Cloud Console, Google Play Console, App Store Connect, and more!</p>
<h1 id="heading-revenuecat-to-the-rescue">RevenueCat to the rescue</h1>
<p>RevenueCat is a powerful and reliable in-app purchase server that makes it easy to build, analyze, and grow your subscriber base whether you're just starting out or already have millions of customers. It comes with powerful features such as Entitlements, Products, Offerings, and more.</p>
<p>Even thought RevenueCat has an amazing documentation, it's easier to get lost while navigating through all the different articles. Besides, there are certain details about the process that are not covered in details.</p>
<h1 id="heading-why-should-i-read-this-long-article">Why should I read this long article?</h1>
<p>The goal of this article is to provide a one-stop location with everything you need to know to go from zero to a working app that displays all your products and allow users to purchase items or subscribe to your app using both Apple &amp; Google native payments.</p>
<p>I also included a practical example that explains how to setup:</p>
<ul>
<li><p>A monthly subscription with a 7-days free trial.</p>
</li>
<li><p>An annual subscription with a 7-days free trial.</p>
</li>
<li><p>A one-time payment to remove Ads from the app.</p>
</li>
</ul>
<p>Grab a cup of coffee. You'll need it!</p>
<hr />
<h1 id="heading-spirokit-for-saas">SpiroKit for SaaS</h1>
<p>In case you want to save weeks of painful hours of work &amp; research on your next React Native app, checkout SpiroKit for SaaS. It's a starter template that comes with Purchases, Push Notifications, Authentication, Analytics, Error Reporting and more. It also includes a Notion template to guide you through the entire process</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://spirokit.com">https://spirokit.com</a></div>
<p> </p>
<hr />
<h1 id="heading-account-setup">Account setup</h1>
<h2 id="heading-create-a-revenuecat-account-and-your-first-app">Create a RevenueCat account and your first app</h2>
<p>You'll need to create a RevenueCat account in order to proceed with the rest of the tutorial. If you already have a RevenueCat account, feel free to jump to the next section.</p>
<p>Otherwise, visit <a target="_blank" href="https://app.revenuecat.com/signup">https://app.revenuecat.com/signup</a> and create a new account.</p>
<blockquote>
<p>During the signup process, you can choose to add your credit card later. RevenueCat won’t require you to enter your credit card until you have reached $2500/month in revenue.</p>
</blockquote>
<h3 id="heading-first-app-flow">First app flow</h3>
<p>You’ll be ask to create your first app. Choose a name and click on “Create Project” to confirm</p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F1cb558fa-5d4d-479b-9de2-86fcaff67b57%2FUntitled.png?table=block&amp;id=64295f7c-9792-4676-8e42-2d87ec399923&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<h3 id="heading-existing-customer-flow">Existing customer flow</h3>
<p>If you already have at least one app in RevenueCat, you’ll need to create a new project using the top navigation bar:</p>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F7d22ec18-a8eb-4f8e-8bee-be20d510a03a%2FUntitled.png?table=block&amp;id=128799eb-9ab2-4d0f-bfef-895f724d19b8&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<h1 id="heading-creating-a-new-react-native-project">Creating a new React Native project</h1>
<p>Let's start by creating a new Expo app</p>
<pre><code class="lang-bash">npx create-expo-app@latest --template tabs@50
</code></pre>
<h1 id="heading-setup-bundle-identifier-amp-package-name">Setup bundle identifier &amp; package name</h1>
<p>Before you can setup RevenueCat, you'll need to setup a bundle identifier for iOS, and a package name for Android. You can do that from the <code>App.js</code> file.</p>
<p>It's recommended to use the same identifier for both platforms for simplicity.</p>
<pre><code class="lang-diff">// App.json
{
  "expo": {
    ...
    "ios": {
      ...
<span class="hljs-addition">+     "bundleIdentifier": "com.yourcompany.yourappname"</span>
    },
    "android": {
      ...
<span class="hljs-addition">+     "package": "com.yourcompany.yourappname"</span>
    },
    ...
  }
}
</code></pre>
<h1 id="heading-setup-environment-variables-for-revenuecat-platforms">Setup environment variables for RevenueCat platforms</h1>
<p>You'll need to create a new <code>.env</code> file to include the API keys for both Android &amp; iOS platforms on RevenueCat.</p>
<pre><code class="lang-bash">touch .env
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">//.env</span>
EXPO_PUBLIC_REVENUECAT_API_KEY_ANDROID=<span class="hljs-string">"[YOUR_EXPO_PUBLIC_REVENUECAT_API_KEY_ANDROID]"</span>
EXPO_PUBLIC_REVENUECAT_API_KEY_IOS=<span class="hljs-string">"[YOUR_EXPO_PUBLIC_REVENUECAT_API_KEY_IOS]"</span>
</code></pre>
<p>We'll come back later to this file to replace the placeholder with the real API keys.</p>
<h1 id="heading-adding-missing-dependencies">Adding missing dependencies</h1>
<p>In order to use the RevenueCat SDK on your React Native app, you'll need to install the <code>react-native-purchases</code> by running the following command:</p>
<pre><code class="lang-bash">npx expo install react-native-purchases
</code></pre>
<hr />
<h1 id="heading-setting-up-your-android-and-ios-apps-or-in-revenuecat"><strong>Setting up your Android and iOS “Apps” or in RevenueCat</strong></h1>
<h2 id="heading-creating-your-play-service-credentials-android">Creating your Play service credentials (Android)</h2>
<p>In order for RevenueCat's servers to communicate with Google on your behalf, you need to provide a set of service credentials.</p>
<blockquote>
<p>Note that this setup takes place on both the Google Play Console and the Google Cloud Console. Due to the nature of the process, there’s some switching back and forth between each console that can’t be helped, but each step will make clear which console you should be looking at.</p>
</blockquote>
<h3 id="heading-creating-a-project-in-google-cloud-platform"><strong>Creating a project in Google Cloud Platform</strong></h3>
<p>Before we can use Google as a social provider in your Expo app, it is essential to create a project in Google Cloud Platform. To set up your project, follow these steps:</p>
<ol>
<li><p>Visit <a target="_blank" href="http://cloud.google.com">cloud.google.com</a><a target="_blank" href="http://cloud.google.com/">and sign in.</a></p>
</li>
<li><p>Click on the dropdown at the top left corner.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fb2cb3925-1536-4380-bd22-d317afb5d85d%2FUntitled.png?table=block&amp;id=cf63baff-cf8e-4c72-8a5e-d457496cb52a&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on "<strong>New Project</strong>" at the top right corner.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fff8008d3-21b6-4dd4-a497-d2ec089da411%2FUntitled.png?table=block&amp;id=5602ca74-ef92-40fe-b976-15daf896d3ac&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Fill in the form and click on "<strong>Create</strong>", and wait until the project is created. It could take a few seconds or even minutes.</p>
<blockquote>
<p>Warning: Once you confirm this form, you won’t be able to change your project id.</p>
</blockquote>
</li>
<li><p>Once the project creation is finished, you receive a notification like in the image below. Click “<strong>Select project</strong>” to set your new project as active, and you’ll be redirected to your new project’s dashboard.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F15b3fa14-b5de-4542-9504-0c666d0e62b9%2FUntitled.png?table=block&amp;id=a0b7f256-29ed-4aac-a231-a3d77323888d&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F7de1f427-0880-4e53-b381-7cf30956cc52%2FUntitled.png?table=block&amp;id=de6e1b4b-c65b-4c08-9272-a187b0be3225&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h3 id="heading-enable-the-google-developer-and-reporting-api-google-cloud-console">Enable the Google Developer and Reporting API (Google Cloud Console)</h3>
<ol>
<li><p>Visit your <a target="_blank" href="https://console.cloud.google.com/apis/dashboard">project dashboard</a> in Google Cloud Console.</p>
<ol>
<li>Make sure to use the same project you created while following this guide</li>
</ol>
</li>
<li><p>Search for “Google Play Android”, and select “Google Play Android Developer API”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F4c53690b-d8c8-40af-93bb-5e8ded083bfa%2FUntitled.png?table=block&amp;id=7544fe65-9bdb-4d35-87c8-01ec7d654a15&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “Enable”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F373dbb7c-ab00-41c3-b942-9635476ca743%2FUntitled.png?table=block&amp;id=3566c484-a59a-4ed7-981f-1453e4d3da4e&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Once this is enabled, you will be redirected. Using the same search box, now search for “reporting api”, and select “Google Play Developer Reporting API”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fb13ac80a-7039-4493-8fa5-8a0e057c59be%2FUntitled.png?table=block&amp;id=3ba1b883-e78e-44a1-8c67-6dde195de548&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “Enable”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F0b3dae52-b633-4aa9-a23b-69a769010a4c%2FUntitled.png?table=block&amp;id=78376ab0-d206-469a-9b55-c28fd0090a9c&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h3 id="heading-create-a-service-account-google-cloud-console">Create a Service Account (Google Cloud Console)</h3>
<ol>
<li><p>Using the same search bar we used before, now search for “service accounts” and choose “Service Accounts” from the result list. This is under the “IAM &amp; Admin” section</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F013e3d09-3cfe-4252-97d0-22c136f8cdc7%2FUntitled.png?table=block&amp;id=cbbcca8f-f131-4b28-b632-94f53a45e275&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “Create Service Account”</p>
</li>
<li><p>Choose a name for the account, like “sa for revenue cat”, and click “Create and continue”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F5a9be3cb-9028-44ad-b7a8-dd31196fa2be%2FUntitled.png?table=block&amp;id=5849800b-46c6-4dd0-856e-b0d616ee460c&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>On the step named '<strong>Grant this service account access to project</strong>', you'll add two Roles:</p>
<ul>
<li><p>Pub/Sub Admin - this enables Platform Server Notifications</p>
</li>
<li><p>Monitoring Viewer - this allows monitoring of the notification queue</p>
</li>
</ul>
</li>
</ol>
<p>    <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F1abcb5cd-4e2e-4de4-8a3e-eb4203a7bad2%2FUntitled.png?table=block&amp;id=b51937e1-dc48-48d5-84d9-28994a663c40&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<ol>
<li><p>Click on “Done” to finish (the third step is not required)</p>
</li>
<li><p>You’ll be redirected to the Service Accounts list. Now, you need to click on the Actions dropdown menu on the right, and select “Manage Keys”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F79a616d7-6daa-4244-a7c9-42eb05ee5307%2FUntitled.png?table=block&amp;id=c38edf56-2185-460e-bd67-ae1b24efb5c6&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “Add key” → “Create new key”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F208ad2db-359e-4c6e-bf7f-8b661c217c32%2FUntitled.png?table=block&amp;id=d988655e-3d71-4688-9c11-60cfae4d3550&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Select “JSON”, and click on “Create” to confirm. Make sure to store that file securely.</p>
</li>
<li><p>Finally, copy the email account associated with your new Service Account. You’ll need this email in the next step</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F34b4a91d-3f2c-4a8a-9ee4-a53f0f8b4840%2FUntitled.png?table=block&amp;id=d1426300-6b87-4550-af36-db4ec65484c0&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h3 id="heading-create-a-new-app-in-google-play-console">Create a new app in Google Play Console</h3>
<p>Before you can grant RevenueCat financial access to your app, you’ll need to create a new app in Google Play Console.</p>
<ol>
<li><p>First, we need to login into <a target="_blank" href="https://play.google.com/console/signup"><strong>Google Play Console</strong></a>. If you still don’t have a Google Play Console account, create a new one before proceeding.</p>
</li>
<li><p>In the right section, click on “<strong>Create app</strong>”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F3aa93ce0-83b6-4b9c-986f-6720aaf5c35a%2FUntitled.png?table=block&amp;id=630286cc-b184-4ff2-9b3d-88f746b691d6&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Complete the form, and click “<strong>Create app</strong>” to confirm</p>
</li>
</ol>
<h3 id="heading-grant-financial-access-to-revenuecat-google-play-console">Grant Financial Access to RevenueCat (Google Play Console)</h3>
<ol>
<li><p>In Google Play Console, click on Click on “Users and Permissions” in the sidebar</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fccedd650-c262-4981-88e2-1abf4dfce3f3%2FUntitled.png?table=block&amp;id=47d82caa-df7d-4182-83ac-5d215acc7541&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “Invite new users”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F82754e06-634f-474c-b30a-1cd1c5bee2eb%2FUntitled.png?table=block&amp;id=35498c34-abc2-47f6-b9f6-93966713ccbd&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Paste the email address you got in the previous step after creating the service account. Then click the “Add app” button, and select your app. Click on “Apply”.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fe13fceb2-9225-4367-a91f-e32dc056f241%2FUntitled.png?table=block&amp;id=039822f8-28ae-4868-b146-46fce19d31eb&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Grant <strong>at least</strong> the following permissions:</p>
<ul>
<li><p>“View app information” &amp; “View app quality information”</p>
<p>  <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F6b1de126-c9d8-4999-9782-f6f1cdc606f8%2FUntitled.png?table=block&amp;id=ce9ade90-adcd-45bf-954f-a9508644869a&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>“View Financial data” &amp; “Manage orders and subscriptions”</p>
<p>  <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F81a370d2-904f-47c7-bda1-8731406c7ffe%2FUntitled.png?table=block&amp;id=97a1ace0-040b-4589-8554-f6e79e4c2b2a&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ul>
</li>
</ol>
<blockquote>
<p>Feel free to read through all the available options and check any additional option based on your needs. Those described above are the bare minimum to integrate with RevenueCat.</p>
</blockquote>
<ol>
<li><p>Click “Apply”, then “Invite User”, and finally “Send Invite”.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F178aa6af-878b-46cc-a776-99d0d3538261%2FUntitled.png?table=block&amp;id=907c11df-9a99-4971-9ab2-8b86d4aadcb3&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h3 id="heading-update-the-credentials-json-in-revenuecat">Update the credentials JSON in RevenueCat</h3>
<ol>
<li><p>Visit your RevenueCat dashboard, and go to your Project page.</p>
</li>
<li><p>Select “Play Store - Android”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F521b911f-5c50-423c-bb55-1a76543a5129%2FUntitled.png?table=block&amp;id=e88b61f7-684b-4069-8343-b62be2bd77df&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose a proper name to easily identify your Android app in RevenueCat</p>
</li>
<li><p>In the “Google Play Package” field, use the same package identifier that you set in the <code>app.js</code>, file under <code>app.androidPackage</code></p>
</li>
<li><p>Upload the JSON file you generated for the Service Account before</p>
</li>
<li><p>Click “Save Changes” to finish.</p>
</li>
</ol>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fcfa68ca5-5a10-4d00-b675-2d96f6d7cbc2%2FUntitled.png?table=block&amp;id=d400f92f-d167-46ef-aee5-c1e72489a096&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<blockquote>
<p>The validation process may take up to 36 hours, as described in the RevenueCat documentation <a target="_blank" href="https://www.revenuecat.com/docs/creating-play-service-credentials#check-the-status-of-your-credentials">here</a></p>
</blockquote>
<h3 id="heading-getting-the-api-key">Getting the Api Key</h3>
<ol>
<li><p>In the sidebar, click on “<strong>API Keys</strong>” under the “<strong>Project settings</strong>” section</p>
</li>
<li><p>Click on “show key” and copy the API key.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F8d4c39e7-e331-4374-933a-50712c3c5e4b%2FUntitled.png?table=block&amp;id=440ad79a-2c35-437a-b27e-c33dae4b2989&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Update the <code>.env</code> file in your React Native project, replacing the following line with the Android Key.</p>
<pre><code class="lang-bash"> <span class="hljs-comment">#.env file</span>
 EXPO_PUBLIC_REVENUECAT_API_KEY_ANDROID=<span class="hljs-string">"[YOUR_EXPO_PUBLIC_REVENUECAT_API_KEY_ANDROID]"</span>
</code></pre>
</li>
</ol>
<h3 id="heading-enable-google-real-time-developer-notifications-to-speed-up-the-verification-process">Enable Google Real-Time Developer Notifications to speed up the verification process</h3>
<p>RevenueCat does not require anything further than service credentials to communicate with Google, but setting up real-time server notifications is a recommended process that can speed up webhook and integration delivery times and reduce lag time for Charts.</p>
<p>Follow the instructions described in the <a target="_blank" href="https://www.revenuecat.com/docs/google-server-notifications">official documentation</a></p>
<h3 id="heading-credentials-needs-attention">"Credentials needs attention"</h3>
<p><img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F2414ea7e-22c4-485d-aec3-2ad85323aee7%2FUntitled.png?table=block&amp;id=f87b9ac9-87c9-4049-9c2c-46684a41cab7&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<p>After getting the Android App Id, you’ll probably see a warning message in RevenueCat that says they couldn’t validate permissions to certain APIs. This is normal. In my experience, you’ll need to build and submit a build to the Google Play Console before you can get rid of this warning.</p>
<p>So, don’t waste time with this warning. We’ll get back later to check if the status has chanced.</p>
<h2 id="heading-ios-setup">iOS setup</h2>
<h3 id="heading-eas-build-setup-to-generate-a-bundle-identifier">EAS Build setup to generate a bundle identifier</h3>
<p>Before you can setup iOS, you need to create an app in App Store Connect, and before you can do that, you need to register a bundle identifier. This is usually a tedius process, but the good news is thanks to Expo Application Services, this is really easy. But you'll need to setup EAS in your project first</p>
<ol>
<li><p>First, you’ll need to install <code>eas-cli</code>. It is the command line app you will use to interact with EAS services from your terminal. To install it, run the command:</p>
<pre><code class="lang-bash"> npm i -g eas-cli
</code></pre>
</li>
<li><p>Login into EAS using the eas-cli package:</p>
<pre><code class="lang-bash"> eas login
</code></pre>
</li>
<li><p>Run the following command to generate a new project in Expo.</p>
<blockquote>
<p>Make sure to set the <code>slug</code> property on your <code>app.config.ts</code> file before proceeding, as this will be the name of your project in Expo.</p>
</blockquote>
<pre><code class="lang-bash"> eas build:configure
</code></pre>
</li>
<li><p>Follow the steps described, and make sure to copy the <code>projectId</code> from your terminal. You’ll need to update your <code>app.js</code> file like this:</p>
<pre><code class="lang-diff"> {
   ...
   extra: {
 +   projectId: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
   }
 }
</code></pre>
</li>
</ol>
<h3 id="heading-register-your-bundle-identifier-using-eas-ios">Register your Bundle Identifier using EAS (iOS)</h3>
<p>Before you can continue configuring RevenueCat, you’ll need to register your Bundle Identifier for iOS. The easiest way to do this is using EAS build. However, you don’t need to go through the entire build process, mainly because you are still missing the RevenueCat App Id for iOS. To avoid unnecesary builds, follow these steps:</p>
<ol>
<li><p>Run the following command to start the build process</p>
<pre><code class="lang-bash"> eas build --profile development --platform ios
</code></pre>
</li>
<li><p>Enter your iOS credentials to sign in.</p>
</li>
<li><p>After a successful login, you should see a message that says that your bundle identifier was registered, like this</p>
<p> <img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F58721214-56fb-4df4-84c3-16c2d69fc10f%2FUntitled.png?table=block&amp;id=4457db5e-f2c5-4b57-ac6b-f2cc1d04b96f&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=209bbd02-fd61-4923-9006-2c80c80afeaf&amp;cache=v2" alt /></p>
</li>
<li><p>Feel free to cancel the build process. You’ll go back later after getting the RevenueCat App Id.</p>
</li>
</ol>
<h3 id="heading-create-your-ios-app-on-app-store-connect">Create your iOS app on App Store Connect</h3>
<p>With the Bundle Identifier registered, you can now create a new App in the <strong>App Store Connect</strong> and select the registered Bundle Identifier.</p>
<ol>
<li><p>Go to App Store Connect</p>
</li>
<li><p>Click on “<strong>My Apps</strong>”</p>
</li>
<li><p>Click on the “<strong>+</strong>” button, and the select “<strong>New App</strong>”</p>
<p> <img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F00c13229-4c66-47ba-8c63-ab9d269bd9e1%2FUntitled.png?table=block&amp;id=7e4245fe-347b-46e8-bec5-0f83f623ce5e&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=209bbd02-fd61-4923-9006-2c80c80afeaf&amp;cache=v2" alt /></p>
</li>
<li><p>Choose a name, and make sure to select the right bundle identifier.</p>
</li>
<li><p>Click on “Create” to confirm</p>
</li>
</ol>
<h3 id="heading-setup-app-store-connect-shared-secret-ios">Setup App Store Connect Shared Secret (iOS)</h3>
<p>With your new app created in App Store Connect, follow these steps to get the Shared Secret.</p>
<ol>
<li><p>Go to <a target="_blank" href="https://appstoreconnect.apple.com/">App Store Connect</a></p>
</li>
<li><p>Navigate to “My Apps” and select your app.</p>
</li>
<li><p>Select "App Information" under the "General" section from the left side menu</p>
</li>
<li><p>Select "Manage" under the App-Specific Share Secret section from the right side</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F444ab952-a3f9-46e3-8cba-a6644314995b%2FUntitled.png?table=block&amp;id=d63fdeef-04da-48e5-a901-9eb08d5e21fc&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “Generate”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F51d5b1a2-e8b9-4e9e-af4f-08149ffe1ee6%2FUntitled.png?table=block&amp;id=f85afa85-2cda-4b82-b660-461925217196&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Copy your shared secret and save it safely. You’ll need to paste it in RevenueCat later.</p>
</li>
</ol>
<h3 id="heading-in-app-purchase-key-configuration-ios">In-App Purchase Key configuration (iOS)</h3>
<p>For RevenueCat to securely validate purchases through Apple Store Kit 2, authenticate and validate <a target="_blank" href="https://docs.revenuecat.com/docs/ios-subscription-offers">Subscription Offers</a> requests with Apple and enable <a target="_blank" href="https://docs.revenuecat.com/docs/customer-lists#find-an-individual-customer">customer lookup</a> via Order ID for iOS apps, you need to upload an <strong>in-app purchase key</strong>, and you'll also need to provide an <strong>Issuer ID.</strong></p>
<p>In-app purchase keys are generated in the <strong><em>Users and Access</em></strong> section of App Store Connect. You can use the same in-app purchase key for all of your apps.</p>
<ol>
<li><p>Go to <a target="_blank" href="https://appstoreconnect.apple.com/">App Store Connect</a></p>
</li>
<li><p>Click on “Users and Access”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F4f59f34b-d81e-466a-9237-f205fca15271%2FUntitled.png?table=block&amp;id=1639b8cc-221e-4fb3-bf67-c5eeb0ce9f63&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click the “Keys” tab, and then the “In-App Purchase” option in the sidebar. Finally, click the “Generate In-App Purchase Key” button</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fcf7125f4-c22d-4dea-b864-660847229ce6%2FUntitled.png?table=block&amp;id=56e050b9-e885-4819-92d3-1c55c30e803d&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose a name, and click “Generate” to confirm</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F24d473ea-b42e-4331-b9fb-0fbf0eeab80d%2FUntitled.png?table=block&amp;id=ccad32a1-f67c-4cb2-a683-f56a7efecced&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Once your key is generated, it will appear in <em>Active Keys</em> and you get one shot to download it. Select “<strong>Download In-App Purchase Key</strong>” and store the file in a safe place, you'll need to upload this to RevenueCat in the next step.</p>
<blockquote>
<p>You can only download an individual API key once. Prepare to copy and save your key in a safe place that you can refer to later on.</p>
</blockquote>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fd1ef3e8b-df9e-4d12-a687-2992a3793768%2FUntitled.png?table=block&amp;id=48aebab2-d748-4ee7-b742-f34674ed2031&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<h3 id="heading-setup-ios-app-in-revenuecat">Setup iOS App in RevenueCat</h3>
<ol>
<li><p>Visit your RevenueCat dashboard, and go to your Project page.</p>
</li>
<li><p>In the top right corner, click on “New”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Fe1822b99-4fff-40c6-9cc2-bc9920a32f77%2FUntitled.png?table=block&amp;id=c9a5a14e-3ebc-4e81-8c3a-8cde08a72910&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Select “App Store”</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F8bd277e2-3058-43e2-9132-e4dc42fc914e%2FUntitled.png?table=block&amp;id=b97b3720-155f-4c42-8f25-2b9e81d55e92&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Choose a proper name to easily identify your iOS app in RevenueCat</p>
</li>
<li><p>Copy the “App Bundle ID” name from the <code>app.js</code> file in your project</p>
</li>
<li><p>Upload the <code>.p8</code> file you just created (In-App Purchase Key)</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F9611cc40-9047-45e2-ab3a-2065c8d70503%2FUntitled.png?table=block&amp;id=80be0d31-a68c-4590-9e3a-658f534fa901&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “Set secret” and paste the Shared Secret you generated before.</p>
</li>
<li><p>Make sure to add the <strong>Key ID</strong> and <strong>Issuer ID</strong>. You can get those values from App Store Connect.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F05653b19-e1aa-4de7-8b28-385b48669ed8%2FUntitled.png?table=block&amp;id=348680c1-2d66-415e-be2a-a22a50f73645&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Feef615ff-c7d7-47c2-878d-dbfb6d23ed4c%2FUntitled.png?table=block&amp;id=5d42721b-4882-408c-9cf2-0c356b63913b&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Click “Save Changes” to finish.</p>
</li>
</ol>
<h3 id="heading-getting-the-api-key-1">Getting the Api Key</h3>
<ol>
<li><p>In the sidebar, click on “<strong>API Keys</strong>” under the “<strong>Project settings</strong>” section</p>
</li>
<li><p>Click on “show key” and copy the API key.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F21ef5283-e1b3-4fd0-8d10-a3e1207eca00%2FUntitled.png?table=block&amp;id=878daf05-55c2-4a07-b3f5-503396550e10&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Update the <code>.env</code> file in your app with the new api key</p>
<pre><code class="lang-bash"> <span class="hljs-comment">#.env file</span>
 EXPO_PUBLIC_REVENUECAT_API_KEY_IOS=<span class="hljs-string">"[YOUR_EXPO_PUBLIC_REVENUECAT_API_KEY_IOS]"</span>
</code></pre>
</li>
</ol>
<h1 id="heading-building-your-app-with-eas">Building your app with EAS</h1>
<p><code>react-native-purchases</code> is a native module, which means you won't be able to test your app using Expo Go on your phone. You'll need to use a <a target="_blank" href="https://docs.expo.dev/develop/development-builds/introduction/">development client</a></p>
<p>But first, you'll to run the following commands:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install EAS globally</span>
npm i -g eas-cli

<span class="hljs-comment"># Run initial configuration</span>
eas build:configure
</code></pre>
<p>This will guide you through the EAS configuration and will create a <code>eas.json</code> file with two profiles: development (used for the dev client) and production (used for to publish to the stores)</p>
<h1 id="heading-building-amp-submitting-your-android-app">Building &amp; Submitting your Android app</h1>
<ol>
<li><p>Now that you have both keys generated, you'll need to build and submit a first version of your app to the Google Play Console.</p>
</li>
<li><p>To generate your first build, run the following command:</p>
<pre><code class="lang-bash"> eas build --platform android --profile production
</code></pre>
</li>
<li><p>You'll get a link to track the progress of your build on EAS. Once it's done, go to <a target="_blank" href="http://expo.dev">expo.dev</a>, then select your project, and go the “Builds” section in the sidebar.</p>
</li>
<li><p>Download the latest Android production build.</p>
</li>
<li><p>Go back to <strong>Google Play Console</strong>, and select your app.</p>
</li>
<li><p>In the sidebar, click on “<strong>Testing</strong>” → “<strong>Internal testing</strong>”</p>
<p> <img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2Ff2c5fbda-6e0a-4706-a432-0bb6feaee297%2FUntitled.png?table=block&amp;id=5f55c27f-6596-4b4f-9ba9-9ace60a2d6e4&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=209bbd02-fd61-4923-9006-2c80c80afeaf&amp;cache=v2" alt /></p>
</li>
<li><p>Click on “<strong>Create new release</strong>”</p>
</li>
<li><p>Upload your app bundle, and add the requested information to create your first release.</p>
</li>
</ol>
<h2 id="heading-checking-permission-status-for-the-apis-android">Checking permission status for the APIs (Android)</h2>
<p>Once you have submitted your first internal build to the Google Play Console, it’s time to go back to RevenueCat and make sure all the permissions to the different APIs are valid.</p>
<ol>
<li><p>Go back to your RevenueCat dashboard, choose your app, and select the Android App.</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F4870f37d-9f19-475c-9dd6-28624a2fd4b9%2FUntitled.png?table=block&amp;id=2018b164-c659-45fa-96c1-66a3b2158274&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
<li><p>Below the “Service Account Credentials JSON” file uploader, Make sure you see a label that says “Valid credentials” and everything is checked like this</p>
<p> <img src="https://spirokit.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F05a3c4f2-44f2-4044-93ee-d94545b507c8%2F9acacc74-4e7f-47f0-86da-9e6af24b3c87%2FUntitled.png?table=block&amp;id=68224738-02f0-4fa1-8148-ff9c74b06f50&amp;spaceId=05a3c4f2-44f2-4044-93ee-d94545b507c8&amp;width=2000&amp;userId=&amp;cache=v2" alt /></p>
</li>
</ol>
<p>Follow the troubleshoot if you have any issue. More information <a target="_blank" href="https://www.revenuecat.com/docs/creating-play-service-credentials#check-the-status-of-your-credentials">here</a></p>
<p>Congrats! You are done with the first part! I know it's tedious, but it will worth the pain in the end!</p>
<h1 id="heading-whats-next">What's next?</h1>
<p>In the next part of the series, I'll guide you through everything you need to setup in the stores to enable in-app purchases &amp; subscriptions.</p>
<p>You can read the next part of the series here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-2">https://blog.spirokit.com/collecting-payments-with-react-native-revenuecat-part-2</a></div>
<p> </p>
<p>Happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Best React Native UI Kits (February - 2024)]]></title><description><![CDATA[Building apps take time
Building a mobile app is a challenging task. From coming up with an idea, prototyping, and finally building the app, there are many things to consider and problems we'll have to figure out:

How do we make sure the entire app ...]]></description><link>https://blog.spirokit.com/best-react-native-ui-kits</link><guid isPermaLink="true">https://blog.spirokit.com/best-react-native-ui-kits</guid><category><![CDATA[React Native]]></category><category><![CDATA[React]]></category><category><![CDATA[UI]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[Mobile apps]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Sun, 04 Feb 2024 03:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1707164321708/cdf9120e-1afd-4d00-9e81-74f19c8d3608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-building-apps-take-time">Building apps take time</h1>
<p>Building a mobile app is a challenging task. From coming up with an idea, prototyping, and finally building the app, there are many things to consider and problems we'll have to figure out:</p>
<ul>
<li><p>How do we make sure the entire app looks consistent?</p>
</li>
<li><p>Do we need to support dark mode?</p>
</li>
<li><p>Should the app feel like an iOS or an Android app? Maybe something in between.</p>
</li>
<li><p>Is our app accessible and inclusive?</p>
</li>
</ul>
<p>If you are building everything from scratch, you may need years of design experience to create something that looks consistent and, at the same time, has a clear answer to all the questions we mentioned early. On the other hand, building an accessible app with dark mode support and a cohesive look and feel takes a lot of time.</p>
<h1 id="heading-react-native-ui-kits">React Native UI Kits</h1>
<p>We already established that building a mobile app from scratch is challenging and takes time. The good news is that there's a solution for that problem: We can use a React Native UI Kit.</p>
<p>A UI kit is a collection of assets containing design elements such as UI components, styles, and rules. UI components are elements that convey meaning and provide functionality to users.</p>
<h2 id="heading-why-should-we-use-a-react-native-ui-kit">Why should we use a React Native UI Kit?</h2>
<p>UI kits can help you improve a design and development workflow in many ways:</p>
<ul>
<li><p>Speed up the design and build processes: We can rely on ready-to-use UI elements from the kit instead of creating our own elements.</p>
</li>
<li><p>Time to market: We can ship faster and test our ideas by drastically reducing the design and development phases.</p>
</li>
<li><p>Don't reinvent the wheel: As developers and entrepreneurs, we aim to solve a critical problem for our users. Therefore, we should focus our time and energy on solving that problem instead of creating a custom interface.</p>
</li>
<li><p>Achieve consistency in your app: The design elements from a UI kit have a consistency that can be hard to achieve if you are building everything from scratch.</p>
</li>
<li><p>Become a better designer: UI kits allow us to inspect the different elements and learn from them.</p>
</li>
</ul>
<h1 id="heading-best-react-native-ui-kits">Best React Native UI Kits</h1>
<p>Here's a list of the best React Native UI kits you can use to build your next app.</p>
<h2 id="heading-spirokit">SpiroKit</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693066366774/9be83d89-fce7-48ac-ac63-070c04fef72e.png" alt class="image--center mx-auto" /></p>
<p>A library with more than two years in the making. SpiroKit is not only a UI Kit but a complete toolkit for indie hackers that includes: Figma templates, UI Kit, Boilerplates &amp; App templates.</p>
<p>It comes with 17 ready-to-use color palettes combined with three shades of gray (warm, neutral, and cool). Besides, it includes a global ThemeProvider that allows you to add your own color palette, custom fonts, and much more!</p>
<p>Every component comes with dark mode support, meaning that you don't need to do anything besides implementing a simple toggle for your users.</p>
<p>SpiroKit also includes a complete documentation portal built with StorybookJS, where you can copy-paste hundreds of working code examples.</p>
<p>This is a paid product, but we support Purchase Power Parity, so if you are interested, check out the landing page to see if you are eligible for a discount.</p>
<p>If you are interested on building a SaaS, We recently announced the release of SpiroKit for SaaS, a React Native starter kit specially designed for SaaS, complemented by a companion management system built on top of Notion.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699575033109/1c804f63-bf6a-420d-be65-62a0bc5c1015.png?w=1600&amp;h=840&amp;fit=crop&amp;crop=entropy&amp;auto=compress,format&amp;format=webp" alt="Announcing SpiroKit for SaaS" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707163782713/b21cf012-d691-4e01-95f2-e4986163403c.png" alt class="image--center mx-auto" /></p>
<p>It comes with Social Authentication, Profile management, Analytics, Push Notifications, In-app Purchases &amp; Subscriptions, Error Reporting, Internationalization, and more!</p>
<h3 id="heading-resources-amp-stats">Resources &amp; stats</h3>
<ul>
<li><p>Links:</p>
<ul>
<li><p><a target="_blank" href="https://spirokit.com">SpiroKit for SaaS - Website</a></p>
</li>
<li><p><a target="_blank" href="https://ui.spirokit.com">SpiroKit UI Kit - Website</a></p>
</li>
<li><p><a target="_blank" href="https://docs.spirokit.com">Storybook Docs</a></p>
</li>
<li><p><a target="_blank" href="https://blog.spirokit.com">Blog</a></p>
</li>
</ul>
</li>
</ul>
<hr />
<h2 id="heading-react-native-elements">React Native Elements</h2>
<p><img src="https://i.imgur.com/27SxVji.png" alt="react native elements - ui kit - banner" /></p>
<p>This is easily one of the more popular options available. It's open source and has its own VSCode Extension for snippets with preview and auto import. About customization, it includes a global <code>ThemeProvider</code> that you can use to setup your own theme from scratch.</p>
<h3 id="heading-resources-amp-stats-1">Resources &amp; stats</h3>
<ul>
<li><p>24k stars on GitHub</p>
</li>
<li><p>Links:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/react-native-elements/react-native-elements">GitHub</a></p>
</li>
<li><p><a target="_blank" href="https://reactnativeelements.com/">Website</a></p>
</li>
</ul>
</li>
</ul>
<hr />
<h2 id="heading-gluestack-ui">Gluestack UI</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693062888469/dda9b467-ee9a-4d93-b7fc-e5226de828f1.png" alt class="image--center mx-auto" /></p>
<p>Gluestack UI is a new headless UI kit that strongly focuses on accessibility and customization. It was built by the same team behind NativeBase, but it's a complete rewrite.</p>
<p>Gluestack UI is just a part of a brother ecosystem that also includes:</p>
<ul>
<li><p>gluestack-styles: Universal styling library</p>
</li>
<li><p>DSX: A design system creator tool (not available yet)</p>
</li>
<li><p>Builder: A low code tool to build apps (not available yet)</p>
</li>
</ul>
<h3 id="heading-resources-amp-stats-2">Resources &amp; stats</h3>
<ul>
<li><p>1.3k stars on GitHub</p>
</li>
<li><p>Links:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/gluestack/gluestack-ui">GitHub</a></p>
</li>
<li><p><a target="_blank" href="https://ui.gluestack.io/">Website</a></p>
</li>
<li><p><a target="_blank" href="https://ui.gluestack.io/docs/getting-started/installation">Docs</a></p>
</li>
</ul>
</li>
</ul>
<hr />
<h2 id="heading-react-native-paper">React Native Paper</h2>
<p><img src="https://i.imgur.com/L7XiCyF.png" alt="react native paper - ui kit - banner" /></p>
<p>Another popular kit. It's cross-platform but based on Google's Material Design guidelines. It has a default theme, but you can implement your own using the global ThemeProvider.</p>
<p>It also includes a documentation portal with all you need to setup your project.</p>
<p>One thing to notice is that this kit doesn't have app boilerplates, so you must go through the installation process to add this UI kit to your projects.</p>
<h3 id="heading-resources-amp-stats-3">Resources &amp; stats</h3>
<ul>
<li><p>11.9k stars on GitHub</p>
</li>
<li><p>Links:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/callstack/react-native-paper">GitHub</a></p>
</li>
<li><p><a target="_blank" href="https://reactnativepaper.com/">Website</a></p>
</li>
</ul>
</li>
</ul>
<hr />
<h2 id="heading-ui-kitten">UI Kitten</h2>
<p><img src="https://i.imgur.com/huI7ibj.png" alt="ui kitten - react native ui kit - banner" /></p>
<p>A beautiful cross-platform UI Kit based on <a target="_blank" href="https://eva.design/">Eva Design System</a>.</p>
<p>It also includes:</p>
<ul>
<li><p>Complete documentation portal with examples to copy/paste</p>
</li>
<li><p>App boilerplates</p>
</li>
<li><p>ThemeProvider and light/dark mode support</p>
</li>
<li><p>You can build your own themes.</p>
</li>
<li><p>It has its own icon pack with 480 icons (based on Eva Design System).</p>
</li>
</ul>
<h3 id="heading-resources-amp-stats-4">Resources &amp; stats</h3>
<ul>
<li><p>10k stars on GitHub</p>
</li>
<li><p>Links:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/akveo/react-native-ui-kitten">GitHub</a></p>
</li>
<li><p><a target="_blank" href="https://akveo.github.io/react-native-ui-kitten/">Website</a></p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-tamagui">Tamagui</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693064149483/6661e9c1-3739-411f-b361-fb2011c107fd.png" alt class="image--center mx-auto" /></p>
<p>A style system, UI kit, and optimizing compiler for React Native &amp; Web</p>
<p>A cross-platform UI Kit for Universal apps with a fantastic innovation: It's also a compiler that optimizes the output on build time. If you are familiar with Svelte, this is similar, but for universal apps with React Native.</p>
<p>This kit also has a core package (@tamagui/core), which is a good option if you want to build your own design system with a solid foundation.</p>
<p>It includes excellent documentation, and the team behind this project is moving fast!</p>
<h3 id="heading-resources-amp-stats-5">Resources &amp; stats</h3>
<ul>
<li><p>9.3k stars on GitHub</p>
</li>
<li><p>Links:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/tamagui/tamagui">GitHub</a></p>
</li>
<li><p><a target="_blank" href="https://tamagui.dev/">Website</a></p>
</li>
<li><p><a target="_blank" href="https://tamagui.dev/docs/intro/introduction">Docs</a></p>
</li>
</ul>
</li>
</ul>
<h1 id="heading-conclusions">Conclusions</h1>
<p>The React Native ecosystem is constantly evolving, with many unique projects like those mentioned above.</p>
<p>Here are a few questions you should ask yourself before choosing the best option for you:</p>
<ul>
<li><p>Do I need complete control to customize every component?</p>
</li>
<li><p>How much time do I have to build my next app?</p>
</li>
<li><p>Do I want to build my own theme / define my color palette?</p>
</li>
<li><p>Is it easy to implement? Does it include good documentation?</p>
</li>
</ul>
<p>Based on your answers, you'll find the best solution for your specific situation.</p>
]]></content:encoded></item><item><title><![CDATA[Announcing SpiroKit for SaaS]]></title><description><![CDATA[Building a Mobile SaaS from scratch with React Native can be time-consuming and complex. From setting up analytics, payments, and error reporting to building an authentication flow or setting up push notifications, there's a lot to be done, and integ...]]></description><link>https://blog.spirokit.com/announcing-spirokit-for-saas</link><guid isPermaLink="true">https://blog.spirokit.com/announcing-spirokit-for-saas</guid><category><![CDATA[Expo]]></category><category><![CDATA[React Native]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[template]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Fri, 10 Nov 2023 10:53:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699575033109/1c804f63-bf6a-420d-be65-62a0bc5c1015.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building a Mobile SaaS from scratch with React Native can be time-consuming and complex. From setting up analytics, payments, and error reporting to building an authentication flow or setting up push notifications, there's a lot to be done, and integrating different services can feel overwhelming without a clear process from start to finish.</p>
<p>I've tackled this challenge many times over the last few years while working on client projects, and today, I'm excited to share some of the lessons I've learned along the way.</p>
<p>I'm pleased to announce that I've been working on a new React Native starter kit.</p>
<hr />
<h1 id="heading-spirokit-for-saas">SpiroKit for SaaS</h1>
<p>SpiroKit for SaaS is a React Native starter kit specially designed for SaaS, complemented by a companion management system built on top of Notion.</p>
<p><img src="https://www.spirokit.com/saas-screenshots.webp" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707161540849/50b31941-c8d1-4754-a94b-cffe20cb1772.png" alt /></p>
<h2 id="heading-how-does-it-work">How does it work?</h2>
<p>When you're ready to kick off a new project, start by cloning our Notion Management System. This system includes a dashboard with step-by-step instructions to guide you through the entire development process.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707161575919/41d8de6c-32c9-4f72-8d3f-24e634462fd9.png" alt class="image--center mx-auto" /></p>
<p>From creating a new project with the starter template to setting up features like push notifications, authentication, social login, in-app purchases, error reporting, localization, etc., to building and submitting your app to the store, we've got you covered.</p>
<p>Each module or feature in the starter kit comes with hooks, providers, or configuration files to streamline your development process. Everything that can be automated is included with the starter kit, and any manual steps are documented and added to an ordered task list in Notion.</p>
<p>Keep track of your progress by marking tasks as "Done" in Notion, and feel free to add custom tasks tailored to each project.</p>
<h1 id="heading-features">Features</h1>
<ul>
<li><p><strong>Analytics with Vexo</strong></p>
<ul>
<li>The starter kit sets up everything for basic tracking of your app usage. Additionally, we'll provide step-by-step guides on setting up Vexo Analytics and examples of how to track custom events.</li>
</ul>
</li>
<li><p><strong>In-app and subscriptions with RevenueCat</strong></p>
<ul>
<li><p>The kit comes with a fully functional integration with RevenueCat, a trusted solution for managing in-app subscriptions and purchases.</p>
</li>
<li><p>This includes a working example and paywall components, allowing you to present your subscription plans in minutes.</p>
</li>
<li><p>Our Notion system will guide you through setting up your RevenueCat project, subscriptions in the stores, and configuring products and entitlements in RevenueCat.</p>
</li>
</ul>
</li>
<li><p><strong>Authentication with Supabase</strong></p>
<ul>
<li><p>We'll integrate Supabase with our UI kit to provide an outstanding authentication experience out of the box. This includes sign-in, sign-up with a password, social login with Google and Apple, magic links, forgot password, change password, user profile, and more.</p>
</li>
<li><p>We'll also include all the required documentation to set up different social providers using Supabase.</p>
</li>
</ul>
</li>
<li><p><strong>Error reporting with Sentry</strong></p>
<ul>
<li><p>The starter kit comes with Sentry configured, a React Error Boundary, and a fallback UI to gracefully present unexpected errors.</p>
</li>
<li><p>We've also included useful screens like "Connection Lost" or "Sign in to continue," so you don't need to build everything from scratch.</p>
</li>
<li><p>Our Notion system will guide you through setting up Sentry and obtaining the relevant keys.</p>
</li>
</ul>
</li>
<li><p><strong>Push notifications with OneSignal</strong></p>
<ul>
<li><p>OneSignal is ready to use in the starter kit, abstracting the process of asking for relevant permissions from users into a convenient hook you can invoke on your home screen.</p>
</li>
<li><p>Relevant documentation and tasks in Notion will guide you through obtaining API keys, setting up in-app messages, etc.</p>
</li>
</ul>
</li>
<li><p><strong>Internationalization with react-i18next</strong></p>
<ul>
<li><p>Our kit comes with a working setup for internationalization with react-i18next. This includes a resource file for English and Spanish, allowing you to add your localization keys.</p>
</li>
<li><p>We also included a section in the user profile screen to allow users to switch languages based on their preferences. This preference is persisted on Supabase, so the next time your user opens the app, this preference will be remembered.</p>
</li>
</ul>
</li>
<li><p><strong>Build, update and submit with EAS (Expo Application Services)</strong></p>
<ul>
<li><p>Our starter comes with an <code>eas.json</code> file ready to fill with your api keys.</p>
</li>
<li><p>Documentation is included so you can set up your channels and branches with EAS updates, enabling you to ship updates instantly without dealing with store reviews.</p>
</li>
<li><p>Our Notion system will also include documentation and tasks covering the entire process, from your first development client build to your first over-the-air update and submission to the stores.</p>
</li>
</ul>
</li>
<li><p><strong>Figma Asset Kit for the stores</strong></p>
<ul>
<li><p>If you've published an app before, you know how challenging it can be to gather all the required information about the size of each asset for each store.</p>
</li>
<li><p>Our kit includes a convenient Figma Kit for a fake app that includes all the necessary assets you'll need</p>
</li>
</ul>
</li>
</ul>
<p>If you want to learn more about this template or get your license, <a target="_blank" href="https://saas.spirokit.com">checkout the new landing page</a> we built for it.</p>
<blockquote>
<p>If you already have a SpiroKit license and want to purchase this starter, feel free to reach-out at <a target="_blank" href="http://mailto:mauro@spirokit.com">mauro@spirokit.com</a></p>
</blockquote>
<hr />
<h1 id="heading-feedback-is-appreciated">Feedback is appreciated</h1>
<p>I would love to hear your thoughts on this Notion + Starter combo. Are there any important elements I might be missing? Did I get something wrong?</p>
<p>Let me know in the comments.</p>
<p>Happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Building a news feed assistant with Supabase, OpenAI, and SpiroKit]]></title><description><![CDATA[Introduction
As devs, we try to stay informed about the latest tech news on an almost daily basis. This can be challenging, considering how fast our world moves and how much information is published on the internet all the time.
RSS feed apps are gre...]]></description><link>https://blog.spirokit.com/building-a-news-feed-assistant-with-supabase-openai-and-spirokit</link><guid isPermaLink="true">https://blog.spirokit.com/building-a-news-feed-assistant-with-supabase-openai-and-spirokit</guid><category><![CDATA[React Native]]></category><category><![CDATA[supabase]]></category><category><![CDATA[AI]]></category><category><![CDATA[Expo]]></category><category><![CDATA[openai]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Tue, 10 Oct 2023 04:19:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696909680106/483b393c-d3df-4968-8501-26a76692b826.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>As devs, we try to stay informed about the latest tech news on an almost daily basis. This can be challenging, considering how fast our world moves and how much information is published on the internet all the time.</p>
<p>RSS feed apps are great for keeping a curated source of news, including your favorite topics and authors. But what if we could enhance that experience even further with an assistant who can use your curated feed to answer any questions and help you find the articles you really want to read?</p>
<p>In this post, we’ll go through the process of building a mobile RSS news feed with an AI assistant, using React Native, Supabase, and the OpenAI API.</p>
<p>The assistant will read your feed and answer your questions about it. It’ll also provide references to the original sources so you can keep reading the full article and dive deeper into the subjects you want to prioritize.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696910972446/1add9bf8-1471-4869-bd64-4015c97e30bb.gif" alt class="image--center mx-auto" /></p>
<blockquote>
<p>This project was built in collaboration with <a class="user-mention" href="https://hashnode.com/@paulasantamaria">Paula Santamaría</a>, who took care of the backend side.</p>
</blockquote>
<hr />
<h1 id="heading-stack">Stack</h1>
<ul>
<li><p>React Native: React Native is a JavaScript framework for writing real, natively rendering mobile applications for iOS and Android.</p>
</li>
<li><p>Expo: Expo is an open-source platform for making universal native apps. It aims to simplify the development process by providing a set of tools, libraries, and services.</p>
</li>
<li><p><a target="_blank" href="https://spirokit.com">SpiroKit</a> is a React Native toolkit, including a UI kit, interactive documentation, and tons of starter templates.</p>
</li>
<li><p><a target="_blank" href="https://supabase.com">Supabase</a>: Supabase is an open-source Firebase alternative with a ton of features. Here are some we used in this project:</p>
<ul>
<li><p>PostgreSQL: The database provided by Supabase. In our case, it was used to store the Feed, embeddings, and all the data required by the application.</p>
</li>
<li><p>JS Client Library: We used it to interact with the database and other Supabase features. It also has TypeScript support!</p>
</li>
<li><p>pgvector extension: <code>pgvector</code> is an open-source PostgreSQL extension that allows you to store embeddings and provides vector similarity search.</p>
</li>
<li><p>Edge Functions: We delegated the data transformations and processing to Edge Functions. They are essentially TypeScript functions powered by Deno that run on the server and are globally distributed.</p>
</li>
<li><p>DB Functions: Ideal for handling data-intensive operations, such as similarity search between vectors.</p>
</li>
<li><p>Migrations: We generated migrations that took care of creating tables and database functions.</p>
</li>
<li><p>CLI: It helped us generate and apply migrations and deploy edge functions directly from the terminal.</p>
</li>
</ul>
</li>
<li><p>OpenAI API: This API takes care of generating embeddings based on the content from the feed and the user input. It also generates chat completions from a prompt we built, combining the user input and feed context.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696910200886/3872c286-f191-4cea-aa0d-d8bdc5045b84.png" alt class="image--center mx-auto" /></p>
<hr />
<h1 id="heading-building-the-app">Building the app</h1>
<p>To start working on the React Native app, we needed to create a new project. In this case, we decided to use SpiroKit as our UI Kit and bootstrap a new project using SpiroKit’s “Expo - Supabase Starter” template.</p>
<p>This template comes with Expo SDK 49 + Expo Router v2 + TypeScript configured. It also includes a Supabase context wrapping the entire app. We can easily access the Supabase client and execute queries using hooks.</p>
<h2 id="heading-mobile-app">Mobile app</h2>
<p>We wanted to keep the app simple: A feed with a list of posts to read, a button to ask questions about the feed, and a section to add new sources of information.</p>
<h3 id="heading-feed-route-home">Feed route (Home)</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696910254876/47f43843-333d-4601-a5d4-d5258ce144f9.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>The home screen includes a list of posts from different sources, ordered by publishing date.</p>
</li>
<li><p>It also includes a floating button to ask for information about the feed using AI.</p>
</li>
<li><p>AI will take information from different posts to reply and list all the relevant sources considered for the reply. Each source will navigate to the corresponding post.</p>
</li>
<li><p>There’s a list of predefined prompts you can choose</p>
</li>
<li><p>We used the <code>ActionSheet</code> component from SpiroKit for the “Ask a question” modal.</p>
</li>
</ul>
<h3 id="heading-sources-route">Sources route</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696910307679/79caafbf-613e-4c96-b222-774439a6a4b6.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>The sources tab allows you to manage all your sources of information. You can add new sources by specifying a name, URL, and color to identify the source in your feed.</p>
</li>
<li><p>You can use the “Sync” button below to retrieve all the latest posts for all your sources.</p>
<ul>
<li>Internally, here is where embeddings are processed so you can ask questions about the posts.</li>
</ul>
</li>
<li><p>We used the <code>ActionSheet</code> component from SpiroKit for the “New source” modal.</p>
</li>
</ul>
<h3 id="heading-post-details-route">Post details route</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696910318363/85580787-829e-4340-93d4-b0d037949200.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>When you navigate to a specific post, you’ll see a header with the title and author, followed by the body with the article's content.</p>
</li>
<li><p>We used an amazing library called <code>react-native-render-html</code> to process the HTML and render it as native elements, which is way better than using a WebView.</p>
</li>
<li><p>We reused the same “Ask a question” modal, but we are sending the <code>post-id</code> as additional information to exclude the rest of the posts.</p>
<ul>
<li>We are using the <code>Skeleton</code> component from SpiroKit to present a loading indicator that emulates the expected output.</li>
</ul>
</li>
</ul>
<h3 id="heading-dark-mode-support">Dark mode support</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696910329675/cd33f06a-50c6-4374-bea6-30ad41b51b44.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>We personally use dark mode for almost everything, so we thought it would be cool to adapt the entire app to provide dark mode support.</p>
</li>
<li><p>Because SpiroKit components already come with Dark mode support, and the library includes tons of hooks to define colors for each color mode and switch between modes, this took us 20 minutes.</p>
</li>
</ul>
<h2 id="heading-model">Model</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696910415340/af9b99b1-6457-4ae1-b35a-cdc43045eb96.png" alt class="image--center mx-auto" /></p>
<p>We decided to keep the model simple for this proof of concept. We have a table for <code>sources</code> that stores the blogs the user would like to include in their feed, along with some metadata.</p>
<p>The <code>feed_items</code> table stores the articles found in each source. We only keep articles from the past week, and expired articles are removed during the sync.</p>
<p>Finally, the <code>feed_item_sections</code> table stores the relevant content extracted from each article, which is split into different sections. It also stores the embedding for each section in a vector column provided by <code>pgvector</code>.</p>
<blockquote>
<p>Note: You'll notice that there is no user data in this model. Since this was a simple proof of concept, we decided to leave that out. However, in a production app, that could be a problem 😂.</p>
</blockquote>
<h2 id="heading-feed-sync">Feed Sync</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696910433490/f7286f03-07a5-45fd-8d5c-e7ba8b03f0d1.png" alt class="image--center mx-auto" /></p>
<p>This Edge Function is responsible for synchronizing the Feed Items from the existing Sources and processing them to store the relevant content and embeddings in the <code>feed_item_sections</code> table.</p>
<p>We first get an array of feed items from each source’s RSS feed, using the module <a target="_blank" href="https://github.com/MikaelPorttila/rss">MikaelPorttila/rss</a>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getRssItems = <span class="hljs-keyword">async</span> (url: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;FeedEntry[]&gt; =&gt; {
    <span class="hljs-keyword">const</span> feedResponse = <span class="hljs-keyword">await</span> fetch(url);
    <span class="hljs-keyword">const</span> { entries } = <span class="hljs-keyword">await</span> parseFeed(<span class="hljs-keyword">await</span> feedResponse.text());

    <span class="hljs-keyword">return</span> entries;
};
</code></pre>
<p>Then we process the content of each entry, to extract the relevant content, and create the embeddings:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> embeddingsResponse = <span class="hljs-keyword">await</span> openai.embeddings.create({
    input: content,
    model: <span class="hljs-string">"text-embedding-ada-002"</span>,
});
</code></pre>
<p>Then it’s just a matter of storing everything in the DB:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabaseClient
  .from(<span class="hljs-string">'feed_items'</span>)
  .insert(newFeedItems);

<span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabaseClient
  .from(<span class="hljs-string">'feed_item_sections'</span>)
  .insert(sections);

<span class="hljs-comment">// Remove expired feed items</span>
<span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> client
    .from(<span class="hljs-string">'feed_items'</span>)
    .delete()
    .lt(dateColumn, expirationDate.toISOString());
</code></pre>
<p>Now our <code>feed_items</code> and <code>feed_item_sections</code> tables are updated with the latest content in our feed!</p>
<h2 id="heading-feed-chat">Feed Chat</h2>
<p>This is another critical Edge Function for this project. It’s goal is to provide an AI generated response based on the user input and the relevant context (through context injection).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696910445049/ef0142f2-56ff-4469-92ae-02ac792e1343.png" alt class="image--center mx-auto" /></p>
<p>First step is to find the relevant content in our <code>feed_item_sections</code>. To do that, we need to generate the embedding for the user input and execute a similarity search against the embeddings stored in our database.</p>
<p><code>pgvector</code> will take care of the similarity search. This is the perfect opportunity to generate a DB Function to encapsulate that logic, so we can simply invoke it like this from within our <code>feed_chat</code> Edge function:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// supabase/functions/feed-chat/index.ts</span>

<span class="hljs-keyword">const</span> { data: sections, error } = <span class="hljs-keyword">await</span> supabaseClient.rpc(<span class="hljs-string">"find_sections"</span>, {
    input_embedding: embedding, <span class="hljs-comment">// user embedding to compare</span>
    min_distance: <span class="hljs-number">0.8</span>, <span class="hljs-comment">// Min distance to match</span>
    max_results: <span class="hljs-number">10</span>, <span class="hljs-comment">// Max result to return</span>
    optional_feed_item_id: feedItemId ?? <span class="hljs-literal">null</span> <span class="hljs-comment">// If user is within a specific feed_item</span>
});
</code></pre>
<blockquote>
<p>The <code>find_sections</code> db function is a bit large to include in this post, but take a look at <a target="_blank" href="https://supabase.com/blog/openai-embeddings-postgres-vector">this post</a> if you want to know more about similarity search.</p>
</blockquote>
<p>We also had to make sure to check the number of tokens we included in the prompt, especially in the content sections, since the model we used supported 4097 tokens, max. We used a tokenizer called <a target="_blank" href="https://github.com/botisan-ai/gpt3-tokenizer">gpt3-tokenizer</a> for that.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> { section_content, section_token_count } <span class="hljs-keyword">of</span> sections) {
    tokenCount += section_token_count; <span class="hljs-comment">// Calculated by tokenizer.</span>

    <span class="hljs-keyword">if</span> (tokenCount &gt;= TOKEN_LIMIT) <span class="hljs-keyword">break</span>;

    context += <span class="hljs-string">`<span class="hljs-subst">${section_content.trim()}</span>\\n`</span>
}
</code></pre>
<p>Once we have the relevant sections, we can move on to building a prompt with all the context the AI needs to create an appropriate response:</p>
<ul>
<li><p>User input (what does the user want?)</p>
</li>
<li><p>Relevant content sections (context injected for the AI to use)</p>
</li>
<li><p>You can also include a personality and expected output format</p>
<ul>
<li>E.g: “You’re an assistant that helps users process a news feed effectively”, “Answer with plain text”.</li>
</ul>
</li>
</ul>
<p>Now we can call OpenAI Chat Completions API, and get a completion response based on our prompt:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> generateCompletions = <span class="hljs-keyword">async</span> (prompt: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">const</span> { choices } = <span class="hljs-keyword">await</span> openai.chat.completions.create({
        messages: [{ content: prompt, role: <span class="hljs-string">'user'</span> }],
        model: <span class="hljs-string">'gpt-3.5-turbo'</span>
    });

        <span class="hljs-comment">// it's an array but by default it only has 1 element.</span>
    <span class="hljs-keyword">return</span> choices[<span class="hljs-number">0</span>]?.message.content; 
}
</code></pre>
<blockquote>
<p>We had to run everything through the Moderation API beforehand to ensure that we didn't violate OpenAI's policies.</p>
</blockquote>
<p>Finally, we return the AI response and the Feed Items used as context, so the user can dive deeper into anything they find interesting:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">return</span> {
  chat_response: completion, <span class="hljs-comment">// Response from OpenAI API </span>
  feed_item_references: references 
};
</code></pre>
<h1 id="heading-final-thoughts">Final thoughts</h1>
<p>We built this proof of concept in a week, and that's in part thanks to the amazing tools we had the opportunity to work with. Both OpenAI and Supabase provide incredible docs that truly made our job much easier.</p>
<p>Once all the parts were hooked up, we were also blown away by the potential of this tool. We feel like we're just scratching the surface of what could be a really powerful tool for knowledge workers.</p>
<p>That being said, we truly appreciate the work bloggers and content creators do. We don't want to build something that will get in the way of people enjoying great human-produced content. Instead, we'd like this tool to help potential readers reach great content easily, so that's a challenge we don't take lightly.</p>
<p>We will continue to explore this technology and idea, and see how it goes. For now, we hope our experience with this project provides you with tools and inspiration!</p>
<h1 id="heading-resources">Resources</h1>
<ul>
<li><p>Supabase Clippy: ChatGPT for Supabase Docs: <a target="_blank" href="https://supabase.com/blog/chatgpt-supabase-docs">https://supabase.com/blog/chatgpt-supabase-docs</a></p>
</li>
<li><p>pgvector: Embeddings and vector similarity: <a target="_blank" href="https://supabase.com/docs/guides/database/extensions/pgvector">https://supabase.com/docs/guides/database/extensions/pgvector</a></p>
</li>
<li><p>SpiroKit: A React Native toolkit <a target="_blank" href="https://www.spirokit.com/">https://www.spirokit.com/</a></p>
</li>
<li><p>TypeScript Gamified: Level up your TypeScript skills. Complete levels, unlock achievements, face challenges, and have fun! <a target="_blank" href="https://www.typescriptgamified.com/">https://www.typescriptgamified.com/</a></p>
</li>
<li><p>Building a mobile authentication flow for your SaaS with Expo and Supabase <a target="_blank" href="https://blog.spirokit.com/building-a-mobile-authentication-flow-for-your-saas-with-expo-and-supabase">https://blog.spirokit.com/building-a-mobile-authentication-flow-for-your-saas-with-expo-and-supabase</a></p>
</li>
<li><p>GPT3 Tokenizer <a target="_blank" href="https://github.com/botisan-ai/gpt3-tokenizer">https://github.com/botisan-ai/gpt3-tokenizer</a></p>
</li>
<li><p>Storing OpenAI embeddings in Postgres with pgvector <a target="_blank" href="https://supabase.com/blog/openai-embeddings-postgres-vector">https://supabase.com/blog/openai-embeddings-postgres-vector</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Announcing the new documentation portal for SpiroKit v2]]></title><description><![CDATA[I'm happy to announce that the new documentation portal for SpiroKit v2 is finally ready. 🥳️

This includes almost 200 new stories with code snippets ready to copy and paste on your next app.
If you have been using the previous docs portal, you'll n...]]></description><link>https://blog.spirokit.com/announcing-the-new-documentation-portal-for-spirokit-v2</link><guid isPermaLink="true">https://blog.spirokit.com/announcing-the-new-documentation-portal-for-spirokit-v2</guid><category><![CDATA[Storybook]]></category><category><![CDATA[React Native]]></category><category><![CDATA[documentation]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Tue, 26 Sep 2023 18:02:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1695751242765/aceb1a39-9fc9-4304-96f1-4eb2b821b948.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm happy to announce that the new documentation portal for SpiroKit v2 is finally ready. 🥳️</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695751078816/e54904c5-70b5-49c0-a935-2eb850104336.png" alt class="image--center mx-auto" /></p>
<p>This includes almost 200 new stories with code snippets ready to copy and paste on your next app.</p>
<p>If you have been using the previous docs portal, you'll notice that the new one is almost identical, and that's on purpose.</p>
<p>Just as I aimed to provide a great experience when migrating from SpiroKit v1 to v2, I also wanted to ensure a smooth transition for the interactive documentation portal.</p>
<p>Here are a few links from the new docs portal that you may find useful:</p>
<p><strong>Dashboard</strong><br /><strong>https://docs.spirokit.com</strong></p>
<p><strong>Installation guide:</strong><br /><a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-installation--page">https://docs.spirokit.com/?path=/docs/getting-started-installation--page</a></p>
<p><strong>Setting up your local environment to work with SpiroKit:</strong><br /><a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-setup-your-local-environment--page">https://docs.spirokit.com/?path=/docs/getting-started-setup-your-local-environment--page</a></p>
<p><strong>Introduction to Theming in SpiroKit:</strong><br /><a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-theme-00-introduction--page">https://docs.spirokit.com/?path=/docs/getting-started-theme-00-introduction--page</a></p>
<p><strong>App template catalog:</strong><br /><a target="_blank" href="https://docs.spirokit.com/?path=/docs/app-templates-catalog--page">https://docs.spirokit.com/?path=/docs/app-templates-catalog--page</a></p>
]]></content:encoded></item><item><title><![CDATA[Announcing the new SpiroKit v2 templates]]></title><description><![CDATA[I'm pleased to announce that I finished migrating all the existing v1 templates to the new SpiroKit v2! 🥳️
It took a little bit more than I initially expected, primarily because all the new templates use Expo Router v2 instead of React Navigation. �...]]></description><link>https://blog.spirokit.com/announcing-the-new-spirokit-v2-templates</link><guid isPermaLink="true">https://blog.spirokit.com/announcing-the-new-spirokit-v2-templates</guid><category><![CDATA[Expo]]></category><category><![CDATA[React Native]]></category><category><![CDATA[template]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Sat, 09 Sep 2023 00:25:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1694219061555/46ce3c7e-645f-41e9-a2bb-29213ed4de87.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm pleased to announce that I finished migrating all the existing v1 templates to the new SpiroKit v2! 🥳️</p>
<p>It took a little bit more than I initially expected, primarily because all the new templates use Expo Router v2 instead of React Navigation. 😅</p>
<p>I also took the time to rename all the existing templates, so v2 is the default now.</p>
<p><strong>Remember that all these templates are included for free with your SpiroKit license. No matter when you bought it.</strong></p>
<p>Here's the complete list of all the available templates:</p>
<h3 id="heading-expo-template-typescript">expo-template-typescript</h3>
<p>The official SpiroKit v2 blank template for Expo SDK 49 + Typescript + Expo Router v2</p>
<pre><code class="lang-typescript">npx create-spirokit-app<span class="hljs-meta">@latest</span> --template expo-template-typescript
</code></pre>
<h3 id="heading-expo-template">expo-template</h3>
<p>The official SpiroKit v2 blank template for Expo SDK 49 + Expo Router v2 (JS only)</p>
<pre><code class="lang-typescript">npx create-spirokit-app<span class="hljs-meta">@latest</span> --template expo-template
</code></pre>
<h3 id="heading-nextjs-template-typescript">nextjs-template-typescript</h3>
<p>The official SpiroKit v2 blank template for NextJS with Typescript (Web only)</p>
<pre><code class="lang-typescript">npx create-spirokit-app<span class="hljs-meta">@latest</span> --template nextjs-template-typescript
</code></pre>
<h3 id="heading-universal-app-template">universal-app-template</h3>
<p>The official SpiroKit v2 universal app template with Solito + Expo SDK 49 + NextJS + Typescript</p>
<pre><code class="lang-typescript">npx create-spirokit-app<span class="hljs-meta">@latest</span> --template universal-app-template
</code></pre>
<h3 id="heading-expo-supabase-template-typescript">expo-supabase-template-typescript</h3>
<p>The official SpiroKit v2 template for Supabase + Expo SDK 49 + Expo Router v2</p>
<pre><code class="lang-typescript">npx create-spirokit-app<span class="hljs-meta">@latest</span> --template expo-supabase-template-typescript
</code></pre>
<h3 id="heading-ecommerce-app-template">ecommerce-app-template</h3>
<p>A SpiroKit v2 E-Commerce app template with Typescript + SDK 49 + Expo Router v2</p>
<p><img src="https://public-files.gumroad.com/rs48haf1ukwewnmtb96hqgiy9n6l" alt /></p>
<pre><code class="lang-typescript">npx create-spirokit-app<span class="hljs-meta">@latest</span> --template ecommerce-app-template
</code></pre>
<h3 id="heading-travel-app-template-typescript">travel-app-template-typescript</h3>
<p>A SpiroKit v2 Travel app template with Typescript + SDK 49 + Expo Router v2</p>
<p><img src="https://public-files.gumroad.com/f1fgfsk7di70wrx12vasfuch6bwj" alt /></p>
<pre><code class="lang-typescript">npx create-spirokit-app<span class="hljs-meta">@latest</span> --template travel-app-template
</code></pre>
<hr />
<p>If you still want to use v1, most of the templates are available with the "-v1" suffix.</p>
<h2 id="heading-whats-next">What's next?</h2>
<p>Next week, I'll work on the new Storybook documentation portal for v2, so stay tuned!</p>
<p>As always, if you have any questions, please reach-out via email at <a target="_blank" href="mailto:mauro@spirokit.com">mauro@spirokit.com</a> or <a target="_blank" href="https://twitter.com/mauro_codes">leave me a DM</a> on Twitter.</p>
<p>Happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Introducing "Theme Shifter"]]></title><description><![CDATA[SpiroKit v1 already included a set of powerful features that allowed you to customize your app. To name a few:

Choose between 17 pre-defined color palettes:
  

Add custom colors and use one of them as your primary color. e.g.: Using a custom "steel...]]></description><link>https://blog.spirokit.com/introducing-theme-shifter-for-spirokit-v2</link><guid isPermaLink="true">https://blog.spirokit.com/introducing-theme-shifter-for-spirokit-v2</guid><category><![CDATA[spirokit]]></category><category><![CDATA[React Native]]></category><category><![CDATA[theme]]></category><category><![CDATA[Design]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Thu, 24 Aug 2023 23:04:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1692918187515/0cefa3bd-67d8-40d8-b9a0-b1925f1716d3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>SpiroKit v1 already included a set of powerful features that allowed you to customize your app. To name a few:</p>
<ul>
<li><p>Choose between 17 pre-defined color palettes:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692913744941/09497db8-f85a-4119-8d62-6fbd715a67d3.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Add custom colors and use one of them as your primary color. e.g.: Using a custom "steelBlue" palette as the primary color:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692914062348/5526a978-c647-4332-b386-05a1073fa3ce.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Light and dark color mode</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692914267627/24f43ca1-2594-48eb-a812-255b728e3102.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Accent color and color mode override at the component level:</p>
<ul>
<li>You can switch between light/dark or accent color at the component level by passing the <code>colorMode</code> and <code>accentColor</code> props.</li>
</ul>
</li>
<li><p>Switch between light and dark mode at runtime.</p>
</li>
</ul>
<p>Those were great features, and it's probably enough for most cases, but I was missing something.</p>
<p>What if you want to switch the accent color at runtime and allow your users to choose the color they want? Or what if you want to override the color mode or accent color for a group of components without passing the props to each component? This is especially annoying when you nest components inside of a modal or action sheet.</p>
<hr />
<h1 id="heading-theme-shifter">Theme Shifter</h1>
<p>Theme shifter is a new set of features, only available with SpiroKit v2, that will allow you to:</p>
<ul>
<li><p>Switch between accent colors at runtime. This is especially useful if you want to allow your users to choose a custom color for the UI.</p>
</li>
<li><p>Apply a new color mode (light/dark) or accent color to a group of components.</p>
</li>
</ul>
<p>Let's see this in action!</p>
<h2 id="heading-switching-accent-color-andamp-color-mode-at-runtime">Switching accent color &amp; color mode at runtime</h2>
<p>The <code>useTheme</code> hook now allows you to make the switch and also get the current accent color and dark mode like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useTheme } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/ui"</span>

<span class="hljs-keyword">const</span> { 
  accentColor, 
  setAccentColor,
  colorMode,
  setColorMode
} = useTheme()

<span class="hljs-keyword">return</span> (
  &lt;VStack space=<span class="hljs-string">"$2"</span>&gt;
    &lt;LargeTitle&gt;You are using the {accentColor} accent color and {colorMode} mode&lt;/LargeTitle&gt;
    &lt;Button onPress={<span class="hljs-function">() =&gt;</span> setColorMode(colorMode === <span class="hljs-string">"light"</span> ? <span class="hljs-string">"dark"</span> : <span class="hljs-string">"light"</span>)}&gt;
      Toggle color mode
    &lt;/Button&gt;
    &lt;Button onPress={<span class="hljs-function">() =&gt;</span> setAccentColor(<span class="hljs-string">"emerald"</span>)}&gt;
      Switch to Emerald theme
    &lt;/Button&gt;
)
</code></pre>
<p>You can safely store your user preference and call the <code>setAccentColor</code> or <code>setColorMode</code> during startup 😉</p>
<h2 id="heading-override-accent-color-andamp-color-mode-for-a-group-of-components">Override accent color &amp; color mode for a group of components</h2>
<p>There's a new component called <code>Theme</code>. You can use it to wrap a group of components so they can inherit a new accent color or color mode:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Theme } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/ui"</span>

<span class="hljs-keyword">return</span> (
  &lt;&gt;
    &lt;Theme accentColor=<span class="hljs-string">"blue"</span> colorMode=<span class="hljs-string">"dark"</span>&gt;
      &lt;VStack padding=<span class="hljs-string">"$2"</span> backgroundColor={<span class="hljs-string">"$primaryDark.0"</span>} space=<span class="hljs-string">"$2"</span>&gt;
        &lt;LargeTitle&gt;Dark blue section&lt;/LargeTitle&gt;
        &lt;HStack space=<span class="hljs-string">"$2"</span>&gt;
          &lt;Button flex={<span class="hljs-number">1</span>}&gt;Cancel&lt;/Button&gt;
          &lt;Button flex={<span class="hljs-number">1</span>}&gt;Confirm&lt;/Button&gt;
        &lt;/HStack&gt;
      &lt;/VStack&gt;
    &lt;/Theme&gt;
    &lt;Theme accentColor=<span class="hljs-string">"red"</span> colorMode=<span class="hljs-string">"light"</span>&gt;
      &lt;VStack padding=<span class="hljs-string">"$2"</span> backgroundColor={<span class="hljs-string">"white"</span>} space=<span class="hljs-string">"$2"</span>&gt;
        &lt;LargeTitle&gt;Red light section&lt;/LargeTitle&gt;
        &lt;HStack space=<span class="hljs-string">"$2"</span>&gt;
          &lt;Button flex={<span class="hljs-number">1</span>}&gt;Cancel&lt;/Button&gt;
          &lt;Button flex={<span class="hljs-number">1</span>}&gt;Confirm&lt;/Button&gt;
        &lt;/HStack&gt;
      &lt;/VStack&gt;
    &lt;/Theme&gt;
  &lt;/&gt;
)
</code></pre>
<p>As you can see in the image below, all 3 components are now inheriting the new preferences:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692917711565/73817695-9af3-4265-8dd2-9a2316bf828d.png" alt class="image--center mx-auto" /></p>
<hr />
<h1 id="heading-start-using-these-new-features-today">Start using these new features today.</h1>
<ul>
<li><p>If you are already using SpiroKit v2, run the <code>yarn upgrade</code> or <code>npm upgrade</code> command to get the latest version. The theme shifter functionality was added on <code>v2.0.0-rc.15</code></p>
</li>
<li><p>If you are using v1, I highly recommend you to upgrade to v2. I just published a comprehensive guide with everything you need to do.</p>
<ul>
<li><p>You can also use our new starters for v2:</p>
<pre><code class="lang-bash">  <span class="hljs-comment"># Starter with Expo SDK 49, Expo Router v2, TypeScript and SpiroKit v2</span>
  npx create-spirokit-app@latest --template expo-template-typescript-v2

  <span class="hljs-comment"># Web Starter with NextJS 13, TypeScript and SpiroKit v2</span>
  npx create-spirokit-app@latest --template nextjs-template-typescript-v2
</code></pre>
</li>
</ul>
</li>
</ul>
<hr />
<p>I hope you can enjoy using these new features! And let me know what you think.</p>
<p>Happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Migrating an existing project from SpiroKit v1 to v2]]></title><description><![CDATA[This document is a work in progress. I'll keep updating this in case I find an edge case I forget to document.

In this article, I'll describe the required steps to migrate an existing SpiroKit v1 project so you can use v2, which comes with an entire...]]></description><link>https://blog.spirokit.com/migrating-an-existing-project-from-spirokit-v1-to-v2</link><guid isPermaLink="true">https://blog.spirokit.com/migrating-an-existing-project-from-spirokit-v1-to-v2</guid><category><![CDATA[Expo]]></category><category><![CDATA[migration]]></category><category><![CDATA[spirokit]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Wed, 23 Aug 2023 00:39:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1692751113064/6bf7729e-0451-4d6b-83b6-cbd472424522.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>This document is a work in progress. I'll keep updating this in case I find an edge case I forget to document.</p>
</blockquote>
<p>In this article, I'll describe the required steps to migrate an existing SpiroKit v1 project so you can use v2, which comes with an entire rewrite and uses Tamagui's compiler. If you want to learn more about v2, please check out <a target="_blank" href="https://blog.spirokit.com/introducing-spirokit-v2-unleashing-the-power-of-tamagui">this article</a>.</p>
<blockquote>
<p>If you are just starting a new project with SpiroKit, you can already use our two starter templates for v2:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Starter with Expo SDK 49, Expo Router v2, TypeScript and SpiroKit v2</span>
npx create-spirokit-app@latest --template expo-template-typescript-v2

<span class="hljs-comment"># Web Starter with NextJS 13, TypeScript and SpiroKit v2</span>
npx create-spirokit-app@latest --template nextjs-template-typescript-v2
</code></pre>
</blockquote>
<hr />
<h1 id="heading-packagejson">Package.json</h1>
<p>On v1, the package you installed from the private repo was called <code>@spirokit/core</code>. On v2, you need to remove that package, and add the following packages:</p>
<ul>
<li><p>For native development with Expo</p>
<ul>
<li><p>@spirokit/ui</p>
</li>
<li><p>@spirokit/native</p>
</li>
</ul>
</li>
<li><p>For web development with NextJS</p>
<ul>
<li><p>@spirokit/ui</p>
</li>
<li><p>@spirokit/web</p>
</li>
</ul>
</li>
</ul>
<p>Besides, SpiroKit is now using <code>@tamagui/lucide-icons</code> instead of <code>react-native-heroicons</code> (More info about how to replace the items below)</p>
<p>Here's an example of your package.json after the changes</p>
<pre><code class="lang-diff">{
    "name": "expo-example",
    "version": "1.0.0",
    "main": "index.js",
    "scripts": {
<span class="hljs-deletion">-        "start": "expo start",</span>
<span class="hljs-addition">+       "start": "TAMAGUI_TARGET=native expo start",</span>
<span class="hljs-deletion">-        "android": "expo start --android",</span>
<span class="hljs-addition">+       "android": "TAMAGUI_TARGET=native expo start --android",</span>
<span class="hljs-deletion">-        "ios": "expo start --ios",</span>
<span class="hljs-addition">+       "ios": "TAMAGUI_TARGET=native expo start --ios",</span>
<span class="hljs-deletion">-        "web": "expo start --web",</span>
<span class="hljs-addition">+       "web": "TAMAGUI_TARGET=web expo start --web",</span>
    },
    "dependencies": {
<span class="hljs-deletion">-        "@spirokit/core": "latest",</span>
<span class="hljs-addition">+       "@spirokit/ui": "next",</span>
<span class="hljs-addition">+       "@spirokit/native": "next",</span>
<span class="hljs-addition">+       "@shopify/flash-list": "^1.4.3",</span>
        "@expo-google-fonts/poppins": "^0.2.2",
<span class="hljs-deletion">-       "@expo/webpack-config": "^0.17.2",</span>
<span class="hljs-deletion">-       "expo": "^47.0.0",</span>
<span class="hljs-addition">+       "expo": "^49.0.0",</span>
<span class="hljs-deletion">-       "expo-linear-gradient": "~12.0.1",</span>
        "expo-status-bar": "~1.4.2",
        "react": "18.1.0",
        "react-dom": "18.1.0",
        "react-native": "0.70.5",
<span class="hljs-deletion">-       "react-native-heroicons": "2.0.2",</span>
<span class="hljs-addition">+       "@tamagui/lucide-icons": "1.40.0",</span>
        "react-native-safe-area-context": "4.4.1",
        "react-native-svg": "13.4.0",
        "react-native-web": "~0.18.7"
    },
    "devDependencies": {
        "@babel/core": "^7.19.3",
        "@types/react": "~18.0.24",
        "@types/react-native": "~0.70.6",
        "typescript": "^4.6.3"
    },
    "private": true
}
</code></pre>
<p>If you can, start from scratch using our new templates mentioned above, and then copy / paste your code.</p>
<p>If you want to keep your current project, check the package.json from our v1 and v2 templates</p>
<ul>
<li><p>v1 expo template: <a target="_blank" href="https://github.com/spirokit/templates/blob/main/expo-template-typescript/package.json">https://github.com/spirokit/templates/blob/main/expo-template-typescript/package.json</a></p>
</li>
<li><p>v2 expo template (with expo router): <a target="_blank" href="https://github.com/spirokit/templates/blob/main/expo-template-typescript-v2/package.json">https://github.com/spirokit/templates/blob/main/expo-template-typescript-v2/package.json</a></p>
</li>
</ul>
<hr />
<h1 id="heading-upgrading-imports">Upgrading imports</h1>
<p>Search for <code>from "@spirokit/core"</code> and replace all the occurrences for <code>from "@spirokit/ui"</code>.</p>
<blockquote>
<p>You may get errors on missing components like <code>DateTimePicker</code> and <code>Pressable</code>. More info about this below.</p>
</blockquote>
<hr />
<h1 id="heading-tokens">Tokens</h1>
<p>One big change from v1 to v2, is that all the design tokens are now strings, and are pre-pended with a "$" sign. This is how Tamagui works. Let me show you an example:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// v1</span>
<span class="hljs-keyword">return</span> (
  &lt;Box padding={<span class="hljs-number">4</span>} marginTop={<span class="hljs-number">2</span>}&gt;
    &lt;Body fontWeight=<span class="hljs-string">"bold"</span>&gt;Hello world&lt;/Body&gt;
  &lt;/Box&gt;
)

<span class="hljs-comment">// v2</span>
<span class="hljs-keyword">return</span> (
  &lt;Box padding=<span class="hljs-string">"$4"</span> marginTop=<span class="hljs-string">"$2"</span>&gt;
    &lt;Body fontWeight=<span class="hljs-string">"$bold"</span>&gt;Hello world&lt;/Body&gt;
  &lt;/Box&gt;
)
</code></pre>
<p>On v1, you usually use numbers to set things like <code>padding</code>, <code>margin</code>, <code>borderRadius</code>, etc. But there were "special values" defined on <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-size-tokens--page">this list</a> that were mapped to other values:</p>
<ul>
<li><p>Use 4 to get 16px</p>
</li>
<li><p>Use 6 to get 24px</p>
</li>
</ul>
<p>That was convenient but also confusing: If you were using 32, you would get 128px, but if you used 33, you get 33px.</p>
<p>On v2, you can still use numeric values, but all the size tokens now are strings with the "$" prefix.</p>
<p>After migrating a few apps to v2, I came up with a list of things you may need to check and replace. Thankfully, VSCode provides an excellent tool to find and replace these tokens quickly.</p>
<h2 id="heading-font-tokens">Font tokens</h2>
<ul>
<li><p>fontWeight</p>
<ul>
<li><p>Search for <code>fontWeight=</code></p>
</li>
<li><p>Add the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-typography-tokens--page#font-weight">Here are the values</a></p>
<ul>
<li><code>&lt;Body fontWeight="bold" /&gt;</code> -&gt; <code>&lt;Body fontWeight="$bold" /&gt;</code></li>
</ul>
</li>
</ul>
</li>
<li><p>fontSize</p>
<ul>
<li><p>Search for <code>fontSize=</code></p>
</li>
<li><p>Add the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-typography-tokens--page#font-size">Here are the values</a></p>
<ul>
<li><code>&lt;Body fontSize="lg" /&gt;</code> -&gt; <code>&lt;Body fontSize="$lg" /&gt;</code></li>
</ul>
</li>
</ul>
</li>
<li><p>lineHeight</p>
<ul>
<li><p>Search for <code>lineHeight=</code></p>
</li>
<li><p>Add the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-typography-tokens--page#line-height">Here are the values</a></p>
<ul>
<li><code>&lt;Body lineHeight="xl" /&gt;</code> -&gt; <code>&lt;Body lineHeight="$xl" /&gt;</code></li>
</ul>
</li>
</ul>
</li>
<li><p>letterSpacing</p>
<ul>
<li><p>Search for <code>letterSpacing=</code></p>
</li>
<li><p>Add the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-typography-tokens--page#letter-spacing">Here are the values</a></p>
<ul>
<li><code>&lt;Body letterSpacing="xl" /&gt;</code> -&gt; <code>&lt;Body letterSpacing="$xl" /&gt;</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="heading-color-tokens">Color tokens</h2>
<ul>
<li><p>backgroundColor, borderColor, color, shadowColor, outlineColor</p>
<ul>
<li><p>Search for <code>backgroundColor=</code>, <code>borderColor=</code>, <code>shadowColor</code>, <code>outlineColor</code></p>
</li>
<li><p>Add the "$" prefix</p>
<ul>
<li><code>&lt;Box backgroundColor="primary.500" /&gt;</code> -&gt; <code>&lt;Box backgroundColor="$primary.500" /&gt;</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="heading-size-tokens">Size tokens</h2>
<ul>
<li><p>margin, marginLeft, marginRight, marginTop, marginBottom</p>
<ul>
<li><p>Search for <code>margin={</code>, <code>marginLeft={</code>, <code>marginRight={</code>, <code>marginTop={</code>, <code>marginBottom={</code></p>
</li>
<li><p>Search for values in the size scale, like 1 to 10, 12,16,20,24, etc. Replace those values with strings with the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-size-tokens--page">Full list of values</a></p>
<ul>
<li><p><code>&lt;Box margin={4} /&gt;</code> -&gt; <code>&lt;Box margin="$4" /&gt;</code></p>
</li>
<li><p><code>&lt;Box marginTop={4} /&gt;</code> -&gt; <code>&lt;Box marginTop="$4" /&gt;</code></p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>padding, paddingLeft, paddingRight, paddingTop, paddingBottom</p>
<ul>
<li><p>Search for <code>padding={</code>, <code>paddingLeft={</code>, <code>paddingRight={</code>, <code>paddingTop={</code>, <code>paddingBottom={</code></p>
</li>
<li><p>Search for values in the size scale, like 1 to 10, 12,16,20,24, etc. Replace those values with strings with the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-size-tokens--page">Full list of values</a></p>
<ul>
<li><p><code>&lt;Box padding={4} /&gt;</code> -&gt; <code>&lt;Box padding="$4" /&gt;</code></p>
</li>
<li><p><code>&lt;Box paddingBottom={4} /&gt;</code> -&gt; <code>&lt;Box paddingBottom="$4" /&gt;</code></p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>marginX</p>
<ul>
<li><p>Search for <code>marginX={</code></p>
</li>
<li><p>On v2, it was renamed to use <code>marginHorizontal</code> instead</p>
</li>
<li><p>Search for values in the size scale, like 1 to 10, 12,16,20,24, etc. Replace those values with strings with the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-size-tokens--page">Full list of values</a></p>
<ul>
<li><code>&lt;Box marginX={4} /&gt;</code> -&gt; <code>&lt;Box marginHorizontal="$4" /&gt;</code></li>
</ul>
</li>
</ul>
</li>
<li><p>marginY</p>
<ul>
<li><p>Search for <code>marginY={</code></p>
</li>
<li><p>On v2, it was renamed to use <code>marginVertical</code> instead</p>
</li>
<li><p>Search for values in the size scale, like 1 to 10, 12,16,20,24, etc. Replace those values with strings with the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-size-tokens--page">Full list of values</a></p>
<ul>
<li><code>&lt;Box marginY={4} /&gt;</code> -&gt; <code>&lt;Box marginVertical="$4" /&gt;</code></li>
</ul>
</li>
</ul>
</li>
<li><p>paddingX</p>
<ul>
<li><p>Search for <code>paddingX={</code></p>
</li>
<li><p>On v2, it was renamed to use <code>paddingHorizontal</code> instead</p>
</li>
<li><p>Search for values in the size scale, like 1 to 10, 12,16,20,24, etc. Replace those values with strings with the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-size-tokens--page">Full list of values</a></p>
<ul>
<li><code>&lt;Box paddingX={4} /&gt;</code> -&gt; <code>&lt;Box paddingHorizontal="$4" /&gt;</code></li>
</ul>
</li>
</ul>
</li>
<li><p>paddingY</p>
<ul>
<li><p>Search for <code>paddingY={</code></p>
</li>
<li><p>On v2, it was renamed to use <code>paddingVertical</code> instead</p>
</li>
<li><p>Search for values in the size scale, like 1 to 10, 12,16,20,24, etc. Replace those values with strings with the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-size-tokens--page">Full list of values</a></p>
<ul>
<li><code>&lt;Box paddingY={4} /&gt;</code> -&gt; <code>&lt;Box paddingVertical="$4" /&gt;</code></li>
</ul>
</li>
</ul>
</li>
<li><p>space</p>
<ul>
<li><p>Search for <code>space={</code></p>
</li>
<li><p>Search for values in the size scale, like 1 to 10, 12,16,20,24, etc. Replace those values with strings with the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-size-tokens--page">Full list of values</a></p>
<ul>
<li><code>&lt;Box space={4} /&gt;</code> -&gt; <code>&lt;Box space="$4" /&gt;</code></li>
</ul>
</li>
</ul>
</li>
<li><p>width, minWidth, maxWidth</p>
<ul>
<li><p>Search for <code>width={</code>, <code>minWidth={</code>, <code>maxWidth={</code></p>
</li>
<li><p>Search for values in the size scale, like 1 to 10, 12,16,20,24, etc. Replace those values with strings with the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-size-tokens--page">Full list of values</a></p>
<ul>
<li><p><code>&lt;Box width={4} /&gt;</code> -&gt; <code>&lt;Box width="$4" /&gt;</code></p>
</li>
<li><p><code>&lt;Box width={"full"} /&gt;</code> -&gt; <code>&lt;Box width="$full" /&gt;</code></p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>height, minHeight, maxHeight</p>
<ul>
<li><p>Search for <code>height={</code>, <code>minHeight={</code>, <code>maxHeight={</code></p>
</li>
<li><p>Search for values in the size scale, like 1 to 10, 12,16,20,24, etc. Replace those values with strings with the "$" prefix. <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-design-tokens-size-tokens--page">Full list of values</a></p>
<ul>
<li><p><code>&lt;Box height={4} /&gt;</code> -&gt; <code>&lt;Box height="$4" /&gt;</code></p>
</li>
<li><p><code>&lt;Box height={"full"} /&gt;</code> -&gt; <code>&lt;Box height="$full" /&gt;</code></p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr />
<h2 id="heading-border-radius-tokens">Border-radius tokens</h2>
<p>On v1, border-radius allowed numeric values and the following tokens:</p>
<pre><code class="lang-typescript">{
    <span class="hljs-string">"none"</span>: <span class="hljs-number">0</span>,
    <span class="hljs-string">"xs"</span>: <span class="hljs-number">2</span>,
    <span class="hljs-string">"sm"</span>: <span class="hljs-number">4</span>,
    <span class="hljs-string">"md"</span>: <span class="hljs-number">6</span>,
    <span class="hljs-string">"lg"</span>: <span class="hljs-number">8</span>,
    <span class="hljs-string">"xl"</span>: <span class="hljs-number">12</span>,
    <span class="hljs-string">"2xl"</span>: <span class="hljs-number">16</span>,
    <span class="hljs-string">"3xl"</span>: <span class="hljs-number">24</span>,
    <span class="hljs-string">"full"</span>: <span class="hljs-number">9999</span>
}
</code></pre>
<p>On v2, you can still use numeric values, and the following tokens (as string, with the "$" prefix:</p>
<pre><code class="lang-typescript">{
  <span class="hljs-number">0</span>: <span class="hljs-number">0</span>,
  <span class="hljs-number">1</span>: <span class="hljs-number">3</span>,
  <span class="hljs-number">2</span>: <span class="hljs-number">5</span>,
  <span class="hljs-number">3</span>: <span class="hljs-number">7</span>,
  <span class="hljs-number">4</span>: <span class="hljs-number">9</span>,
  <span class="hljs-literal">true</span>: <span class="hljs-number">9</span>,
  <span class="hljs-number">5</span>: <span class="hljs-number">10</span>,
  <span class="hljs-number">6</span>: <span class="hljs-number">16</span>,
  <span class="hljs-number">7</span>: <span class="hljs-number">19</span>,
  <span class="hljs-number">8</span>: <span class="hljs-number">22</span>,
  <span class="hljs-number">9</span>: <span class="hljs-number">26</span>,
  <span class="hljs-number">10</span>: <span class="hljs-number">34</span>,
  <span class="hljs-number">11</span>: <span class="hljs-number">42</span>,
  <span class="hljs-number">12</span>: <span class="hljs-number">50</span>,
}
</code></pre>
<p>You can use the new tokens like this</p>
<ul>
<li><p><code>&lt;Box borderRadius="2xl"/&gt;</code> -&gt; <code>&lt;Box borderRadius="$6"</code></p>
</li>
<li><p><code>&lt;Box borderRadius="full"/&gt;</code> -&gt; <code>&lt;Box borderRadius="$12"</code></p>
</li>
</ul>
<p>Or you can replace the old tokens with the numeric values</p>
<ul>
<li><code>&lt;Box borderRadius="2xl"/&gt;</code> -&gt; <code>&lt;Box borderRadius={16}</code></li>
</ul>
<hr />
<h1 id="heading-pseudo-props">Pseudo props</h1>
<p>SpiroKit v1 was built on top of NativeBase. That's why you could use pseudo props like:</p>
<ul>
<li><p><code>_hover</code></p>
</li>
<li><p><code>_pressed</code></p>
</li>
<li><p><code>_focus</code></p>
</li>
<li><p>Platform-specific</p>
<ul>
<li><p><code>_web</code></p>
</li>
<li><p><code>_iOS</code></p>
</li>
<li><p><code>_android</code></p>
</li>
</ul>
</li>
</ul>
<p>On SpiroKit v2, we are inheriting pseudo props from Tamagui. Platform-specific props are not available yet, so you'll need to check for the platform and conditionally add styles for now.</p>
<p>But you can still use the hover, pressed, and focus props with a new name.</p>
<ul>
<li><p><code>_hover</code> -&gt; <code>_hoverStyle</code></p>
</li>
<li><p><code>_pressed</code> -&gt; <code>_pressStyle</code></p>
</li>
<li><p><code>_focus</code> -&gt; <code>_focusStyle</code></p>
</li>
</ul>
<p>Learn more about these props <a target="_blank" href="https://tamagui.dev/docs/intro/props#pseudo-style-props">here</a></p>
<hr />
<h1 id="heading-media-props">Media props</h1>
<p>On v1, you were able to set responsive values like this:</p>
<pre><code class="lang-typescript">&lt;Box width={{ base: <span class="hljs-string">"full"</span>, lg: <span class="hljs-string">"1/2"</span> }}/&gt;
</code></pre>
<p>On v2, we have separated props for each breakpoint:</p>
<pre><code class="lang-typescript">&lt;Box width=<span class="hljs-string">"$full"</span> $gtLg=<span class="hljs-string">"$1/2"</span> /&gt;
</code></pre>
<hr />
<h1 id="heading-breaking-changes-in-components">Breaking changes in components</h1>
<ul>
<li><p>The <code>Pressable</code> component was removed. Now all the common layout components like <code>Box</code>, <code>VStack</code>, <code>HStack</code>, etc already include a <code>onPress</code> prop.</p>
</li>
<li><p><strong>Button</strong> component</p>
<ul>
<li><p>The <code>size</code> prop no longer include the value "xs". use "sm" instead.</p>
<blockquote>
<p>You may notice that the buttons are a little bit smaller than in v1. This was based on feedback from users.</p>
</blockquote>
</li>
</ul>
</li>
<li><p><strong>Badge</strong> and <strong>FlatList</strong> components</p>
<ul>
<li><p>On v1, you could add style props like this:</p>
<pre><code class="lang-typescript">  &lt;Badge paddingX={<span class="hljs-number">4</span>} paddingY={<span class="hljs-number">2</span>}&gt;Hello&lt;/Badge&gt;
</code></pre>
</li>
<li><p>On v2, style props were moved into the <code>_container</code> prop</p>
<pre><code class="lang-typescript">  &lt;Badge _container={{ 
    paddingHorizontal: <span class="hljs-string">"$4"</span>, 
    paddingVertical: <span class="hljs-string">"$2"</span>
  }}&gt;Hello&lt;/Badge&gt;
</code></pre>
</li>
</ul>
</li>
<li><p><strong>Switch</strong> component</p>
<ul>
<li><code>value</code> prop was replaced with the <code>checked</code> prop.</li>
</ul>
</li>
<li><p>Use <strong>DateTimeInput</strong> instead of <strong>DateTimePicker</strong></p>
<ul>
<li><p>The DatePicker component was temporarily removed. v1 was built on top of react-native-modern-datepicker and it had performance issues.</p>
</li>
<li><p>I'll rebuild this component soon. In the meantime, you can use the new <strong>DateTimeInput</strong> component. Documentation with examples will be available soon.</p>
</li>
</ul>
</li>
<li><p><strong>Select</strong> component</p>
<ul>
<li><p><code>ItemComponent</code> prop was renamed. Use <code>renderItem</code> instead.</p>
<blockquote>
<p>This component is now using <a target="_blank" href="https://github.com/Shopify/flash-list">FlashList by Shopify</a>, so enjoy the new performance boost 😉</p>
</blockquote>
</li>
</ul>
</li>
<li><p>Box, HStack, VStack, etc</p>
<ul>
<li><p>LinearGradient is not available yet. Will be added later.</p>
</li>
<li><p>SafeArea props are no longer available. I'll probably add them again later. In the meantime, you can get the same functionality doing the following refactor:</p>
<pre><code class="lang-typescript">  <span class="hljs-comment">// v1</span>
  &lt;Box safeAreaTop /&gt;

  <span class="hljs-comment">// v2</span>
  <span class="hljs-keyword">import</span> { useSafeAreaInsets } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-safe-area-context"</span>;
  <span class="hljs-keyword">const</span> { top } = useSafeAreaInsets();

  &lt;Box top={top}&gt;
</code></pre>
</li>
</ul>
</li>
</ul>
<hr />
<h1 id="heading-upgrading-icons">Upgrading icons</h1>
<ul>
<li><p>SpiroKit v1 was using <code>react-native-heroicons</code> (292 icons)</p>
</li>
<li><p>SpiroKit v2 is now using <code>@tamagui/lucide-icons</code> (1237 icons)</p>
</li>
<li><p>Components like <code>Button</code>, <code>Alert</code>, <code>Badge</code>, <code>HorizontalCard</code>, <code>VerticalCard</code>, <code>PortraitCard</code>, <code>Input</code>, <code>Tab</code>, <code>TextArea</code> should still work with the old icons, but I highly recommend you to make the switch. You can search for all the available components <a target="_blank" href="https://lucide.dev/">here</a></p>
</li>
</ul>
<p>Migration is straightforward:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// v1</span>
<span class="hljs-keyword">import</span> { MapIcon } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-heroicons/outline"</span>
&lt;Button IconLeftComponent={MapIcon}&gt;

<span class="hljs-comment">// v2</span>
<span class="hljs-keyword">import</span> { <span class="hljs-built_in">Map</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"@tamagui/lucide-icons"</span>
&lt;Button IconLeftComponent={<span class="hljs-built_in">Map</span>}&gt;
</code></pre>
<hr />
<h1 id="heading-hooks">Hooks</h1>
<h2 id="heading-usecolormode">useColorMode</h2>
<ul>
<li><p>In v1, there was a "useColorMode" hook inherited from NativeBase. In v2, I decided to remove this hook, and unify all the features related to color mode and accent color in the "useTheme" hook. More information about this in an upcoming post introducing new features related to theming capabilities.</p>
</li>
<li><p>If you were using this hook to check for the active color mode, or to toggle between dark and light mode, you can now use the "useTheme" hook to achieve the same</p>
</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-comment">// v1</span>
<span class="hljs-keyword">const</span> { colorMode, toggleColorMode } = useColorMode()

<span class="hljs-comment">// v2</span>
<span class="hljs-keyword">const</span> { colorMode, setColorMode, accentColor, setAccentColor } = useTheme()
</code></pre>
<hr />
<h1 id="heading-feedback-is-appreciated">Feedback is appreciated 🙏</h1>
<p>If you are trying to migrate and you find another issue that is not included on this post, please feel free to reach out to me at <a target="_blank" href="http://mailto:mauro@spirokit.com">mauro@spirokit.com</a> or <a target="_blank" href="https://twitter.com/mauro_codes">send me a DM on Twitter</a></p>
]]></content:encoded></item><item><title><![CDATA[Introducing SpiroKit v2: 
Unleashing the Power of Tamagui]]></title><description><![CDATA[After months of hard work, I'm thrilled to announce the launch of SpiroKit v2. This version lays the foundation required to elevate SpiroKit to an entirely new level.
In this post, I'll walk you through what's new in this version and explain the reas...]]></description><link>https://blog.spirokit.com/introducing-spirokit-v2-unleashing-the-power-of-tamagui</link><guid isPermaLink="true">https://blog.spirokit.com/introducing-spirokit-v2-unleashing-the-power-of-tamagui</guid><category><![CDATA[Expo]]></category><category><![CDATA[React Native]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[tamagui]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Fri, 11 Aug 2023 11:51:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1691625944846/cb865704-721b-406c-bbca-9a7f15fec678.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After months of hard work, I'm thrilled to announce the launch of SpiroKit v2. This version lays the foundation required to elevate SpiroKit to an entirely new level.</p>
<p>In this post, I'll walk you through what's new in this version and explain the reasoning behind choosing a new framework.</p>
<h1 id="heading-a-fresh-start">A fresh start</h1>
<p>SpiroKit v1 was built on top of NativeBase, a popular React Native UI kit.</p>
<p>A while ago, I began reading about significant performance issues inherited from NativeBase, which impacted SpiroKit users (particularly Android). Furthermore, the team behind it announced a new library and decided to kill NativeBase. In light of these performance problems and the news about NativeBase being replaced with a new (and entirely different) library, I started searching for a better long-term solution.</p>
<p>Before continuing, I would like to give a shoutout to the NativeBase team for their hard work. They are now developing a new library called Gluestack that appears to address these issues, and I'm grateful for their contribution, which laid the foundation for SpiroKit v1 🙌</p>
<hr />
<h1 id="heading-seeking-new-solutions">Seeking new solutions</h1>
<p>After some research, I found Tamagui, and it immediately caught my attention. They were building a compiler that optimizes for web and mobile, and the benchmarks were mind-blowing:</p>
<p><strong>React Native:</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1691627785391/faf511f9-d043-46d6-a4d1-3f31b054a1d9.png" alt class="image--center mx-auto" /></p>
<p><strong>Web:</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1691627876456/010cecd1-a089-4372-8cab-b3503904405b.png" alt class="image--center mx-auto" /></p>
<p>If you want to read more about the benchmarks, <a target="_blank" href="https://tamagui.dev/docs/intro/benchmarks">here</a> you can find all the details</p>
<p>On top of those numbers, they have a vibrant and active community, and it aligns perfectly with SpiroKit's mission to make development smoother without sacrificing performance.</p>
<hr />
<h1 id="heading-whats-new-in-spirokit-v2">What's new in SpiroKit v2</h1>
<p>SpiroKit no longer depends on NativeBase. The new version was completely rebuilt from the ground up to take advantage of Tamagui's compiler. Nevertheless, I wanted to provide a smooth transition from v1, and that's why all the components and the vast majority of the props remain the same.</p>
<p>The theming system, colors, and tokens are also the same, meaning that you can mostly copy/paste code from SpiroKit v1 to v2, and it should work with minor adjustments. In other words, same interface but a new implementation.</p>
<p>Yes, there are breaking changes, but don't worry—I'm currently working on a simple guide to help you transition smoothly. This guide will be available soon, alongside the new documentation portal built on top of Storybook.</p>
<hr />
<h1 id="heading-try-it-today">Try it today!</h1>
<p>I'm excited to announce that the release candidate of SpiroKit v2 is now available on our private NPM registry! You can dive into the improvements right away if you're a licensed user, or <a target="_blank" href="https://maurocodes.gumroad.com/l/spirokit-figma-react-native?layout=profile">You can get your license here</a> (we support parity purchase power 🌎). You can expect to see the final version 2.0 in the upcoming days as I go through a few more bugs.</p>
<h2 id="heading-new-templates-available">New Templates available</h2>
<p>I've created two new blank templates to help you get started quickly with SpiroKit v2.</p>
<ol>
<li>Expo SDK 49 + Expo Router v2 + TypeScript<br /> This is the perfect template if you just want to focus on mobile. You get a web app for free thanks to Expo Router.</li>
</ol>
<pre><code class="lang-bash">npx create-spirokit-app@latest --template expo-template-typescript-v2
</code></pre>
<ol>
<li>NextJS 13 Web-only template<br /> If you only want to focus on web, this template is for you.</li>
</ol>
<pre><code class="lang-bash">npx create-spirokit-app@latest --template nextjs-template-typescript-v2
</code></pre>
<blockquote>
<p>These are blank templates, with everything configured and ready so you can start building. If you don´t want to start from scratch, we are working on migrating the rest of the templates, including auth with supabase, e-commerce template with more than 20 screens, travel-app template, and more!</p>
</blockquote>
<hr />
<h1 id="heading-whats-coming-next">What's coming next?</h1>
<p>Stay tuned as I work on migrating the rest of the templates from v1. Among these templates, I'm especially excited about the Universal app template, which combines NextJS, Expo, SpiroKit, and Solito for efficient cross-platform development.</p>
<p>Once everything is migrated, I'll resume my work on new components and more documentation to simplify things like authentication with Supabase, payment integrations with Stripe, and more!</p>
<hr />
<h1 id="heading-closing-words">Closing Words</h1>
<p>SpiroKit v2 represents a big step forward in my journey to provide tools that simplify development while boosting creativity. I'm thankful for your support and can't wait to see the incredible applications you'll build with SpiroKit. As always, my goal is to empower developers and simplify React Native development, so you can focus on shipping your ideas instead of looking for the perfect color for a button 😉</p>
<p>I would love to hear your thought about this new direction, and let me know if you want me to focus on something in particular.</p>
<p>Happy coding 💪</p>
]]></content:encoded></item><item><title><![CDATA[Setting Up Storybook Web and Native with Expo Router v2, SDK 49, and TypeScript]]></title><description><![CDATA[Introduction
In my previous post, I shared a comprehensive guide about setting up Storybook to work with Expo SDK 49, Expo Router v2, and TypeScript.
If you haven't read that article yet, I highly recommend checking it out, as I'll be using the repos...]]></description><link>https://blog.spirokit.com/setting-up-storybook-web-and-native-with-expo-router-v2-sdk-49-and-typescript</link><guid isPermaLink="true">https://blog.spirokit.com/setting-up-storybook-web-and-native-with-expo-router-v2-sdk-49-and-typescript</guid><category><![CDATA[Expo]]></category><category><![CDATA[Storybook]]></category><category><![CDATA[React Native]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Sat, 22 Jul 2023 14:00:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1689987847405/2543571f-5eaf-4cd0-a245-c7add7323efe.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>In my previous post, I shared a comprehensive guide about setting up Storybook to work with Expo SDK 49, Expo Router v2, and TypeScript.</p>
<p>If you haven't read that article yet, I highly recommend checking it out, as I'll be using the repository from the last post as a starting point.</p>
<p><a target="_blank" href="https://blog.spirokit.com/using-storybook-with-expo-router-v2-sdk-49-typescript">Using Storybook with Expo Router v2, SDK 49 &amp; TypeScript</a></p>
<p>You can also fork my <a target="_blank" href="https://github.com/mauro-codes/expo-router-storybook-starter">GitHub repo</a></p>
<p>Since the Storybook Native setup is already working with Expo SDK 49, Expo Router v2, and TypeScript on my repo, this article will focus on adding Storybook web, so we can easily share our components with the rest of the team, or even publish it with Vercel.</p>
<blockquote>
<p>The <a target="_blank" href="http://spirokit.com">SpiroKit UI Kit</a> includes an interactive documentation portal that is built with Storybook and is publicly available <a target="_blank" href="http://docs.spirokit.com">here</a>. Feel free to explore it for inspiration and gather some ideas.</p>
</blockquote>
<hr />
<h1 id="heading-adding-dependencies">Adding dependencies</h1>
<h2 id="heading-tldr">TLDR</h2>
<ol>
<li><p>Update your <code>package.json</code> to include all the required and recommended dependencies</p>
<pre><code class="lang-diff">{
 "scripts": {
   ...
 },
 "dependencies": {
   ...
 },
 "devDependencies": {
   "@babel/core": "^7.20.0",
   "typescript": "^5.1.3",
   "@types/react": "~18.2.14",
   "@babel/plugin-proposal-export-namespace-from": "^7.18.9",
   "@react-native-async-storage/async-storage": "^1.19.0",
   "@react-native-community/datetimepicker": "^7.4.0",
   "@react-native-community/slider": "^4.4.2",
   "@storybook/addon-actions": "^6.5.16",
   "@storybook/addon-controls": "^6.5.16",
<span class="hljs-addition">+   "@storybook/addon-essentials": "^6.5.16",</span>
<span class="hljs-addition">+   "@storybook/addon-links": "^6.5.16",</span>
   "@storybook/addon-ondevice-actions": "^6.5.4",
   "@storybook/addon-ondevice-controls": "^6.5.4",
<span class="hljs-addition">+   "@storybook/addon-react-native-web": "^0.0.21",</span>
   "@storybook/react-native": "^6.5.4",
<span class="hljs-addition">+   "@storybook/react": "^6.5.16",</span>
<span class="hljs-addition">+   "@storybook/builder-webpack5": "^6.5.14",</span>
<span class="hljs-addition">+   "@storybook/manager-webpack5": "^6.5.14",</span>
<span class="hljs-addition">+   "babel-plugin-react-docgen-typescript": "^1.5.1",</span>
<span class="hljs-addition">+   "babel-plugin-react-native-web": "^0.18.10",</span>
<span class="hljs-addition">+   "metro-react-native-babel-preset": "^0.77.0",</span>
   "babel-loader": "^8.3.0"
 },
 "expo": {
   ...
 },
<span class="hljs-addition">+ "resolutions": {</span>
<span class="hljs-addition">+   "react-docgen-typescript": "2.2.2",</span>
<span class="hljs-addition">+   "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0"</span>
<span class="hljs-addition">+ },</span>
<span class="hljs-addition">+ "overrides": {</span>
<span class="hljs-addition">+   "react-docgen-typescript": "2.2.2",</span>
<span class="hljs-addition">+   "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0"</span>
<span class="hljs-addition">+ },</span>
}
</code></pre>
</li>
<li><p>Run <code>npm install</code></p>
</li>
</ol>
<p>For the web setup, we need to add a few dependencies. Some are required, and some are optional (but recommended for a better experience).</p>
<ul>
<li><p><a target="_blank" href="https://storybook.js.org/addons/@storybook/addon-links">@storybook/addon-links</a> (optional)</p>
<ul>
<li>This addon can be used to create links that navigate between stories in Storybook.</li>
</ul>
</li>
<li><p><a target="_blank" href="https://storybook.js.org/integrations/tag/essentials">@storybook/addon-essentials</a> (optional, but highly recommended)</p>
<ul>
<li><p>Storybook ships by default with a set of “essential” addons that add to the initial user experience. There are many third-party add-ons as well as “official” add-ons developed by the Storybook core team, such as:</p>
<ul>
<li><p><a target="_blank" href="https://storybook.js.org/docs/react/writing-docs/introduction">Docs</a></p>
</li>
<li><p><a target="_blank" href="https://storybook.js.org/docs/react/essentials/controls">Controls</a></p>
</li>
<li><p><a target="_blank" href="https://storybook.js.org/docs/react/essentials/actions">Actions</a></p>
</li>
<li><p><a target="_blank" href="https://storybook.js.org/docs/react/essentials/viewport">Viewport</a></p>
</li>
<li><p><a target="_blank" href="https://storybook.js.org/docs/react/essentials/backgrounds">Backgrounds</a></p>
</li>
<li><p><a target="_blank" href="https://storybook.js.org/docs/react/essentials/toolbars-and-globals">Toolbars &amp; globals</a></p>
</li>
<li><p><a target="_blank" href="https://storybook.js.org/docs/react/essentials/measure-and-outline">Measure &amp; outline</a></p>
</li>
<li><p><a target="_blank" href="https://storybook.js.org/docs/react/essentials/highlight">Highlight</a></p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://storybook.js.org/addons/@storybook/addon-react-native-web">@storybook/addon-react-native-web</a> <strong>(Required)</strong></p>
<ul>
<li>This addon configures <code>@storybook/react</code> to display React Native (RN) projects using React Native for Web (RNW)</li>
</ul>
</li>
<li><p><a target="_blank" href="https://www.npmjs.com/package/@storybook/builder-webpack5">@storybook/builder-webpack5</a> &amp; @storybook/manager-webpack5 <strong>(Required)</strong></p>
<ul>
<li>Builder implemented with webpack5 to spin up a dev environment, compile your code into an executable bundle, and update the browser in real-time.</li>
</ul>
</li>
<li><p><a target="_blank" href="https://www.npmjs.com/package/@storybook/react">@storybook/react</a> <strong>(Required)</strong></p>
<ul>
<li>Storybook React renderer for the web</li>
</ul>
</li>
<li><p>Babel plugins &amp; presets <strong>(Required)</strong></p>
<ul>
<li><p><a target="_blank" href="https://www.npmjs.com/package/babel-plugin-react-docgen-typescript">babel-plugin-react-docgen-typescript</a>: Plugin to generate docgen data from React components written in TypeScript.</p>
</li>
<li><p><a target="_blank" href="https://www.npmjs.com/package/babel-plugin-react-native-web">babel-plugin-react-native-web:</a> Plugin that will alias react-native to react-native-web and exclude any modules not required by your app (keeping bundle size down).</p>
</li>
<li><p><a target="_blank" href="https://www.npmjs.com/package/metro-react-native-babel-preset">metro-react-native-babel-preset:</a> Babel preset for React Native applications. React Native itself uses this Babel preset by default when transforming your app's source code.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>Storybook also allows you to use Vite instead of Webpack, but I didn't try it yet. If you are interested on that kind of setup, check out <a target="_blank" href="https://storybook.js.org/docs/react/builders/vite">this link</a></p>
</blockquote>
<hr />
<h1 id="heading-refactoring-our-project-structure">Refactoring our project structure</h1>
<p>After finishing the previous post, you'll have a <code>.storybook</code> folder in the root of your project, containing all the config files for Storybook Native. However, since Storybook Web uses different plugins and addons, we'll need to create separate <code>main.ts</code> and <code>preview.ts</code> files.</p>
<p>So, instead of having this project structure:</p>
<pre><code class="lang-plaintext">/.storybook
-- (Configs for native)
-- stories
</code></pre>
<p>We'll have something like this:</p>
<pre><code class="lang-plaintext">/.storybook
-- stories
-- web (Folder for the web setup)
---- main.ts
---- preview.ts
-- native (Folder for the native setup)
---- index.ts (exports the Storybook UI)
---- main.ts
---- preview.ts
---- storybook.requires.js (autogenerated)
</code></pre>
<ol>
<li><p>Run the following command to create the required folders</p>
<pre><code class="lang-bash"> mkdir .storybook/web .storybook/native
</code></pre>
</li>
<li><p>Move the existing files inside the <code>native</code> folder</p>
<pre><code class="lang-bash"> mv .storybook/index.ts .storybook/native/index.ts
 mv .storybook/main.ts .storybook/native/main.ts
 mv .storybook/preview.ts .storybook/native/preview.ts
 mv .storybook/storybook.requires.js .storybook/native/storybook.requires.js
</code></pre>
</li>
<li><p>In the <code>native</code> folder, we need to update the <code>main.ts</code> file to look into the right paths:</p>
<pre><code class="lang-diff">module.exports = {
<span class="hljs-deletion">- stories: ["./stories/**/*.stories.?(ts|tsx|js|jsx)"],</span>
<span class="hljs-addition">+ stories: ["../stories/**/*.stories.?(ts|tsx|js|jsx)"],</span>
 addons: [
   "@storybook/addon-ondevice-controls",
   "@storybook/addon-ondevice-actions",
 ],
};
</code></pre>
</li>
<li><p>Create the <code>main.ts</code> and <code>preview.ts</code> files inside the <code>web</code> folder</p>
<pre><code class="lang-bash"> touch .storybook/web/main.ts .storybook/web/preview.ts
</code></pre>
</li>
</ol>
<hr />
<h1 id="heading-storybook-web-configurations">Storybook Web Configurations</h1>
<h2 id="heading-adding-config-files">Adding config files</h2>
<p>As I mentioned earlier, we need a different configuration for Storybook Web.</p>
<ol>
<li><p>Let's start by creating the <code>main.ts</code> and <code>preview.ts</code> files inside the <code>.storybook/web</code> directory:</p>
<pre><code class="lang-bash"> touch .storybook/web/main.ts
 touch .storybook/web/preview.ts
</code></pre>
</li>
<li><p>Then, add the following content to the <code>main.ts</code> file:</p>
<pre><code class="lang-bash"> module.exports = {
   stories: [
     <span class="hljs-string">"../../src/components/**/*.stories.mdx"</span>,
     <span class="hljs-string">"../../src/components/**/*.stories.@(js|jsx|ts|tsx)"</span>,
   ],
   addons: [
     <span class="hljs-string">"@storybook/addon-links"</span>,
     <span class="hljs-string">"@storybook/addon-essentials"</span>,
     <span class="hljs-string">"@storybook/addon-react-native-web"</span>,
   ],
   core: {
     builder: <span class="hljs-string">"webpack5"</span>,
   },
   framework: <span class="hljs-string">"@storybook/react"</span>,
 };
</code></pre>
<p> Here are a few things to mention about this configuration:</p>
<ul>
<li><p>It loads stories from the <code>src/components</code> directory with MDX, JS, JSX, TS, and TSX file extensions.</p>
</li>
<li><p>Includes essential add-ons, links, and React Native Web support.</p>
</li>
<li><p>Includes Webpack 5 setup to be used as the builder and set the framework to Storybook React.</p>
</li>
</ul>
</li>
<li><p>Finally, add the following content to the <code>preview.ts</code> file. Although this file is the same for both Web and Mobile at the moment, it's ok to keep these files separated. That way, we can apply different customizations for each platform as needed.</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> const parameters = {
   actions: { argTypesRegex: <span class="hljs-string">"^on[A-Z].*"</span> },
   controls: {
     matchers: {
       color: /(background|color)$/i,
       date: /Date$/,
     },
   },
 };
</code></pre>
</li>
</ol>
<h2 id="heading-setup-babel-to-generate-docs-for-typescript-components">Setup babel to generate docs for TypeScript components</h2>
<p>We need to add the <code>babel-plugin-react-docgen-typescript</code> plugin in our <code>babel.config.js</code> file, so we can get useful generated docs for our TypeScript components:</p>
<pre><code class="lang-diff">module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: [
      "react-native-reanimated/plugin",
      require.resolve("expo-router/babel"),
<span class="hljs-addition">+     ["babel-plugin-react-docgen-typescript", { exclude: "node_modules" }],</span>
    ],
  };
};
</code></pre>
<h2 id="heading-adding-npm-scripts-for-storybook-web">Adding NPM Scripts for Storybook web</h2>
<p>Remember how running <code>npm start</code> launches our native app? I wanted to add two more commands: one to run Storybook Web and another to build the web portal. The latter will be useful if you plan to deploy it to Vercel later.</p>
<blockquote>
<p>Note that I also had to update the npm start command to point to the new config path. Make sure to check that as well; otherwise, you'll get an error. 🙃</p>
</blockquote>
<pre><code class="lang-diff">{
  "scripts": {
<span class="hljs-deletion">-   "start": "sb-rn-get-stories &amp;&amp; expo start",</span>
<span class="hljs-addition">+   "start": "sb-rn-get-stories --config-path .storybook/native &amp;&amp; expo start",</span>
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "storybook-generate": "sb-rn-get-stories",
    "storybook-watch": "sb-rn-watcher",
<span class="hljs-addition">+   "storybook:web": "start-storybook --config-dir .storybook/web -p 6006",</span>
<span class="hljs-addition">+   "build-storybook": "build-storybook"</span>
  },
  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
  },
  ...
}
</code></pre>
<hr />
<h1 id="heading-final-touch">Final touch</h1>
<p>Go to the <code>app/index.tsx</code> file and update the require statement to point to the new <code>native</code> folder like this</p>
<pre><code class="lang-diff">import React from "react";
import { Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

const storybookEnabled = process.env.EXPO_PUBLIC_STORYBOOK_ENABLED <span class="hljs-comment">=== "true";</span>

const Index = () =&gt; {
  return (
    &lt;SafeAreaView&gt;
      &lt;Text&gt;Hello world&lt;/Text&gt;
    &lt;/SafeAreaView&gt;
  );
};

let EntryPoint = Index;

if (storybookEnabled) {
<span class="hljs-deletion">- const StorybookUI = require("../.storybook").default;</span>
<span class="hljs-addition">+ const StorybookUI = require("../.storybook/native").default;</span>
  EntryPoint = () =&gt; {
    return (
      &lt;View style={{ flex: 1 }}&gt;
        &lt;StorybookUI /&gt;
      &lt;/View&gt;
    );
  };
}

export default EntryPoint;
</code></pre>
<p>Last but not least, add the <code>storybook-static</code> directory to your .gitignore to prevent static files from being included in your source control.</p>
<pre><code class="lang-diff">node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
<span class="hljs-addition">+ storybook-static/</span>

# macOS
.DS_Store
</code></pre>
<p>That's it! Now run <code>npm run storybook:web</code> and enjoy!</p>
<hr />
<h1 id="heading-recap">Recap</h1>
<p>In this article, we installed additional dependencies and dealt with Babel configurations to successfully run Storybook web with our React Native components.</p>
<p>I love that everything related to Storybook configuration now resides in the <code>.storybook</code> folder, serving as a single source of truth (including the stories).</p>
<p>Now, you can write your stories once and run Storybook on both native and web platforms. 🪄</p>
<p>Additionally, you can publish your Storybook Web portal to Vercel using the <code>build</code> script.</p>
<hr />
<p>If you found this article helpful, please let me know in the comments section or hit the like button, so I'll continue writing about this topic.</p>
<p>Happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Using Storybook with Expo Router v2, SDK 49 & TypeScript]]></title><description><![CDATA[Introduction
Imagine having a web portal to showcase, document, test, and improve all your React Native components. A place where you can build your own library over time and then use that library to quickly build and release all those app ideas you ...]]></description><link>https://blog.spirokit.com/using-storybook-with-expo-router-v2-sdk-49-typescript</link><guid isPermaLink="true">https://blog.spirokit.com/using-storybook-with-expo-router-v2-sdk-49-typescript</guid><category><![CDATA[React Native]]></category><category><![CDATA[Expo]]></category><category><![CDATA[Storybook]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Thu, 13 Jul 2023 16:14:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1689205072950/14b8b2b1-f269-417b-8f1b-324058a2909e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Imagine having a web portal to showcase, document, test, and improve all your React Native components. A place where you can build your own library over time and then use that library to quickly build and release all those app ideas you have.</p>
<p>If that sounds appealing to you, you'll love Storybook.</p>
<p>In this post, I'll talk about what Storybook is, how it can help you, and how to add it to your Expo project, taking advantage of Expo SDK 49 new features and Expo Router v2.</p>
<p>At the end of the post, you’ll have a working repo with Expo SDK 49, Storybook for React Native, Expo Router v2, TypeScript, and more!</p>
<hr />
<h1 id="heading-show-me-the-code">Show me the code</h1>
<p>If you already know Storybook, and you just want a working repo to fork, here is <a target="_blank" href="https://github.com/mauro-codes/expo-router-storybook-starter">the link to the Github repo</a></p>
<hr />
<h1 id="heading-what-is-storybook-and-how-can-it-help-you">What is Storybook, and how can it help you?</h1>
<p>Storybook is an open-source tool for building UI components and pages in isolation. In other words, it's a library you can add to your project to test and document your components.</p>
<p>Are you still confused? No worries. I had to read the official documentation and search for real-life examples before genuinely understanding how powerful this tool is. So, follow me with this basic example.</p>
<p>Let's say we have a simple "Button" component. It receives the following props:</p>
<ul>
<li>text (string)</li>
<li>disabled (boolean)</li>
<li>onPress (function)</li>
</ul>
<p>If you are working for a company, chances are that you'll have to communicate how your new component works to the rest of the team.</p>
<p>Besides, your teammates must know that your Button component exists to avoid code duplication.</p>
<p>Last but not least, you will need to test your component with different props combinations to make sure everything works as expected. For example, what happens if we use a really long text for our button? Is our component responsive?</p>
<p>Storybook is the solution to all the problems I mentioned before. It helps you build a centralized component library with rich and interactive documentation so your teammates can reuse or even improve it.</p>
<hr />
<h1 id="heading-setup-options">Setup options</h1>
<p>There are two main ways to use Storybook with Expo. Based on what you choose, the installation process will be different.</p>
<h2 id="heading-storybook-web">Storybook web</h2>
<ul>
<li>You work on your React Native components as usual and reference those components as Stories that can be rendered directly into the browser.</li>
<li>You setup your Storybook project using the <code>react</code> starter</li>
<li>Then, you use Webpack to transpile your native modules into something you can run on your browser</li>
<li><strong>Pros</strong><ul>
<li>You can publish your Storybook web portal and share it with your teammates. Everyone can access the documentation and play with the components without having to install anything.</li>
</ul>
</li>
<li><strong>Cons</strong><ul>
<li>Any native component like a Date time picker won't be rendered. For those components, you will need to use the other method described below.</li>
<li>Even if you test your UI in the browser, some issues will only appear while running on a native device, so it’s not completely reliable.</li>
</ul>
</li>
</ul>
<h2 id="heading-storybook-native">Storybook native</h2>
<ul>
<li>You replace the entry point of your React Native app with the Storybook UI. Everything is presented directly within your native device.</li>
<li>You setup your Storybook project using the <code>react-native</code> starter</li>
<li>Then, you tweak metro bundler to handle the Storybook UI, which is rendered on a native device.</li>
<li><strong>Pros</strong><ul>
<li>You don't have any limitations to render native components like Date Time Picker because everything runs on your phone.</li>
<li>The tests are completely reliable because you are running your components on a native device</li>
</ul>
</li>
<li><strong>Cons</strong><ul>
<li>Reading documentation directly from your phone is not the best option if you want to promote collaboration within your team. Having a web interface is always better for developers who will spend hours a day working with a design system.</li>
</ul>
</li>
</ul>
<p>This article will be focused on showing how to run Storybook on a native device, but let me know if you are interested in learning how to build and deploy a web interface to share with the rest of the team. Maybe I could write about it in the next post. </p>
<hr />
<h1 id="heading-setting-up-an-expo-project-with-expo-router-v2-and-sdk-49">Setting up an Expo project with Expo Router v2 and SDK 49</h1>
<p>As of the writing of this article (July, 2023), I couldn’t find an official expo template that comes with TypeScript and Expo Router v2 configured, so let’s start by using the vanilla JS Expo Router starter and add TypeScript later.</p>
<ol>
<li><p>Run the following command to create an empty project with Expo Router configured.</p>
<pre><code class="lang-bash"> npx create-expo-app@latest -e with-router
</code></pre>
</li>
<li><p>Because this starter comes with Expo SDK 48, we’ll need to run the following command to update Expo to SDK 49 </p>
<pre><code class="lang-bash"> expo upgrade
</code></pre>
<blockquote>
<p>Make sure to check if the starter already comes with SDK 49. Maybe it’s not necessary to do the upgrade anymore (The Expo team moves really fast)</p>
<p>You may need to install the <code>expo-cli</code> globally before running the upgrade. If that’s the case, run <code>npm i -g expo-cli</code></p>
</blockquote>
</li>
<li><p>Based on Expo official docs, for Expo Router v2 is no longer necessary to include the <code>resolutions</code> and <code>overrides</code> sections in the <code>package.json</code>, so we can delete them like this:</p>
<pre><code class="lang-diff">{
 "scripts": {
   "start": "expo start",
   "android": "expo start --android",
   "ios": "expo start --ios",
   "web": "expo start --web"
 },
 "dependencies": {
   ...
 },
 "devDependencies": {
   "@babel/core": "^7.20.0",
   "@babel/plugin-proposal-export-namespace-from": "^7.18.9"
 },
<span class="hljs-deletion">-"resolutions": {</span>
<span class="hljs-deletion">-  "metro": "^0.73.7",</span>
<span class="hljs-deletion">-  "metro-resolver": "^0.73.7"</span>
<span class="hljs-deletion">-},</span>
<span class="hljs-deletion">-"overrides": {</span>
<span class="hljs-deletion">-  "metro": "^0.73.7",</span>
<span class="hljs-deletion">-  "metro-resolver": "^0.73.7"</span>
<span class="hljs-deletion">-},</span>
 "name": "expo-router-storybook-starter",
 "version": "1.0.0",
 "private": true
}
</code></pre>
</li>
<li><p>Run the following commands to create an empty <code>src</code> folder where our components will be added, and an <code>app</code> folder for our application routes.</p>
<pre><code class="lang-bash"> mkdir src
 mkdir app
</code></pre>
</li>
</ol>
<hr />
<h1 id="heading-adding-typescript">Adding TypeScript</h1>
<ol>
<li><p>Create an empty <code>tsconfig.json</code> by running this command</p>
<pre><code class="lang-bash"> touch tsconfig.json
</code></pre>
</li>
<li><p>With the <code>tsconfig.json</code> created, you can run <code>npx expo start</code>. Expo will detect a <code>tsconfig.json</code> file, ask you to install the missing dependencies, and handle the required setup for you, inheriting a few defaults from Expo’s base config.</p>
</li>
<li>Update the <code>package.json</code> to move both <code>typescript</code> and <code>@types/react</code> dependencies under <code>devDependencies</code>. </li>
</ol>
<hr />
<h1 id="heading-adding-support-for-path-aliases-optional">Adding support for Path Aliases (Optional)</h1>
<p>Expo SDK 49 introduces support for Path Aliases. You can optionally follow these steps to setup a path alias for the <code>src</code> directory</p>
<ol>
<li><p>Update your <code>app.json</code> by adding the “experiments” section like this: </p>
<pre><code class="lang-diff">{
 "expo": {
   "scheme": "acme",
   "web": {
     "bundler": "metro"
   },
   "name": "expo-router-storybook-starter",
   "slug": "expo-router-storybook-starter",
<span class="hljs-addition">+  "experiments": {</span>
<span class="hljs-addition">+    "tsconfigPaths": true</span>
<span class="hljs-addition">+  }</span>
 }
}
</code></pre>
</li>
<li><p>Update the <code>tsconfig.json</code> to apply this configuration to our <code>src</code> directory.</p>
<pre><code class="lang-diff">{
 "compilerOptions": {
<span class="hljs-addition">+  "baseUrl": ".",</span>
<span class="hljs-addition">+  "paths": {</span>
<span class="hljs-addition">+    "@/*": ["src/*"]</span>
<span class="hljs-addition">+  }</span>
 },
 "extends": "expo/tsconfig.base"
}
</code></pre>
</li>
</ol>
<p>That’s it! Now you can import your components like this:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> Button <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/Button'</span>;
</code></pre>
<p>Instead of doing this:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> Button <span class="hljs-keyword">from</span> <span class="hljs-string">'../../components/Button'</span>
</code></pre>
<hr />
<h1 id="heading-adding-storybook-to-the-project">Adding Storybook to the project</h1>
<p>Thanks to the amazing Storybook CLI, adding Storybook to your project is as simple as executing the following command</p>
<pre><code class="lang-diff">npx storybook@latest init
</code></pre>
<p>A few things I would like to mention about this process:</p>
<ul>
<li>Storybook will detect your framework and add all the required dependencies to your project</li>
<li>It will also add a few essential add-ons like:<ul>
<li><a target="_blank" href="https://www.npmjs.com/package/@storybook/addon-actions">@storybook/addon-actions</a><ul>
<li>Can be used to display data received by event handlers in <strong><a target="_blank" href="https://storybook.js.org/">Storybook</a></strong>.</li>
</ul>
</li>
<li><a target="_blank" href="https://www.npmjs.com/package/@storybook/addon-controls">@storybook/addon-controls</a><ul>
<li>Gives you a graphical UI to interact with a component's arguments dynamically, without needing to code. It creates an addon panel next to your component examples ("stories"), so you can edit them live.</li>
</ul>
</li>
<li><a target="_blank" href="https://www.npmjs.com/package/@storybook/addon-ondevice-actions">@storybook/addon-ondevice-actions</a><ul>
<li>Allows you to log events/actions inside stories in Storybook while running on a mobile device</li>
</ul>
</li>
<li><a target="_blank" href="https://www.npmjs.com/package/@storybook/addon-ondevice-controls">@storybook/addon-ondevice-controls</a><ul>
<li>Allows editing a component's arguments dynamically via a graphical UI without needing to code while running on a mobile device</li>
</ul>
</li>
</ul>
</li>
<li>You’ll see a new folder called <code>.storybook</code> that will include all the initial configs so you can run Storybook with the essential add-ons activated.<ul>
<li>Here you’ll also find a Button component with their stories. We’ll talk more about this component later</li>
</ul>
</li>
</ul>
<hr />
<h1 id="heading-opting-out-from-package-version-validations-for-storybook-dependencies">Opting out from package version validations for Storybook dependencies</h1>
<p>During setup, Storybook will install a few additional dependencies that, at the time of this writing, will throw a warning when you run <code>npx expo start</code> due to a mismatch between the installed versions and the ones expected by Expo. I’m talking about <code>@react-native-async-storage/async-storage</code> and <code>@react-native-community/datetimepicker</code></p>
<p>Because I want to keep these versions, I decided to take advantage of a new feature released with SDK 49, which allows you to opt out from package version validations for a list of dependencies.</p>
<blockquote>
<p>Feel free to run <code>expo doctor --fix</code> if you want to downgrade your dependencies to the version expected by Expo.</p>
</blockquote>
<p>To use this new feature, we need to update the <code>package.json</code>, adding an array with the dependencies we want to exclude from the validation.</p>
<pre><code class="lang-diff">{
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "storybook-generate": "sb-rn-get-stories",
    "storybook-watch": "sb-rn-watcher"
  },
  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
  },
<span class="hljs-addition">+ "expo": {</span>
<span class="hljs-addition">+   "install": {</span>
<span class="hljs-addition">+     "exclude": [</span>
<span class="hljs-addition">+       "@react-native-async-storage/async-storage",</span>
<span class="hljs-addition">+       "@react-native-community/datetimepicker"</span>
<span class="hljs-addition">+     ]</span>
<span class="hljs-addition">+   }</span>
<span class="hljs-addition">+ },</span>
  "name": "expo-router-storybook-starter",
  "version": "1.0.0",
  "private": true
}
</code></pre>
<p>You can now run <code>npm start</code> and verify you no longer see the previous warnings</p>
<hr />
<h1 id="heading-using-client-environment-variables-to-conditionally-load-storybook-ui">Using client environment variables to conditionally load Storybook UI</h1>
<p>I wanted to use an environment variable to enable/disable the Storybook UI based on my needs during development, so I decided to try another new feature included in SDK 49: Client environment variables.</p>
<blockquote>
<p>If you want to learn more about Environment variables in Expo, checkout the <a target="_blank" href="https://docs.expo.dev/guides/environment-variables/#migrating-to-expo-environment-variables">amazing official docs</a> by the Expo team. They even included steps to migrate from different tools like <code>direnv</code> or <code>react-native-config</code> 🙌</p>
</blockquote>
<p>When you use client environment variables, you can simply create an <code>.env</code> file, and add your keys using the <code>EXPO_PUBLIC_</code> prefix, and Expo will automatically expose those keys through Metro. That means we no longer need the <code>transform-inline-environment-variables</code> babel plugin.</p>
<ol>
<li><p>Remove the mentioned plugin from <code>babel.config.js</code></p>
<pre><code class="lang-diff">module.exports = function (api) {
 api.cache(true);
 return {
   presets: ["babel-preset-expo"],
   plugins: [
<span class="hljs-deletion">-    "transform-inline-environment-variables",</span>
     "react-native-reanimated/plugin",
     require.resolve("expo-router/babel"),
   ],
 };
};
</code></pre>
</li>
<li><p>Create an empty <code>.env</code> file</p>
<pre><code class="lang-bash"> touch .env
</code></pre>
</li>
<li><p>Update the new <code>.env</code> file, adding the following environment variable</p>
<pre><code class="lang-jsx"> EXPO_PUBLIC_STORYBOOK_ENABLED=<span class="hljs-literal">true</span>
</code></pre>
</li>
<li><p>Because we are using Expo Router, we need to create a <a target="_blank" href="https://docs.expo.dev/routing/layouts/">layout route</a>. On this route, we’ll simply export a React component with a <code>Slot</code>, so Expo Router can render the child route there.</p>
<pre><code class="lang-bash"> touch app/_layout.tsx
</code></pre>
<pre><code class="lang-jsx"> <span class="hljs-comment">// app/_layout.tsx</span>

 <span class="hljs-keyword">import</span> { Slot } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;

 <span class="hljs-keyword">let</span> RootApp = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Slot</span> /&gt;</span></span>;
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> RootApp;
</code></pre>
</li>
<li><p>Then, add a new <code>Index.tsx</code> file inside the <code>app</code> directory. In this file, we’ll check the value of the environment variable to decide what to render</p>
<pre><code class="lang-bash"> touch app/Index.tsx
</code></pre>
<pre><code class="lang-jsx"> <span class="hljs-comment">// app/Index.tsx</span>
 <span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
 <span class="hljs-keyword">import</span> { Text, View } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
 <span class="hljs-keyword">import</span> { SafeAreaView } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-safe-area-context"</span>;

 <span class="hljs-keyword">const</span> storybookEnabled = process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === <span class="hljs-string">"true"</span>;

 <span class="hljs-keyword">const</span> Index = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">return</span> (
     <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">SafeAreaView</span>&gt;</span>
       <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Hello world<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
     <span class="hljs-tag">&lt;/<span class="hljs-name">SafeAreaView</span>&gt;</span></span>
   );
 };

 <span class="hljs-keyword">let</span> EntryPoint = Index;

 <span class="hljs-keyword">if</span> (storybookEnabled) {
   <span class="hljs-keyword">const</span> StorybookUI = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../.storybook"</span>).default;
   EntryPoint = <span class="hljs-function">() =&gt;</span> {
     <span class="hljs-keyword">return</span> (
       <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">flex:</span> <span class="hljs-attr">1</span> }}&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">StorybookUI</span> /&gt;</span>
       <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span></span>
     );
   };
 }

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> EntryPoint;
</code></pre>
</li>
</ol>
<hr />
<h1 id="heading-updating-metro-config">Updating metro config</h1>
<p>Based on Storybook docs, we also need to customize our <code>metro.config.js</code> to use the <code>sbmodern</code> resolver field in order to resolve the modern version of storybook packages. Doing this removes the polyfills that ship in commonjs modules and fixes multiple long-standing issues such as the promises never resolving bug and more (caused by corejs promises polyfill).</p>
<ol>
<li><p>Create a new <code>metro.config.js</code> file</p>
<pre><code class="lang-bash"> touch metro.config.js
</code></pre>
</li>
<li><p>Use the following configuration</p>
<pre><code class="lang-js"> <span class="hljs-keyword">const</span> { getDefaultConfig } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"expo/metro-config"</span>);

 <span class="hljs-built_in">module</span>.exports = (<span class="hljs-keyword">async</span> () =&gt; {
   <span class="hljs-keyword">let</span> defaultConfig = <span class="hljs-keyword">await</span> getDefaultConfig(__dirname);
   defaultConfig.resolver.resolverMainFields.unshift(<span class="hljs-string">"sbmodern"</span>);
   <span class="hljs-keyword">return</span> defaultConfig;
 })();
</code></pre>
</li>
</ol>
<hr />
<h1 id="heading-loading-storybook-with-npm-start">Loading Storybook with <code>npm start</code></h1>
<p>We’ll need to update our <code>start</code> script so Storybook can load the stories before the app starts.</p>
<pre><code class="lang-diff">{
  "scripts": {
<span class="hljs-deletion">-   "start": "expo start",</span>
<span class="hljs-addition">+   "start": "sb-rn-get-stories &amp;&amp; expo start",</span>
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "storybook-generate": "sb-rn-get-stories",
    "storybook-watch": "sb-rn-watcher"
  },
  ...
}
</code></pre>
<p>With everything in place, we should be able to run our app and see the Storybook UI like this:</p>
<p><img src="https://i.imgur.com/t0KSgqp.jpg" alt="Storybook UI in React Native" /></p>
<p>If you now go to your <code>.env</code> file and set <code>EXPO_PUBLIC_STORYBOOK_ENABLED</code> to <code>false</code>, you can hit reload on your running app, and instantly see how your app loads instead of the Storybook UI 🪄</p>
<blockquote>
<p>After running your app for the first time with Storybook, you should see a new file that was automatically generated by Storybook. I’m talking about the <code>storybook.requires.js</code> file. Please avoid changing this file.</p>
</blockquote>
<p>We could stop here, but I wanted to add a few stories using TypeScript, so everything is set up correctly. Let’s work on that!</p>
<hr />
<h1 id="heading-cleaning-up">Cleaning up</h1>
<p>One thing I don’t like about the default Storybook setup, is that it adds two example files: <code>.storybook/stories/Button.js</code> and <code>.storybook/stories/Button.stories.js</code>. If you are new to React Native and to Storybook, you may think it’s a good idea to store all your components directly in the <code>.storybook/stories</code> directory. Instead, I want to create a <code>Button.tsx</code> component inside <code>src/components</code>, and then import that component into my Storybook story.</p>
<ol>
<li><p>Create the new <code>components</code> folder inside the <code>src</code> directory</p>
<pre><code class="lang-bash"> mkdir src/components
</code></pre>
</li>
<li><p>Move the <code>Button.js</code> file into the new directory, and rename it so you can use TypeScript</p>
<pre><code class="lang-bash"> mv .storybook/stories/Button/Button.js src/components/Button.tsx
</code></pre>
</li>
<li><p>Move <code>Button.stories.js</code> to the <code>.storybook/stories</code> directory, and rename it to use TypeScript</p>
<pre><code class="lang-bash"> mv .storybook/stories/Button/Button.stories.js .storybook/stories/Button.stories.tsx
</code></pre>
</li>
<li><p>Remove the Button directory, as it is no longer needed</p>
<pre><code class="lang-bash"> rm -rf .storybook/stories/Button
</code></pre>
</li>
<li><p>Rename a few more files to use TypeScript</p>
<pre><code class="lang-bash"> mv .storybook/main.js .storybook/main.ts
 mv .storybook/preview.js .storybook/preview.ts
 mv .storybook/index.js .storybook/index.ts
</code></pre>
</li>
</ol>
<hr />
<h1 id="heading-using-typescript-within-components-and-stories">Using TypeScript within components and Stories</h1>
<ol>
<li><p>Update the <code>Button.tsx</code> component to add a type for the props, and also add an additional flag  for the disabled state</p>
<pre><code class="lang-jsx"> <span class="hljs-comment">// src/components/Button.tsx</span>

 <span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
 <span class="hljs-keyword">import</span> { TouchableOpacity, Text, StyleSheet } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

 <span class="hljs-keyword">export</span> type MyButtonProps = {
   onPress?: <span class="hljs-function">() =&gt;</span> <span class="hljs-keyword">void</span>;
   text: string;
   disabled?: boolean;
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> MyButton: React.FC&lt;MyButtonProps&gt; = <span class="hljs-function">(<span class="hljs-params">{
   onPress,
   text,
   disabled
 }</span>) =&gt;</span> {
   <span class="hljs-keyword">return</span> (
     <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">TouchableOpacity</span>
       <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.container,</span> { <span class="hljs-attr">opacity:</span> <span class="hljs-attr">disabled</span> ? <span class="hljs-attr">0.3</span> <span class="hljs-attr">:</span> <span class="hljs-attr">1</span> }]}
       <span class="hljs-attr">onPress</span>=<span class="hljs-string">{onPress}</span>
       <span class="hljs-attr">activeOpacity</span>=<span class="hljs-string">{0.8}</span>
       <span class="hljs-attr">disabled</span>=<span class="hljs-string">{disabled}</span>
     &gt;</span>
       <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.text}</span>&gt;</span>
         {text}
       <span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
     <span class="hljs-tag">&lt;/<span class="hljs-name">TouchableOpacity</span>&gt;</span></span>
   );
 };

 <span class="hljs-keyword">const</span> styles = StyleSheet.create({
   <span class="hljs-attr">container</span>: {
     <span class="hljs-attr">paddingHorizontal</span>: <span class="hljs-number">16</span>,
     <span class="hljs-attr">paddingVertical</span>: <span class="hljs-number">8</span>,
     <span class="hljs-attr">backgroundColor</span>: <span class="hljs-string">"purple"</span>,
     <span class="hljs-attr">borderRadius</span>: <span class="hljs-number">8</span>,
   },
   <span class="hljs-attr">text</span>: { <span class="hljs-attr">color</span>: <span class="hljs-string">"white"</span> },
 });
</code></pre>
</li>
<li><p>Update the <code>Button.stories.tsx</code> file to reference the Button component from the new location, and we’ll also use a few types from Storybook to get proper IntelliSense for the args on each story.</p>
<pre><code class="lang-jsx"> <span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
 <span class="hljs-keyword">import</span> { View } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
 <span class="hljs-keyword">import</span> { MyButton, MyButtonProps } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../src/components/Button"</span>;
 <span class="hljs-keyword">import</span> { Meta, StoryObj } <span class="hljs-keyword">from</span> <span class="hljs-string">"@storybook/react-native"</span>;

 <span class="hljs-keyword">const</span> meta: Meta&lt;MyButtonProps&gt; = {
   <span class="hljs-attr">title</span>: <span class="hljs-string">"Button"</span>,
   <span class="hljs-attr">component</span>: MyButton,
   <span class="hljs-attr">argTypes</span>: {
     <span class="hljs-attr">onPress</span>: {
       <span class="hljs-attr">action</span>: <span class="hljs-string">"onPress event"</span>,
     },
   },

   <span class="hljs-attr">decorators</span>: [
     <span class="hljs-function">(<span class="hljs-params">Story</span>) =&gt;</span> (
       <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">flex:</span> <span class="hljs-attr">1</span>, <span class="hljs-attr">justifyContent:</span> "<span class="hljs-attr">center</span>", <span class="hljs-attr">alignItems:</span> "<span class="hljs-attr">center</span>" }}&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">Story</span> /&gt;</span>
       <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span></span>
     ),
   ],
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> meta;

 type Story = StoryObj&lt;MyButtonProps&gt;;

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Basic: Story = {
   <span class="hljs-attr">storyName</span>: <span class="hljs-string">"Basic"</span>,
   <span class="hljs-attr">args</span>: {
     <span class="hljs-attr">disabled</span>: <span class="hljs-literal">false</span>,
     <span class="hljs-attr">text</span>: <span class="hljs-string">"Tap me"</span>,
   },
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Disabled: Story = {
   <span class="hljs-attr">args</span>: {
     <span class="hljs-attr">disabled</span>: <span class="hljs-literal">true</span>,
     <span class="hljs-attr">text</span>: <span class="hljs-string">"Disabled"</span>,
   },
 };
</code></pre>
<ul>
<li><p>A few things to mention here:</p>
<ul>
<li>We are using the <code>Meta</code> type from Storybook to get better intelliSense for args and argTypes.</li>
<li>We also added decorators to wrap and center stories within a View.</li>
</ul>
<blockquote>
<p>You can use the <code>argTypes</code> object inside <code>meta</code> to describe what control you want for each arg. If you want to learn more about available controls, checkout <a target="_blank" href="https://storybook.js.org/docs/6.5/react/essentials/controls#annotation">this link</a></p>
</blockquote>
</li>
</ul>
</li>
</ol>
<hr />
<h1 id="heading-conclusions-and-closing-words">Conclusions and closing words</h1>
<p>I hope you find this post valuable! I would like to mention that the starter React Native template from Storybook was an amazing starting point. I just wanted to go the extra mile to set up everything with SDK 49, Expo Router, and TypeScript.</p>
<p>I would also like to thank:</p>
<ul>
<li><a target="_blank" href="https://twitter.com/oxeltrabeton">oxeltra_beton</a> for giving me the idea to combine Storybook, Expo Router v2, and SDK 49 and write about it.</li>
<li><a target="_blank" href="https://twitter.com/Danny_H_W">Daniel Williams</a> for all his contributions to the React Native and Storybook communities. I remember doing similar experiments with Expo and Storybook a few years ago, and I can't believe how much easier it was this time around. Thanks again, Daniel!</li>
</ul>
<p>Please let me know if I’m missing something, or if I’m making a silly mistake in the process. I’m here to learn and improve.</p>
<p>Happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Maps in React Native: Adding interactive markers]]></title><description><![CDATA[Introduction
In a previous article, we covered the initial setup process for a React Native project and explained how to configure a basic MapView component using the react-native-maps library. If you haven't read that article yet, I highly recommend...]]></description><link>https://blog.spirokit.com/maps-in-react-native-adding-interactive-markers</link><guid isPermaLink="true">https://blog.spirokit.com/maps-in-react-native-adding-interactive-markers</guid><category><![CDATA[React]]></category><category><![CDATA[React Native]]></category><category><![CDATA[google maps]]></category><category><![CDATA[Expo]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Thu, 29 Jun 2023 22:15:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1688076790557/85db0a80-9d03-4393-a1f5-98a8ac16522b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p><a target="_blank" href="https://blog.spirokit.com/maps-in-react-native-a-step-by-step-guide">In a previous article</a>, we covered the initial setup process for a React Native project and explained how to configure a basic <strong><code>MapView</code></strong> component using the <strong><code>react-native-maps</code></strong> library. If you haven't read that article yet, I highly recommend you check it out. </p>
<p>This time, I’ll skip the project setup and basic <strong><code>MapView</code></strong> configuration and focus on adding markers, customizing their icons and callouts, implementing animations, and handling marker interactions.</p>
<h1 id="heading-adding-interactive-markers">Adding interactive markers</h1>
<p>Markers are an essential component in <strong><code>react-native-maps</code></strong> that allow us to place points of interest on the map. Each marker can have various properties, such as coordinates (latitude and longitude), title, description, and even custom icons.</p>
<p>To add markers to the <strong><code>MapView</code></strong>, we first need to define their properties. Let's start by updating the <code>App.tsx</code> from the previous article.</p>
<pre><code class="lang-diff">import React from "react";
import { StyleSheet } from "react-native";
<span class="hljs-deletion">- import MapView, { PROVIDER_GOOGLE } from "react-native-maps";</span>
<span class="hljs-addition">+ import MapView, { Callout, Marker, PROVIDER_GOOGLE } from "react-native-maps";</span>

export default function App() {
<span class="hljs-addition">+ const markers = [</span>
<span class="hljs-addition">+   {</span>
<span class="hljs-addition">+     coordinate: {</span>
<span class="hljs-addition">+       latitude: -34.603851,</span>
<span class="hljs-addition">+       longitude: -58.381775,</span>
<span class="hljs-addition">+     },</span>
<span class="hljs-addition">+     title: "Obelisco",</span>
<span class="hljs-addition">+     description:</span>
<span class="hljs-addition">+       "The Obelisco is an iconic monument located in Buenos Aires, Argentina. Standing tall at the intersection of Avenida 9 de Julio and Avenida Corrientes, it serves as a symbol of the city and a tribute to its historical and cultural significance.",</span>
<span class="hljs-addition">+   },</span>
<span class="hljs-addition">+   {</span>
<span class="hljs-addition">+     coordinate: {</span>
<span class="hljs-addition">+       latitude: -34.6011,</span>
<span class="hljs-addition">+       longitude: -58.3835,</span>
<span class="hljs-addition">+     },</span>
<span class="hljs-addition">+     title: "Teatro Colón",</span>
<span class="hljs-addition">+     description:</span>
<span class="hljs-addition">+       "Teatro Colón, also known as the Colon Theatre, is a world-renowned opera house situated in Buenos Aires, Argentina. With its stunning architecture and rich history, it is considered one of the finest opera houses globally, hosting exceptional performances and captivating audiences with its grandeur.",</span>
<span class="hljs-addition">+   },</span>
<span class="hljs-addition">+   // Add more markers as needed</span>
<span class="hljs-addition">+ ];</span>

<span class="hljs-addition">+ const renderMarkers = () =&gt; {</span>
<span class="hljs-addition">+   return markers.map((marker, index) =&gt; (</span>
<span class="hljs-addition">+     &lt;Marker</span>
<span class="hljs-addition">+       key={index}</span>
<span class="hljs-addition">+       coordinate={marker.coordinate}</span>
<span class="hljs-addition">+       title={marker.title}</span>
<span class="hljs-addition">+       description={marker.description}</span>
<span class="hljs-addition">+     /&gt;</span>
<span class="hljs-addition">+   ));</span>
<span class="hljs-addition">+ };</span>

  return (
    &lt;MapView
      provider={PROVIDER_GOOGLE} // Specify Google Maps as the provider
      style={styles.map}
      initialRegion={{
        latitude: -34.603738,
        longitude: -58.38157,
        latitudeDelta: 0.01,
        longitudeDelta: 0.01,
      }}
    &gt;
<span class="hljs-addition">+     {renderMarkers()}</span>
    &lt;/MapView&gt;
  );
}

const styles = StyleSheet.create({
  map: {
    flex: 1,
  },
});
</code></pre>
<p>A few things to mention about the code snippet above:</p>
<ul>
<li>We updated our <code>App.tsx</code> to define an array of markers, each with its own <strong><code>coordinate</code></strong>, <strong><code>title</code></strong>, and <strong><code>description</code></strong>.</li>
<li>We then use the <code>renderMarkers</code> function to render a <strong><code>&lt;Marker&gt;</code></strong> component for each item in the <strong><code>markers</code></strong> array, passing in the corresponding properties.</li>
<li>By including the <strong><code>&lt;Marker&gt;</code></strong> components within the <strong><code>&lt;MapView&gt;</code></strong> component, the markers will be displayed on the map at their respective coordinates.</li>
</ul>
<p>If everything went ok, you should see something like this:</p>
<p><img src="https://i.imgur.com/PqQZHw3.png" alt="Markers in react native maps with default callout" /></p>
<p>That’s fine for a basic scenario, but if you are like me, you may think that we could improve the UI for that “tooltip” bubble that appears after pressing a marker. That tooltip is called “Callout”, and <code>react-native-maps</code> includes a <code>&lt;Callout&gt;</code> component we can use to wrap our custom card, so let’s create a <code>&lt;CustomCallout&gt;</code> component!</p>
<h1 id="heading-customizing-the-callout-component">Customizing the Callout component</h1>
<ol>
<li><p>Before working on our new component, we’ll need to update our array of markers to add a new property called <code>imageUrl</code>. We’ll use this URL to display an Image for each point of interest. We are also adding a new type called <code>MarkerWithMetadata</code> to get better IntelliSense later in the Callout component.</p>
<pre><code class="lang-diff">// App.tsx

<span class="hljs-addition">+ export type MarkerWithMetadata = {</span>
<span class="hljs-addition">+   coordinate: MapMarkerProps["coordinate"];</span>
<span class="hljs-addition">+   title?: MapMarkerProps["title"];</span>
<span class="hljs-addition">+   description?: MapMarkerProps["description"];</span>
<span class="hljs-addition">+   imageUrl?: string;</span>
<span class="hljs-addition">+ };</span>

export default function App() {

 const markers: MarkerWithMetadata[] = [
   {
     coordinate: {
       latitude: -34.603851,
       longitude: -58.381775,
     },
     title: "Obelisco",
<span class="hljs-addition">+     imageUrl: "https://upload.wikimedia.org/wikipedia/commons/f/fc/Buenos_Aires_%2820234294752%29.jpg",</span>
     description: "The Obelisco is an iconic monument located in Buenos Aires, Argentina. Standing tall at the intersection of Avenida 9 de Julio and Avenida Corrientes, it serves as a symbol of the city and a tribute to its historical and cultural significance.",
   },
   {
     coordinate: {
       latitude: -34.6011,
       longitude: -58.3835,
     },
<span class="hljs-addition">+     imageUrl: "https://upload.wikimedia.org/wikipedia/commons/1/1e/Buenos_Aires_Teatro_Colon_2.jpg",</span>
     title: "Teatro Colón",
     description: "Teatro Colón, also known as the Colon Theatre, is a world-renowned opera house situated in Buenos Aires, Argentina. With its stunning architecture and rich history, it is considered one of the finest opera houses globally, hosting exceptional performances and captivating audiences with its grandeur.",
   },
   // Add more markers as needed
 ];

 ...
}
</code></pre>
</li>
<li><p>Create a new folder called <code>components</code> and an empty file inside for our new component by running the following command on your terminal</p>
<pre><code class="lang-bash"> mkdir components
 touch components/CustomCallout.tsx
</code></pre>
</li>
<li><p>Now, open the <code>CustomCallout</code> file and add the following code</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
 <span class="hljs-keyword">import</span> { View, StyleSheet, Dimensions, Image, Text } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
 <span class="hljs-keyword">import</span> { Callout } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-maps"</span>;
 <span class="hljs-keyword">import</span> { MarkerWithMetadata } <span class="hljs-keyword">from</span> <span class="hljs-string">"../App"</span>;

 <span class="hljs-keyword">const</span> screenWidth = Dimensions.get(<span class="hljs-string">"window"</span>).width;

 <span class="hljs-keyword">const</span> CustomCallout: React.FC&lt;{
   marker: MarkerWithMetadata;
 }&gt; = <span class="hljs-function">(<span class="hljs-params">{ marker }</span>) =&gt;</span> {
   <span class="hljs-keyword">return</span> (
     &lt;Callout tooltip&gt;
       &lt;View&gt;
         &lt;View style={styles.container}&gt;
           &lt;Image
             source={{
               uri: marker.imageUrl,
             }}
             resizeMode=<span class="hljs-string">"cover"</span>
             style={{ width: <span class="hljs-number">100</span>, height: <span class="hljs-string">"100%"</span> }}
           &gt;&lt;/Image&gt;
           &lt;View style={{ paddingHorizontal: <span class="hljs-number">16</span>, paddingVertical: <span class="hljs-number">8</span>, flex: <span class="hljs-number">1</span> }}&gt;
             &lt;Text
               style={{
                 fontWeight: <span class="hljs-string">"bold"</span>,
                 fontSize: <span class="hljs-number">18</span>,
               }}
             &gt;
               {marker.title}
             &lt;/Text&gt;

             &lt;Text&gt;{marker.description}&lt;/Text&gt;
           &lt;/View&gt;
         &lt;/View&gt;
         &lt;View style={styles.triangle}&gt;&lt;/View&gt;
       &lt;/View&gt;
     &lt;/Callout&gt;
   );
 };

 <span class="hljs-keyword">const</span> styles = StyleSheet.create({
   container: {
     backgroundColor: <span class="hljs-string">"white"</span>,
     width: screenWidth * <span class="hljs-number">0.8</span>,
     flexDirection: <span class="hljs-string">"row"</span>,
     borderWidth: <span class="hljs-number">2</span>,
     borderRadius: <span class="hljs-number">12</span>,
     overflow: <span class="hljs-string">"hidden"</span>,
   },
   triangle: {
     left: (screenWidth * <span class="hljs-number">0.8</span>) / <span class="hljs-number">2</span> - <span class="hljs-number">10</span>,
     width: <span class="hljs-number">0</span>,
     height: <span class="hljs-number">0</span>,
     backgroundColor: <span class="hljs-string">"transparent"</span>,
     borderStyle: <span class="hljs-string">"solid"</span>,
     borderTopWidth: <span class="hljs-number">20</span>,
     borderRightWidth: <span class="hljs-number">10</span>,
     borderBottomWidth: <span class="hljs-number">0</span>,
     borderLeftWidth: <span class="hljs-number">10</span>,
     borderTopColor: <span class="hljs-string">"black"</span>,
     borderRightColor: <span class="hljs-string">"transparent"</span>,
     borderBottomColor: <span class="hljs-string">"transparent"</span>,
     borderLeftColor: <span class="hljs-string">"transparent"</span>,
   },
 });

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> CustomCallout;
</code></pre>
<p> A few things to mention:</p>
<ul>
<li>We receive the marker with our custom type. This includes all the data we need to render our new card.</li>
<li>We are wrapping our UI with the <code>&lt;Callout&gt;</code> component from <code>react-native-maps</code>. We also pass the <code>tooltip</code> prop so we can take absolute control over the UI.</li>
<li>I also wanted to add a “tooltip” triangle below, so I included the styles for the triangle. Feel free to remove it if you want.</li>
</ul>
</li>
<li><p>Finally, we need to update the <code>App.tsx</code> file to render our custom callout</p>
<pre><code class="lang-diff">// App.tsx

...
<span class="hljs-addition">+ import CustomCallout from "./components/CustomCallout";</span>

export default function App() {
 const markers: MarkerWithMetadata[] = [
   ...
 ];

 const renderMarkers = () =&gt; {
   return markers.map((marker, index) =&gt; {
     return (
       &lt;Marker 
         key={index} 
         coordinate={marker.coordinate}
<span class="hljs-deletion">-         title={marker.title}</span>
<span class="hljs-deletion">-         description={marker.description}        </span>
       &gt;
<span class="hljs-addition">+         &lt;CustomCallout marker={marker}&gt;&lt;/CustomCallout&gt;</span>
       &lt;/Marker&gt;
     );
   });
 };

 return (
   ...
 );
}
</code></pre>
</li>
</ol>
<p>That’s it! You should now look something like this:</p>
<p><img src="https://i.imgur.com/tymRMFm.png" alt="Markers in React Native Maps with custom callout" /></p>
<h1 id="heading-using-a-custom-marker">Using a custom marker</h1>
<p>Another cool thing about <code>react-native-maps</code> is that you can customize the marker icon to suit your needs.  In the library docs, you can find two props available within the <code>&lt;Marker&gt;</code> component: <code>image</code> and <code>icon</code>. </p>
<p>The differences between these two properties are not clear to me (except for the fact that <code>icon</code> only works with the google maps provider), and in both cases you can pass a local image resource like this:</p>
<pre><code class="lang-diff">// App.tsx

...
<span class="hljs-addition">+ import MarkerIcon from "./assets/marker.png";</span>

export default function App() {
  const markers: MarkerWithMetadata[] = [
    ...
  ];

  const renderMarkers = () =&gt; {
    return markers.map((marker, index) =&gt; {
      return (
        &lt;Marker
<span class="hljs-addition">+         image={MarkerIcon}</span>
<span class="hljs-addition">+         // You can also use icon with Google Maps</span>
<span class="hljs-addition">+         icon={MarkerIcon}</span>
          key={new Date().getTime() + index}
          coordinate={marker.coordinate}
        &gt;
          &lt;CustomCallout marker={marker}&gt;&lt;/CustomCallout&gt;
        &lt;/Marker&gt;
      );
    });
  };

  return (
    ...
  );
}
</code></pre>
<p>But there’s a catch! Because these props expect an <code>ImageSource</code>, it’s a little bit limited. As example, I couldn’t find an easy way to change the size of the image. </p>
<p>If you want to go crazy and implement a complex marker, you can add a children component inside the <code>&lt;Marker&gt;</code>, and the rendered content will replace the marker symbol. In this example, I’m only using an <code>Image</code> component and customizing the background color and borders, but feel free to experiment:</p>
<pre><code class="lang-diff"><span class="hljs-addition">+ import MarkerIcon from "./assets/marker.png";</span>

export default function App() {
  const markers: MarkerWithMetadata[] = [
    ...
  ];

  const renderMarkers = () =&gt; {
    return markers.map((marker, index) =&gt; {
      return (
        &lt;Marker
          key={new Date().getTime() + index}
          coordinate={marker.coordinate}
        &gt;
<span class="hljs-addition">+         &lt;Image source={MarkerIcon} style={styles.marker}&gt;&lt;/Image&gt;</span>
          &lt;CustomCallout marker={marker}&gt;&lt;/CustomCallout&gt;
        &lt;/Marker&gt;
      );
    });
  };

  return (
    ...
  );
}

const styles = StyleSheet.create({
  map: {
    flex: 1,
  },
<span class="hljs-addition">+ marker: {</span>
<span class="hljs-addition">+   width: 60,</span>
<span class="hljs-addition">+   height: 60,</span>
<span class="hljs-addition">+   resizeMode: "contain",</span>
<span class="hljs-addition">+   backgroundColor: "yellow",</span>
<span class="hljs-addition">+   borderRadius: 30,</span>
<span class="hljs-addition">+   borderWidth: 2,</span>
<span class="hljs-addition">+ }</span>
});
</code></pre>
<p>After these changes, your custom markers should look like this:</p>
<p><img src="https://i.imgur.com/52PiomX.jpg" alt="Markers in React Native Maps using a custom Marker" /></p>
<p>This is just a basic example, but the possibilities are endless. As example, if you are working on a food delivery app, you could design a custom icon to represent how expensive is each restaurant, like: "💲", "💲💲", "💲💲💲", etc. You can then use conditional rendering to display the right icon based on your marker metadata.</p>
<h1 id="heading-adding-animations-to-markers">Adding animations to markers</h1>
<p>Disclaimer: I’m not good at animations, but I thought it could be fun to add a little touch to the map. What if each marker would provide subtle feedback to the user when it’s tapped?</p>
<p>Let’s make some changes to our <code>App.tsx</code> file, then I’ll explain a few points.</p>
<pre><code class="lang-diff"><span class="hljs-deletion">- import { StyleSheet, Image } from "react-native";</span>
<span class="hljs-addition">+ import { StyleSheet, Image, Animated } from "react-native";</span>
...

export type MarkerWithMetadata = {
<span class="hljs-addition">+ id: number;</span>
  coordinate: MapMarkerProps["coordinate"];
  title?: MapMarkerProps["title"];
  description?: MapMarkerProps["description"];
  imageUrl?: string;
};

export default function App() {
  const markers: MarkerWithMetadata[] = [
    {
<span class="hljs-addition">+     id: 1,</span>
      ...
    },
    {
<span class="hljs-addition">+     id: 2,</span>
      ...
    },
    // Add more markers as needed
  ];

<span class="hljs-addition">+ const markerScales = React.useRef&lt;{</span>
<span class="hljs-addition">+   [key: number]: Animated.Value;</span>
<span class="hljs-addition">+ }&gt;({});</span>

<span class="hljs-addition">+ for (const marker of markers) {</span>
<span class="hljs-addition">+   markerScales.current[marker.id] = new Animated.Value(1);</span>
<span class="hljs-addition">+ }</span>

<span class="hljs-addition">+ const handleMarkerPress = (marker: MarkerWithMetadata) =&gt; {</span>
<span class="hljs-addition">+   const scale = markerScales.current[marker.id];</span>
<span class="hljs-addition">+   Animated.timing(scale, {</span>
<span class="hljs-addition">+     toValue: 1.25,</span>
<span class="hljs-addition">+     duration: 100,</span>
<span class="hljs-addition">+     useNativeDriver: false,</span>
<span class="hljs-addition">+   }).start(() =&gt; {</span>
<span class="hljs-addition">+     Animated.timing(scale, {</span>
<span class="hljs-addition">+       toValue: 1,</span>
<span class="hljs-addition">+       duration: 100,</span>
<span class="hljs-addition">+       useNativeDriver: false,</span>
<span class="hljs-addition">+     }).start();</span>
<span class="hljs-addition">+   });</span>
<span class="hljs-addition">+ };</span>

  const renderMarkers = () =&gt; {
    return markers.map((marker) =&gt; {
      return (
        &lt;Marker
<span class="hljs-addition">+         key={marker.id}</span>
          coordinate={marker.coordinate}
          onPress={() =&gt; handleMarkerPress(marker)}
        &gt;
<span class="hljs-addition">+         &lt;Animated.View</span>
<span class="hljs-addition">+           style={{</span>
<span class="hljs-addition">+             padding: 10,</span>
<span class="hljs-addition">+             transform: [{ scale: markerScales.current[marker.id] }],</span>
<span class="hljs-addition">+           }}</span>
          &gt;
            &lt;Image source={MarkerIcon} style={[styles.marker]}&gt;&lt;/Image&gt;
<span class="hljs-addition">+         &lt;/Animated.View&gt;</span>
          &lt;CustomCallout marker={marker}&gt;&lt;/CustomCallout&gt;
        &lt;/Marker&gt;
      );
    });
  };

  return (
    ...
}
</code></pre>
<p>What’s happening here?</p>
<ul>
<li>We are adding the <code>id</code> prop on each marker:<ul>
<li>The <code>id</code> prop is added to each marker to uniquely identify them. This id will be used later to update the scale of the tapped marker.</li>
</ul>
</li>
<li>We are also adding an object to track the scale of each marker:<ul>
<li>The <code>markerScales</code> object is created using <code>useRef</code> to keep track of the scale value for each marker. It is initialized as an empty object.</li>
</ul>
</li>
<li>We then handle the marker’s <code>onPress</code> event:<ul>
<li>The <code>handleMarkerPress</code> function is called when a marker is pressed. It takes the <code>marker</code> object as a parameter, which contains the metadata for the pressed marker. Inside the function, an animation is triggered using <code>Animated.timing</code> to increase the scale of the marker to 1.25 over a duration of 100 milliseconds. Once this animation completes, a second animation is started to bring the marker's scale back to 1. This creates a visual effect of the marker briefly expanding and returning to its original size.</li>
</ul>
</li>
<li>Finally, we are wrapping our markers with <code>Animated.View</code>:<ul>
<li>Inside the <code>renderMarkers</code> function, an <code>Animated.View</code> is used to wrap the marker. The <code>transform</code> style property is applied to the <code>Animated.View</code> to scale the marker based on the corresponding <code>markerScales.current[marker.id].scale</code> value.</li>
</ul>
</li>
</ul>
<p>If everything went as expected, you should look something like this:</p>
<p><img src="https://i.imgur.com/VopFUH8.gif" alt="https://i.imgur.com/VopFUH8.gif" /></p>
<h1 id="heading-handling-marker-interactions">Handling marker interactions</h1>
<p>As we saw in the previous example, the <code>&lt;Marker&gt;</code> component comes with an <code>onPress</code> event that allows you to run a custom logic every time a marker is pressed.</p>
<p>But <code>react-native-maps</code> also includes a few more things I would like to mention:</p>
<ul>
<li><p>If you set <code>draggable</code> in your marker, you can also use <code>onDragStart</code> and <code>onDragEnd</code> to update your marker’s position.</p>
<pre><code class="lang-diff">// App.tsx

export default function App() {
  ...

  const renderMarkers = () =&gt; {
    return markers.map((marker) =&gt; {
      return (
        &lt;Marker
          key={marker.id}
<span class="hljs-addition">+         draggable</span>
<span class="hljs-addition">+         onDragEnd={(event) =&gt; {</span>
<span class="hljs-addition">+           // use the new coordinates to update marker location</span>
<span class="hljs-addition">+           const newCoordinate = event.nativeEvent.coordinate;</span>
<span class="hljs-addition">+           marker.coordinate = newCoordinate;</span>
          }}
          coordinate={marker.coordinate}
          onPress={() =&gt; handleMarkerPress(marker)}
        &gt;
          ...
        &lt;/Marker&gt;
      );
    });
  };

  return (
    ...
  );
}
</code></pre>
<p>  <img src="https://i.imgur.com/3Da7jiX.gif" alt="https://i.imgur.com/3Da7jiX.gif" /></p>
</li>
<li><p>You can also handle the <code>onPress</code> event inside the <code>Callout</code> component like this</p>
<pre><code class="lang-diff">// CustomCallout.tsx

...

const CustomCallout: React.FC&lt;{
  marker: MarkerWithMetadata;
}&gt; = ({ marker }) =&gt; {
  return (
    &lt;Callout
      tooltip
<span class="hljs-addition">+     onPress={() =&gt; {</span>
<span class="hljs-addition">+       Alert.alert(`${marker.title} pressed`);</span>
<span class="hljs-addition">+     }}</span>
    &gt;
      ...
    &lt;/Callout&gt;
  );
};

...

export default CustomCallout;
</code></pre>
</li>
</ul>
<p>That’s it for this article. Please let me know if you find it useful. I’m planning to keep writing about Maps in React Native, so feel free to reach out if there is any specific topic you would want me to cover in the future.</p>
<p>To learn more about the <code>&lt;Marker&gt;</code> component, you can also visit the <a target="_blank" href="https://github.com/react-native-maps/react-native-maps/blob/master/docs/marker.md">docs on GitHub</a></p>
<p>Happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Maps in React Native: A Step-by-Step Guide]]></title><description><![CDATA[Introduction
Maps are everywhere and can be used for a wide range of purposes, allowing you to represent different types of data, and enabling powerful interactions for your users.
Whether you need to display store locations for a delivery app, show ...]]></description><link>https://blog.spirokit.com/maps-in-react-native-a-step-by-step-guide</link><guid isPermaLink="true">https://blog.spirokit.com/maps-in-react-native-a-step-by-step-guide</guid><category><![CDATA[React Native]]></category><category><![CDATA[google maps]]></category><category><![CDATA[Expo]]></category><category><![CDATA[maps]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Wed, 14 Jun 2023 22:43:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1686782652618/c570a2ce-b603-4807-a887-340bba4da3ba.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Maps are everywhere and can be used for a wide range of purposes, allowing you to represent different types of data, and enabling powerful interactions for your users.</p>
<p>Whether you need to display store locations for a delivery app, show routes in a fitness app, or highlight nearby hospitals in a healthcare app, chances are you will need to deal with maps sooner or later.</p>
<p>In this series, we'll cover the basics of working with maps in React Native:</p>
<ul>
<li><p>Expo Project setup</p>
</li>
<li><p>Google Cloud Console setup</p>
<ul>
<li><p>Creating a Google project</p>
</li>
<li><p>Getting API keys for each platform</p>
</li>
<li><p>Setting up restrictions for each platform</p>
<ul>
<li>Getting the required SHA-1 Certificate Fingerprint by building our app with EAS build</li>
</ul>
</li>
</ul>
</li>
<li><p>Creating a basic map</p>
</li>
<li><p>Requesting user permission to access their current location</p>
</li>
<li><p>Displaying data within the map</p>
</li>
<li><p>Handling user interactions.</p>
</li>
<li><p>More!</p>
</li>
</ul>
<p>These series are divided into two parts:</p>
<ul>
<li><p><strong>The boring (but needed) part:</strong> Project setup, getting the API keys, building the app, and getting the SHA-1 Certificate (required for your API keys)</p>
</li>
<li><p><strong>The fun part:</strong> Start playing with maps and adding cool features.</p>
</li>
</ul>
<p>By the end of this article, you’ll be done with the boring stuff, and you’ll have a basic map working in your app. The next article will cover the fun part, and will be released next week.</p>
<hr />
<h1 id="heading-creating-a-new-project">Creating a new project</h1>
<p>Let’s start by creating an empty react native project with Expo. I’ll be using the <code>expo-template-blank-typescript</code> to avoid dealing with TypeScript configurations.</p>
<pre><code class="lang-bash">npx create-expo-app -t expo-template-blank-typescript
</code></pre>
<hr />
<h1 id="heading-adding-react-native-maps">Adding react-native-maps</h1>
<p>If you're working with maps in React Native, look no further than <code>react-native-maps</code>. It offers a lot of handy features, including markers, user location tracking, and event handling, as well as advanced functionalities like overlays and marker clustering.</p>
<p>Furthermore, you have the freedom to completely customize the appearance of your map to align with your brand or to support dark mode.</p>
<p>Run the following command in the root of your project to add <code>react-native-maps</code></p>
<pre><code class="lang-bash">npx expo install react-native-maps
</code></pre>
<p>By using the <code>npx expo install</code> command, we make sure that we get the right version of <code>react-native-maps</code> for the Expo SDK version we are using. At the time I’m writing this article, the latest SDK version is 48.</p>
<hr />
<h1 id="heading-setting-up-google-maps-sdk">Setting up Google Maps SDK</h1>
<p>While working with <code>react-native-maps</code>. You’ll need to use the <strong>GOOGLE_PROVIDER</strong> to present a unified look and feel on both platforms. Because the Google provider relies on Google APIs for many features, we’ll need to get our API Keys and tie everything up within our Expo project so everything works as expected.</p>
<p>But there’s a catch. In order to properly setup your Android API key restrictions (highly recommended to protect your API key from unauthorized use), you’ll need to provide a <strong>SHA-1 Certificate Fingerprint</strong>. There are two ways you can get this certificate:</p>
<ol>
<li><p>Build your app and upload your binary to the Google Play console.</p>
</li>
<li><p>Create a development build with <a target="_blank" href="https://docs.expo.dev/build/introduction/">EAS build</a>, the Expo service that allows you to build your apps in the cloud.</p>
</li>
</ol>
<p>You’ll eventually need to upload your binary before publishing your app to the Play Store, but it involves many additional steps that are outside of the scope of this tutorial, so we’ll proceed with option 2.</p>
<h2 id="heading-creating-a-development-build-with-eas-build">Creating a development build with EAS build</h2>
<p>For this part of the tutorial, you’ll need to have an Expo account and install the <code>eas-cli</code> globally. If you don’t have an account already, please visit <a target="_blank" href="https://expo.dev">https://expo.dev</a> and sign up.</p>
<p>If you are already using EAS and want to add maps to an existing project, you can skip the following steps</p>
<ol>
<li><p>Once you have an Expo account, run the following command on your terminal to globally install the <code>eas-cli</code> package</p>
<pre><code class="lang-bash"> npm install -g eas-cli
</code></pre>
</li>
<li><p>Run the <code>eas login</code> command, and enter your username and password.</p>
</li>
<li><p>Once you are logged in, run the following command to set up your project.</p>
<pre><code class="lang-bash"> eas build:configure
</code></pre>
<blockquote>
<p>Select all platforms when asked, so you can build for both platforms later.</p>
</blockquote>
</li>
<li><p>After this step, your project will now include a new <code>eas.json</code> file where you can setup your different build profiles. For the scope of this article, we’ll use the “<strong>preview</strong>” profile that is already configured. Besides, Expo will create a new project for you. You can now visit <a target="_blank" href="http://expo.dev">expo.dev</a> and search for your new project.</p>
</li>
<li><p>Update your <code>app.json</code> file to provide a package name for your Android app, and a bundle identifier for your iOS app.</p>
<pre><code class="lang-diff">{
 "expo": {
   "name": "maps-demo",
   "slug": "maps-demo",
   "version": "1.0.0",
   "orientation": "portrait",
   "icon": "./assets/icon.png",
   "userInterfaceStyle": "light",
   "splash": {
     "image": "./assets/splash.png",
     "resizeMode": "contain",
     "backgroundColor": "#ffffff"
   },
   "assetBundlePatterns": ["**/*"],
   "ios": {
     "supportsTablet": true,
<span class="hljs-addition">+    "bundleIdentifier": "com.example.mapsdemo"</span>
   },
   "android": {
     "adaptiveIcon": {
       "foregroundImage": "./assets/adaptive-icon.png",
       "backgroundColor": "#ffffff"
     },
<span class="hljs-addition">+    "package": "com.example.mapsdemo"</span>
   },
   "web": {
     "favicon": "./assets/favicon.png"
   },
   "extra": {
     "eas": {
       "projected": "..."
     }
   }
 }
}
</code></pre>
</li>
<li><p>Run the following command to create your first Android build</p>
<pre><code class="lang-bash"> eas build --platform android --profile preview
</code></pre>
</li>
</ol>
<p>Follow the instructions, and EAS will proceed to create a new Android Keystore for you. This Keystore is used to sign the Android app during the build process, ensuring its security and integrity.</p>
<p>After signing the app, Expo will automatically start building your app in the cloud.</p>
<p>At this point, you can visit <a target="_blank" href="https://expo.dev">https://expo.dev</a> and see your new project with the first build in progress.</p>
<p><img src="https://i.imgur.com/zka9SKo.png" alt="expo.dev home page" /></p>
<p>This build can take a few minutes, but we can already get the SHA-1 Certificate we need. 😉</p>
<h2 id="heading-getting-the-sha-1-certificate-fingerprint">Getting the SHA-1 Certificate Fingerprint</h2>
<ol>
<li><p>Go to <a target="_blank" href="https://expo.dev">https://expo.dev</a> and click on your new project in the sidebar, as shown in the image above.</p>
</li>
<li><p>From your project dashboard, click on “<strong>Credentials</strong>” within the “<strong>Configure</strong>” section</p>
<p> <img src="https://i.imgur.com/VI2OGxu.png" alt /></p>
</li>
<li><p>With “<strong>Android</strong>” active in the top menu (active by default), click on your application identifier</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686780790687/3fa7419e-b114-4179-a985-8441a74e49b7.png" alt /></p>
</li>
<li><p>Within the “<strong>Android Keystore</strong>” section, you’ll see your generated <strong>SHA-1 Certificate Fingerprint</strong>. Copy this, as you’ll need it below while setting up everything in Google Cloud Console.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686780825653/ca58bb32-8cd2-49b7-939a-5dde6b6e8b44.png" alt /></p>
</li>
</ol>
<h2 id="heading-creating-a-project-in-google-cloud-platform"><strong>Creating a project in Google Cloud Platform</strong></h2>
<p>Before we can use Google as a Maps provider, it is essential to create a project in Google Cloud Platform. To set up your project, follow these steps:</p>
<ol>
<li><p>Visit <a target="_blank" href="http://cloud.google.com"><strong>cloud.google.com</strong></a> and sign in.</p>
</li>
<li><p>Click on the dropdown at the top left corner.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686780854416/1479f3bb-6280-45dd-912b-52baf0d4cbed.png" alt /></p>
</li>
<li><p>Click on "<strong>New Project</strong>" at the top right corner.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686780878032/bbc5202e-1d02-4c5a-bb27-9b6f435a2671.png" alt /></p>
</li>
<li><p>Fill in the form and click on "<strong>Create</strong>", and wait until the project is created. It could take a few seconds or even minutes.</p>
<blockquote>
<p><strong><em>Warning: Once you confirm this form, you won’t be able to change your project id.</em></strong></p>
</blockquote>
</li>
<li><p>Once the project creation is finished, you receive a notification like in the image below. Click “<strong>Select project</strong>” to set your new project as active, and you’ll be redirected to your new project’s dashboard.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686780896305/ea1527b9-b23b-40e5-b638-5cf4acf7b097.png" alt /></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686780904144/5e0000eb-1374-4f76-9ad6-1e0e7b7293eb.png" alt /></p>
</li>
</ol>
<h2 id="heading-enabling-google-maps-sdk">Enabling Google Maps SDK</h2>
<p>With the new project created, we now need to enable Google Maps SDK. Let’s do that step-by-step:</p>
<ol>
<li><p>Click on “<strong>APIs &amp; Services</strong>”</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686780927785/e6fc3d44-243d-4951-a40a-56090b3a4e3a.png" alt /></p>
</li>
<li><p>Click on “<strong>Enable APIs and services</strong>”</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686780938690/a1cd52ea-cac9-4e13-a1ce-6c73b9a27342.png" alt /></p>
</li>
<li><p>We’ll need to enable “<strong>Maps SDK for Android</strong>” and “<strong>Maps SDK for iOS</strong>”.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686780951594/bcedc576-efd6-4e88-9b80-ee967e947bd8.png" alt /></p>
</li>
<li><p>Let’s start with Android first. Click on the “<strong>Maps SDK for Android</strong>” card.</p>
</li>
<li><p>Next, click on “<strong>Enable</strong>”. This will take a few seconds, and you’ll get your API key.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686780976174/fdb88783-0d96-46e7-9ad1-3b6113c442f2.png" alt /></p>
<blockquote>
<p>Make sure to store the key. You’ll need it later.</p>
</blockquote>
</li>
<li><p>Repeat the same steps for iOS to enable “<strong>Maps SDK for iOS</strong>”</p>
<blockquote>
<p>At least in my case, I didn’t get an API key for iOS after enabling it. So we’ll create an API key later in this article.</p>
</blockquote>
</li>
</ol>
<p>You now have both services enabled for your Google Console project. The next step is to set up a few app restrictions for each platform.</p>
<h2 id="heading-setting-up-app-restrictions">Setting up “App restrictions”</h2>
<p>Setting up app restrictions while working with the Google Maps SDK is highly recommended for several reasons:</p>
<ol>
<li><p><strong>Security</strong>: By setting up app restrictions, you can ensure that only your authorized apps are able to use the Google Maps SDK with your API key.</p>
</li>
<li><p><strong>Quota management</strong>: The Google Maps SDK has usage limits and quotas associated with it. By setting up app restrictions, you can effectively manage and monitor the usage of the SDK for each of your authorized apps. This helps prevent excessive usage that could lead to unexpected costs.</p>
</li>
<li><p><strong>API key management</strong>: App restrictions provide an additional layer of protection for your API key. If you need to revoke access for a specific app or if you suspect unauthorized usage, you can easily update the app restrictions to disable access for that app without affecting other authorized apps.</p>
</li>
</ol>
<h3 id="heading-app-restrictions-for-android">App restrictions for Android</h3>
<ol>
<li><p>Within “APIs &amp; Services”, click on the “<strong>Credentials</strong>” button in the sidebar, and then click on “<strong>Maps API Key</strong>”</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686781154853/9605f2f3-ceda-44d6-8915-0f6fff78432b.png" alt /></p>
</li>
<li><p>Update the name of the API key so it’s easier to differentiate between Android and iOS. I added the “Android” prefix, but feel free to choose the name you want.</p>
</li>
<li><p>Within the “<strong>Set an application restriction</strong>” section, choose “<strong>Android apps</strong>”</p>
</li>
<li><p>A new section called “<strong>Android restrictions</strong>” will appear. Click on the “<strong>+ Add</strong>” button, and complete the form with your <strong>package name</strong> (needs to match with your <code>app.json</code> file) and your <strong>SHA-1 certificate fingerprint</strong>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686781171775/8a6dddf8-884a-421d-b8cd-df123182a81f.png" alt /></p>
</li>
<li><p>Click “<strong>Done</strong>” and then “<strong>Save</strong>” to confirm your new restrictions.</p>
</li>
<li><p>Copy your API key and add it to your <code>app.json</code> file:</p>
<pre><code class="lang-diff">{
 "expo": {
   ...
   "android": {
     ...
     "package": "com.example.mapsdemo",
<span class="hljs-addition">+     "config": {</span>
<span class="hljs-addition">+       "googleMaps": {</span>
<span class="hljs-addition">+         "apiKey": "YOUR_ANDROID_API_KEY"</span>
<span class="hljs-addition">+       }</span>
<span class="hljs-addition">+     }</span>
   }
 }
}
</code></pre>
</li>
</ol>
<h3 id="heading-app-restrictions-for-ios">App restrictions for iOS</h3>
<ol>
<li><p>Go back to the "<strong>Credentials</strong>" section and click on the "<strong>Create credentials</strong>" menu. then click on "<strong>API key"</strong></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686781251496/a4f6a806-c066-460f-b776-849c0f325bbf.png" alt /></p>
</li>
<li><p>Google will generate a new API Key. Copy it for later. Then, close the modal and click on the new API to set up the restrictions, the same as you did for Android before.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686781298220/02cd69bb-aa4b-4415-a9fb-1bed435cde84.png" alt /></p>
</li>
<li><p>Update the name of the API key so it’s easier to differentiate between Android and iOS. I added the “iOS” prefix.</p>
</li>
<li><p>Within the “<strong>Set an application restriction</strong>” section, choose “<strong>iOS apps</strong>”</p>
</li>
<li><p>A new section called “<strong>iOS restrictions</strong>” will appear. Click on the “<strong>+ Add</strong>” button, and complete the form with your <strong>bundle identifier</strong> (it needs to match with your <code>app.json</code> file)</p>
</li>
<li><p>Click “<strong>Done</strong>” and then “<strong>Save</strong>” to confirm your new restrictions.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686781318026/7bdeaa83-5f02-4ec2-a4c0-43f71c1fa41e.png" alt /></p>
</li>
<li><p>Copy your API key and update your <code>app.json</code> file like this</p>
<pre><code class="lang-diff">{
 "expo": {
   ...
   "ios": {
     "bundleIdentifier": "com.example.mapsdemo",
<span class="hljs-addition">+     "config": {</span>
<span class="hljs-addition">+       "googleMapsApiKey": "YOUR_IOS_API_KEY"</span>
<span class="hljs-addition">+     }</span>
   },
   ...
 }
}
</code></pre>
</li>
</ol>
<hr />
<h1 id="heading-adding-a-basic-map">Adding a basic map</h1>
<p>Update your <code>App.tsx</code> file to render a <strong>MapView</strong> that covers the entire screen</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { StyleSheet } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> MapView, { PROVIDER_GOOGLE } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-maps"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;MapView
      provider={PROVIDER_GOOGLE} <span class="hljs-comment">// Specify Google Maps as the provider</span>
      style={styles.map}
      initialRegion={{
        latitude: <span class="hljs-number">-34.603738</span>,
        longitude: <span class="hljs-number">-58.38157</span>,
        latitudeDelta: <span class="hljs-number">0.01</span>,
        longitudeDelta: <span class="hljs-number">0.01</span>,
      }}
    /&gt;
  );
}

<span class="hljs-keyword">const</span> styles = StyleSheet.create({
  map: {
    flex: <span class="hljs-number">1</span>
  },
});
</code></pre>
<p>Here are a few things to mention about this code:</p>
<ul>
<li><p>The <code>provider</code> prop is set to <strong>PROVIDER_GOOGLE</strong>, indicating the use of Google Maps as the map provider for both platforms.</p>
</li>
<li><p>The <code>initialRegion</code> prop defines the initial center and zoom level of the map using the specified latitude, longitude, and delta values.</p>
</li>
<li><p>For the map styles, we are using <code>flex:1</code>, so we can fill the screen with the map.</p>
</li>
</ul>
<p>You can now run your app with <code>npm start</code> or <code>yarn start</code>. If everything went as expected, you should see your full-screen map ✨</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686781357514/913b8022-2dc2-4659-8492-b30842e9073e.png" alt /></p>
<p>Congrats! You are done with the boring stuff! Stay tuned for the next article, where we’ll start working with our map to add new features, track user events, and more.</p>
<h1 id="heading-final-thoughts">Final thoughts</h1>
<p>I saw a few articles that talked about how to use maps in React Native, but I couldn’t find an article focused on the boring but important configurations required to use maps in a secure way, so my goal for this article was to address all those aspects that are usually overlooked.</p>
<p>Please let me know if you find it useful, and if you want me to cover specific topics about react-native-maps or React Native in general.</p>
<p>Happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Localization in React Native]]></title><description><![CDATA[Introduction
In today's connected world, it's crucial for mobile apps to provide proper support for users who speak different languages.
In this article, we are going to build a fully-functioning React Native app that supports multiple languages, all...]]></description><link>https://blog.spirokit.com/localization-in-react-native</link><guid isPermaLink="true">https://blog.spirokit.com/localization-in-react-native</guid><category><![CDATA[React Native]]></category><category><![CDATA[localization]]></category><category><![CDATA[Expo]]></category><category><![CDATA[i18n]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Thu, 08 Jun 2023 10:57:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1686221705779/80f5e1dd-18f8-4fff-b67f-218b241b390e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>In today's connected world, it's crucial for mobile apps to provide proper support for users who speak different languages.</p>
<p>In this article, we are going to build a fully-functioning React Native app that supports multiple languages, allowing users to even switch between different languages based on their preferences.</p>
<p>As a bonus, we’ll also build a custom hook to deal with localized dates.</p>
<h2 id="heading-final-code">Final code</h2>
<p>If you want to jump directly into the code, here's the Snack</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://snack.expo.dev/@maurogarcia0209/localization-demo">https://snack.expo.dev/@maurogarcia0209/localization-demo</a></div>
<p> </p>
<h2 id="heading-what-is-localization">What is localization?</h2>
<p>Localization refers to the process of adapting an application to meet the specific language, cultural, and regional requirements of different target audiences. The process of implementing localization usually starts with translating the text of the UI, but it can go beyond and consider other preferences like date and time formats, currency symbols, measurement units, and more.</p>
<h2 id="heading-benefits-of-implementing-localization">Benefits of implementing localization</h2>
<p>By implementing localization, mobile apps can reach a broader audience and enhance user engagement. It allows users to interact with the app in their native language, making it more accessible, intuitive, and enjoyable for them</p>
<h1 id="heading-creating-a-new-react-native-project">Creating a new React Native project</h1>
<p>Let’s start by creating an empty react native using Expo. I’ll be using the <code>expo-template-blank-typescript</code> to avoid dealing with TypeScript configuration.</p>
<pre><code class="lang-bash">npx create-expo-app -t expo-template-blank-typescript
</code></pre>
<p>Once the project is ready, <code>cd</code> into your project and run the following command in the root to add the required dependencies</p>
<pre><code class="lang-bash">npx expo install expo-localization i18next react-i18next
</code></pre>
<p>By using the <code>npx expo install</code> command, we make sure that we get the right version of each package for the Expo SDK version we are using. At the time I’m writing this article, the latest SDK version is 48.</p>
<p>We’ll use <code>expo-localization</code> to get the user’s preferred locale, <code>i18next</code> to simplify the process of translating and displaying text in different languages, and <code>react-i18next</code> as an extension of the <code>i18next</code> library that includes a list of convenient hooks and components we can use while translating a react app.</p>
<hr />
<h1 id="heading-setting-up-localization">Setting up localization</h1>
<h2 id="heading-creating-the-required-files">Creating the required files</h2>
<p>To setup localization in our Expo app, let’s start by creating a <code>i18n.ts</code> file at the root of the project.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create an empty file</span>
touch i18n.ts
</code></pre>
<blockquote>
<p>Feel free to choose whatever name you want for the file.</p>
</blockquote>
<p>We also need to create a <code>translate</code> folder where we’ll add a separate <code>json</code> file for each supported language. In this article, I’ll add support for English and Spanish.</p>
<pre><code class="lang-typescript">mkdir translate
touch translate/es.json
touch translate/en.json
</code></pre>
<h2 id="heading-adding-localization-keys-for-each-supported-language">Adding localization keys for each supported language</h2>
<p>In the new <code>json</code> files, make sure to initialize both <code>json</code> files to avoid annoying errors when setting up localizations later in the article.</p>
<p><code>translate/en.json</code></p>
<pre><code class="lang-json">{
  greetings: <span class="hljs-string">"Hello world"</span>
}
</code></pre>
<p><code>translate/es.json</code></p>
<pre><code class="lang-json">{
  greetings: <span class="hljs-string">"Hola mundo"</span>
}
</code></pre>
<h2 id="heading-configuring-the-i18n-instance">Configuring the i18n instance</h2>
<p>Next, let’s add the following code in the new <code>i18n.ts</code> file to initialize <code>i18next</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//i18n.ts</span>

<span class="hljs-keyword">import</span> i18n <span class="hljs-keyword">from</span> <span class="hljs-string">"i18next"</span>
<span class="hljs-keyword">import</span> { initReactI18next } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-i18next"</span>
<span class="hljs-keyword">import</span> es <span class="hljs-keyword">from</span> <span class="hljs-string">"./translate/es.json"</span>
<span class="hljs-keyword">import</span> en <span class="hljs-keyword">from</span> <span class="hljs-string">"./translate/en.json"</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> Localization <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-localization"</span>;

<span class="hljs-keyword">const</span> getLangCode = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> code = Localization.getLocales().shift();
  <span class="hljs-keyword">if</span> (!code) <span class="hljs-keyword">return</span> <span class="hljs-string">"en"</span>;
  <span class="hljs-keyword">return</span> code.languageCode;
};

i18n.use(initReactI18next)
    .init({
        compatibilityJSON: <span class="hljs-string">"v3"</span>,
        lng: getLangCode(),
        defaultNS: <span class="hljs-string">"translation"</span>,
        interpolation: {
            <span class="hljs-comment">// We disable this because it's not required, given that react already scapes the text</span>
            escapeValue: <span class="hljs-literal">false</span>
        },
        <span class="hljs-comment">// Here you can add all your supported languages</span>
        resources: {
            es: es,
            en: en
        }
    })

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> i18n
</code></pre>
<p>Here are a few things to mention about this block of code:</p>
<ul>
<li><p>We use <code>initReactI18next</code> to initialize and setup <code>i18next</code> on our expo app.</p>
</li>
<li><p>We retrieve the current device locales using <code>expo-localization</code> and extract the language code from the first locale.</p>
</li>
<li><p>We are using the <code>compatibilityJSON</code> setting to make sure the default behavior is to use a JSON parser that supports advanced features such as nested objects and pluralization.</p>
</li>
<li><p>We are disabling text escaping in the interpolation to rely on React’s built-in escaping.</p>
</li>
<li><p>We are defining the resource files for each supported language.</p>
</li>
<li><p>We set “translation” as the default namespace</p>
<ul>
<li>If you want to learn more about namespaces in i18next, checkout <a target="_blank" href="https://www.i18next.com/principles/namespaces">this article</a></li>
</ul>
</li>
<li><p>Finally, we export the <code>i18n</code> instance to use it later.</p>
</li>
</ul>
<h2 id="heading-wrapping-our-app-with-the-i18nextprovider">Wrapping our app with the I18nextProvider</h2>
<p>Thanks to <code>react-i18next</code>, we can wrap our entire app using the <code>I18nextProvider</code>. Here are a few benefits to mention:</p>
<ol>
<li><p><strong>Centralized Configuration:</strong> The <code>I18nextProvider</code> allows you to centralize the configuration of i18next in a single place. By wrapping the app with the provider, you can set up and configure i18next once and have it accessible throughout your entire application.</p>
</li>
<li><p><strong>Context Propagation:</strong> The <code>I18nextProvider</code> utilizes React's Context API to provide the i18next instance and translations to all nested components. This eliminates the need to pass down the i18next instance manually through props to every component that requires access to translations.</p>
</li>
<li><p><strong>Efficient Updates:</strong> The <code>I18nextProvider</code> manages the <code>i18next</code> instance and ensures that updates to language and translations propagate efficiently to the components that depend on them. It optimizes the rendering process by selectively re-rendering only the affected components when language changes occur.</p>
</li>
<li><p><strong>Simplified Usage:</strong> Wrapping the app with the <code>I18nextProvider</code> simplifies the usage of <code>i18next</code> within components. It eliminates the need to manually import and initialize <code>i18next</code> in every component that requires translations, as the provider takes care of this automatically.</p>
</li>
<li><p><strong>Future-Proofing:</strong> Using the <code>I18nextProvider</code> ensures that your application is prepared for any future updates or changes to the i18next library. It provides a standardized and recommended way to integrate i18next with React, ensuring compatibility and ease of maintenance.</p>
</li>
</ol>
<p>Let’s update the <code>App.tsx</code> file to include the provider:</p>
<pre><code class="lang-diff">import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
<span class="hljs-addition">+ import i18n from "./i18n";</span>
<span class="hljs-addition">+ import { I18nextProvider } from "react-i18next";</span>

export default function App() {
  return (
<span class="hljs-addition">+   &lt;I18nextProvider i18n={i18n}&gt;</span>
      &lt;View style={styles.container}&gt;
        &lt;Text&gt;Open up App.tsx to start working on your app!&lt;/Text&gt;
        &lt;StatusBar style="auto" /&gt;
      &lt;/View&gt;
<span class="hljs-addition">+   &lt;/I18nextProvider&gt;</span>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});
</code></pre>
<hr />
<h1 id="heading-using-localization-keys-within-a-react-component">Using localization keys within a React component</h1>
<p>With everything in place, it’s time to use our localization keys inside a React component.</p>
<ol>
<li><p>First, we need to create a new folder and an empty file for the card component:</p>
<pre><code class="lang-bash"> mkdir components
 touch components/Card.tsx
</code></pre>
</li>
<li><p>Add the following code to the <code>Card</code> component</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// components/Card.tsx</span>

 <span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
 <span class="hljs-keyword">import</span> { View, Text, StyleSheet } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

 <span class="hljs-keyword">type</span> CardProps = {
   title: <span class="hljs-built_in">string</span>;
   content: <span class="hljs-built_in">string</span>;
 };

 <span class="hljs-keyword">const</span> Card: React.FC&lt;CardProps&gt; = <span class="hljs-function">(<span class="hljs-params">{ title, content }</span>) =&gt;</span> {
   <span class="hljs-keyword">return</span> (
     &lt;View style={styles.container}&gt;
       &lt;Text style={styles.title}&gt;{title}&lt;/Text&gt;
       &lt;Text style={styles.content}&gt;{content}&lt;/Text&gt;
     &lt;/View&gt;
   );
 };

 <span class="hljs-keyword">const</span> styles = StyleSheet.create({
   container: {
     backgroundColor: <span class="hljs-string">"#fff"</span>,
     borderRadius: <span class="hljs-number">8</span>,
     padding: <span class="hljs-number">16</span>,
     marginBottom: <span class="hljs-number">16</span>,
     shadowColor: <span class="hljs-string">"#000"</span>,
     shadowOffset: { width: <span class="hljs-number">0</span>, height: <span class="hljs-number">2</span> },
     shadowOpacity: <span class="hljs-number">0.2</span>,
     shadowRadius: <span class="hljs-number">2</span>,
     elevation: <span class="hljs-number">2</span>,
   },
   title: {
     fontSize: <span class="hljs-number">18</span>,
     fontWeight: <span class="hljs-string">"bold"</span>,
     marginBottom: <span class="hljs-number">8</span>,
   },
   content: {
     fontSize: <span class="hljs-number">16</span>,
     lineHeight: <span class="hljs-number">22</span>,
   },
 });

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Card;
</code></pre>
</li>
<li><p>Now, let’s update the <code>App.tsx</code> to add a few instances of the <code>Card</code> component:</p>
</li>
</ol>
<pre><code class="lang-diff">import { StatusBar } from 'expo-status-bar';
<span class="hljs-deletion">- import { StyleSheet, Text, View } from 'react-native';</span>
<span class="hljs-addition">+ import { StyleSheet, SafeAreaView, View } from 'react-native';</span>
import i18n from "./i18n";
<span class="hljs-deletion">- import { I18nextProvider } from "react-i18next";</span>
<span class="hljs-addition">+ import { I18nextProvider, useTranslation } from "react-i18next";</span>

export default function App() {
  return (
    &lt;I18nextProvider i18n={i18n}&gt;
<span class="hljs-deletion">-     &lt;View style={styles.container}&gt;</span>
<span class="hljs-deletion">-       &lt;Text&gt;Open up App.tsx to start working on your app!&lt;/Text&gt;</span>
<span class="hljs-deletion">-       &lt;StatusBar style="auto" /&gt;</span>
<span class="hljs-deletion">-     &lt;/View&gt;</span>
<span class="hljs-addition">+     &lt;Home /&gt;</span>
<span class="hljs-addition">+     &lt;StatusBar style="auto" /&gt;</span>
    &lt;/I18nextProvider&gt;
  );
}

<span class="hljs-addition">+ const Home = () =&gt; {</span>
<span class="hljs-addition">+   const { t } = useTranslation("cards");</span>
<span class="hljs-addition">+   return (</span>
<span class="hljs-addition">+     &lt;SafeAreaView style={{ flex: 1 }}&gt;</span>
<span class="hljs-addition">+       &lt;View style={styles.container}&gt;</span>
<span class="hljs-addition">+         &lt;Card title={t("cardTitle")} content={t("cardDescription")} /&gt;</span>
<span class="hljs-addition">+         &lt;Card</span>
<span class="hljs-addition">+           title={t("anotherCardTitle")}</span>
<span class="hljs-addition">+           content={t("anotherCardDescription")}</span>
<span class="hljs-addition">+         /&gt;</span>
<span class="hljs-addition">+       &lt;/View&gt;</span>
<span class="hljs-addition">+     &lt;/SafeAreaView&gt;</span>
<span class="hljs-addition">+   );</span>
<span class="hljs-addition">+ };</span>

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});
</code></pre>
<ol>
<li><p>Finally, let’s update our resource files to include all the required localization keys:</p>
<p> <code>translate/en.json</code></p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"cards"</span>: {
     <span class="hljs-attr">"cardTitle"</span>: <span class="hljs-string">"Card Title"</span>,
     <span class="hljs-attr">"cardDescription"</span>: <span class="hljs-string">"Card Description"</span>,
     <span class="hljs-attr">"anotherCardTitle"</span>: <span class="hljs-string">"Another Card Title"</span>,
     <span class="hljs-attr">"anotherCardDescription"</span>: <span class="hljs-string">"Another Card Description"</span>
   }
 }
</code></pre>
<p> <code>translate/es.json</code></p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"cards"</span>: {
     <span class="hljs-attr">"cardTitle"</span>: <span class="hljs-string">"Titulo"</span>,
     <span class="hljs-attr">"cardDescription"</span>: <span class="hljs-string">"Descripción"</span>,
     <span class="hljs-attr">"anotherCardTitle"</span>: <span class="hljs-string">"Otro Titulo"</span>,
     <span class="hljs-attr">"anotherCardDescription"</span>: <span class="hljs-string">"Otra Descripción"</span>
   }
 }
</code></pre>
</li>
</ol>
<p>Run <code>npm start</code> and you should see all the card content localized based on your preferences.</p>
<h1 id="heading-switching-between-languages">Switching between languages</h1>
<p>To wrap up this article, let’s add a new component that will list all the supported languages, allowing the user to switch between them.</p>
<ol>
<li><p>Create a new file for the component by executing the following command:</p>
<pre><code class="lang-bash"> touch components/LangSwitcher.tsx
</code></pre>
</li>
<li><p>Update the new empty file with the following code</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// components/LangSwitcher.tsx</span>

 <span class="hljs-keyword">import</span> { useTranslation } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-i18next"</span>;
 <span class="hljs-keyword">import</span> { TouchableOpacity, Text, View, StyleSheet } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

 <span class="hljs-keyword">const</span> LangSwitcher = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">const</span> { i18n, t } = useTranslation(<span class="hljs-string">"supportedLanguages"</span>);
   <span class="hljs-keyword">const</span> supportedLanguages = [<span class="hljs-string">"en"</span>, <span class="hljs-string">"es"</span>];

   <span class="hljs-keyword">const</span> changeLanguage = <span class="hljs-function">(<span class="hljs-params">lng: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
     i18n.changeLanguage(lng);
   };

   <span class="hljs-keyword">return</span> (
     &lt;View style={styles.container}&gt;
       &lt;Text style={styles.title}&gt;Choose your language&lt;/Text&gt;
       {supportedLanguages.map(<span class="hljs-function">(<span class="hljs-params">lng</span>) =&gt;</span> (
         &lt;TouchableOpacity
           onPress={<span class="hljs-function">() =&gt;</span> changeLanguage(lng)}
           style={styles.item}
         &gt;
           &lt;Text style={styles.itemText}&gt;{t(lng)}&lt;/Text&gt;
         &lt;/TouchableOpacity&gt;
       ))}
     &lt;/View&gt;
   );
 };

 <span class="hljs-comment">// improve styles</span>
 <span class="hljs-keyword">const</span> styles = StyleSheet.create({
   container: {
     flex: <span class="hljs-number">1</span>,
     backgroundColor: <span class="hljs-string">"#fff"</span>,
     alignItems: <span class="hljs-string">"center"</span>,
     justifyContent: <span class="hljs-string">"center"</span>,
     padding: <span class="hljs-number">16</span>,
   },
   title: {
     marginBottom: <span class="hljs-number">16</span>,
     fontSize: <span class="hljs-number">24</span>,
     fontWeight: <span class="hljs-string">"bold"</span>,
   },
   item: {
     width: <span class="hljs-string">"100%"</span>,
     padding: <span class="hljs-number">16</span>,
     borderBottomWidth: <span class="hljs-number">1</span>,
     borderBottomColor: <span class="hljs-string">"#ccc"</span>,
   },
   itemText: {
     fontSize: <span class="hljs-number">16</span>,
     textAlign: <span class="hljs-string">"center"</span>,
   },
 });

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> LangSwitcher;
</code></pre>
</li>
<li><p>Update the resource files, so all the supported languages in the switcher are localized 😉</p>
<p> <code>translate/en.json</code></p>
</li>
</ol>
<pre><code class="lang-diff">{
  "cards": {
    "cardTitle": "Card Title",
    "cardDescription": "Card Description",
    "anotherCardTitle": "Another Card Title",
    "anotherCardDescription": "Another Card Description"
  },
<span class="hljs-addition">+ "supportedLanguages": {</span>
<span class="hljs-addition">+   "en": "English",</span>
<span class="hljs-addition">+   "es": "Spanish"</span>
<span class="hljs-addition">+ }</span>
}
</code></pre>
<p><code>translate/es.json</code></p>
<pre><code class="lang-diff">{
  "cards": {
    "cardTitle": "Titulo",
    "cardDescription": "Descripción",
    "anotherCardTitle": "Otro Titulo",
    "anotherCardDescription": "Otra Descripción"
  },
<span class="hljs-addition">+ "supportedLanguages": {</span>
<span class="hljs-addition">+   "en": "Inglés",</span>
<span class="hljs-addition">+   "es": "Español"</span>
<span class="hljs-addition">+ }</span>
}
</code></pre>
<ol>
<li>Finally, we need to update the <code>App.tsx</code> file to add the new component</li>
</ol>
<pre><code class="lang-diff">...
<span class="hljs-addition">+ import LangSwitcher from "./components/LangSwitcher";</span>

...

const Home = () =&gt; {
  const { t } = useTranslation("cards");
  return (
    &lt;SafeAreaView style={{ flex: 1 }}&gt;
      &lt;View style={styles.container}&gt;
        ...
<span class="hljs-addition">+       &lt;LangSwitcher&gt;&lt;/LangSwitcher&gt;</span>
      &lt;/View&gt;
    &lt;/SafeAreaView&gt;
  );
};
</code></pre>
<h1 id="heading-translating-app-metadata-ios-optional">Translating app metadata (iOS - Optional)</h1>
<p>To further improve your multi-language support, you can also provide localized string for your app metadata. This includes things like:</p>
<ul>
<li><p>Your app display name</p>
</li>
<li><p>System dialogs (every time you request permissions like location, camera, etc).</p>
</li>
</ul>
<ol>
<li>Update your <code>app.json</code> file to set <code>CFBundleAllowMixedLocalizations</code> to <code>true</code> like this:</li>
</ol>
<pre><code class="lang-diff">{
  "expo": {
<span class="hljs-addition">+   "ios": {</span>
<span class="hljs-addition">+     "infoPlist": {</span>
<span class="hljs-addition">+       "CFBundleAllowMixedLocalizations": true</span>
<span class="hljs-addition">+     }</span>
<span class="hljs-addition">+   },</span>
  }
}
</code></pre>
<ol>
<li>You also need to provide a <code>locales</code> object, where you’ll define the path to a <code>json</code> file for each supported language.</li>
</ol>
<pre><code class="lang-diff">{
  "expo": {
    "ios": {
      "infoPlist": {
        "CFBundleAllowMixedLocalizations": true
      }
    },
<span class="hljs-addition">+   "locales": {</span>
<span class="hljs-addition">+     "en": "./metadata/en.json",</span>
<span class="hljs-addition">+     "es": "./metadata/es.json",</span>
<span class="hljs-addition">+   }</span>
  }
}
</code></pre>
<ol>
<li><p>Create the new folder and required files with the following command</p>
<pre><code class="lang-diff"> mkdir metadata
 touch metadata/en.json
 touch metadata/es.json
</code></pre>
</li>
<li><p>Update each <code>json</code> file to include your localized metadata. Here’s an example: <code>metadata/en.json</code></p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"CFBundleDisplayName"</span>: <span class="hljs-string">"My cooking app"</span>,
   <span class="hljs-attr">"NSCameraUsageDescription"</span>: <span class="hljs-string">"The app need to use the camera so you can take photos of your dishes"</span>,
   <span class="hljs-attr">"NSLocationWhenInUseUsageDescription"</span>: <span class="hljs-string">"The app needs access to your location to share the address of your favorite restorants"</span>
 }
</code></pre>
<p> <code>metadata/es.json</code></p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"CFBundleDisplayName"</span>: <span class="hljs-string">"Mi aplicación de cocina"</span>,
   <span class="hljs-attr">"NSCameraUsageDescription"</span>: <span class="hljs-string">"La aplicación necesita usar la cámara para que puedas tomar fotos de tus platos"</span>,
   <span class="hljs-attr">"NSLocationWhenInUseUsageDescription"</span>: <span class="hljs-string">"La aplicación necesita acceso a tu ubicación para compartir la dirección de tus restaurantes favoritos"</span>
 }
</code></pre>
</li>
</ol>
<h1 id="heading-bonus-adding-a-localized-date">Bonus: Adding a localized date</h1>
<p>Just for fun, let’s combine <code>expo-localization</code> with the built-in method <code>toLocaleString()</code> to localize our dates.</p>
<p>Update the <code>Card</code> component to include an additional prop called <code>date</code></p>
<pre><code class="lang-diff">import React from "react";
import { View, Text, StyleSheet } from "react-native";
<span class="hljs-addition">+ import * as Localization from "expo-localization";</span>

type CardProps = {
  title: string;
  content: string;
<span class="hljs-addition">+ date: Date;</span>
};

const Card: React.FC&lt;CardProps&gt; = ({ title, content, date }) =&gt; {

<span class="hljs-addition">+ // Get the user's preferred locale</span>
<span class="hljs-addition">+ const locale = Localization.getLocales().shift();</span>
<span class="hljs-addition">+ const languageCode = locale?.languageCode || "en";</span>

<span class="hljs-addition">+ const localizedDate = date.toLocaleString(languageCode, {</span>
<span class="hljs-addition">+   dateStyle: "long",</span>
<span class="hljs-addition">+   timeStyle: "long",</span>
<span class="hljs-addition">+ });</span>

  return (
    &lt;View style={styles.container}&gt;
      &lt;Text style={styles.title}&gt;{title}&lt;/Text&gt;
      &lt;Text style={styles.content}&gt;{content}&lt;/Text&gt;
<span class="hljs-addition">+     &lt;Text style={styles.content}&gt;{localizedDate}&lt;/Text&gt;</span>
    &lt;/View&gt;
  );
};
</code></pre>
<p>Now, you should be able to see a localized date. You can even go a step further and add a convenient hook to encapsulate this logic, so you don’t need to add all this boilerplate every time you want to display a localized date.</p>
<p>Besides, the code provided above has a catch: If the user changes the language of the app in runtime, the localized string won’t update.</p>
<p>Let’s fix that problem with the <code>useLocalizedDate</code> hook</p>
<ol>
<li><p>First, we need to create a new folder and an empty file for the new hook.</p>
<pre><code class="lang-bash"> mkdir hooks
 touch hooks/useLocalizedDate.tsx
</code></pre>
</li>
<li><p>Now, add the following code for the hook</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// hooks/useLocalizedDate.tsx</span>

 <span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> Localization <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-localization"</span>;
 <span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
 <span class="hljs-keyword">import</span> { useTranslation } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-i18next"</span>;

 <span class="hljs-keyword">const</span> useLocalizedDate = <span class="hljs-function">(<span class="hljs-params">
   date: <span class="hljs-built_in">Date</span>,
   options: <span class="hljs-built_in">Intl</span>.DateTimeFormatOptions = {}
 </span>) =&gt;</span> {
   <span class="hljs-keyword">const</span> { i18n } = useTranslation();

   <span class="hljs-keyword">const</span> defaultOptions: <span class="hljs-built_in">Intl</span>.DateTimeFormatOptions = {
     dateStyle: <span class="hljs-string">"long"</span>,
     timeStyle: <span class="hljs-string">"long"</span>,
   };

   <span class="hljs-keyword">const</span> [localizedDate, setLocalizedDate] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);

   <span class="hljs-comment">// When the component mounts, set the initial value of the localized date and add a listener for language changes</span>
   useEffect(<span class="hljs-function">() =&gt;</span> {
     setInitialValue();
     i18n.on(<span class="hljs-string">"languageChanged"</span>, <span class="hljs-function">(<span class="hljs-params">lng</span>) =&gt;</span> {
       setLocalizedDate(date.toLocaleString(lng, defaultOptions));
     });

     <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
       i18n.off(<span class="hljs-string">"languageChanged"</span>);
     };
   }, []);

   <span class="hljs-keyword">const</span> setInitialValue = <span class="hljs-function">() =&gt;</span> {
     <span class="hljs-comment">// Merge the user's options with the default options</span>
     <span class="hljs-keyword">const</span> mergedOptions = { ...defaultOptions, ...options };

     <span class="hljs-comment">// Get the user's preferred locale</span>
     <span class="hljs-keyword">const</span> locale = Localization.getLocales().shift();
     <span class="hljs-keyword">const</span> languageCode = locale?.languageCode || <span class="hljs-string">"en"</span>;
     <span class="hljs-comment">// Localize the date</span>
     setLocalizedDate(date.toLocaleString(languageCode, mergedOptions));
   };

   <span class="hljs-keyword">return</span> localizedDate <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>;
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> useLocalizedDate;
</code></pre>
<p> In this improved version, we are also listening to <code>i18next</code> events to update the localized dates when the language of the user changes.</p>
</li>
<li><p>Finally, we can update the card component to use the new hook like this:</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// components/Card.tsx</span>

 <span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
 <span class="hljs-keyword">import</span> { View, Text, StyleSheet } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
 <span class="hljs-keyword">import</span> useLocalizedDate <span class="hljs-keyword">from</span> <span class="hljs-string">"../hooks/useLocalizedDate"</span>;

 <span class="hljs-keyword">type</span> CardProps = {
   title: <span class="hljs-built_in">string</span>;
   content: <span class="hljs-built_in">string</span>;
   date: <span class="hljs-built_in">Date</span>;
 };

 <span class="hljs-keyword">const</span> Card: React.FC&lt;CardProps&gt; = <span class="hljs-function">(<span class="hljs-params">{ title, content, date }</span>) =&gt;</span> {
   <span class="hljs-keyword">const</span> localizedDate = useLocalizedDate(date);

   <span class="hljs-keyword">return</span> (
     &lt;View style={styles.container}&gt;
       &lt;Text style={styles.title}&gt;{title}&lt;/Text&gt;
       &lt;Text style={styles.content}&gt;{content}&lt;/Text&gt;
       &lt;Text style={styles.content}&gt;{localizedDate}&lt;/Text&gt;
     &lt;/View&gt;
   );
 };
</code></pre>
</li>
</ol>
<h1 id="heading-final-thoughts">Final thoughts</h1>
<p>Implementing localization in mobile apps is essential for reaching a broader audience and enhancing user engagement. Thanks to libraries like <code>expo-localization</code>, <code>i18next</code> and <code>react-i18next</code>, it’s easier than ever to implement localization.</p>
<p>By providing support for multiple languages, your app will be more accessible, intuitive, and enjoyable for your users.</p>
]]></content:encoded></item><item><title><![CDATA[Google authentication with Expo & Supabase]]></title><description><![CDATA[While building a mobile app for a SaaS, chances are you’re going to deal with social login sooner or later, and sometimes, this can be a little bit overwhelming. 
However, the benefits of providing these additional mechanisms can drastically improve ...]]></description><link>https://blog.spirokit.com/google-authentication-with-expo-supabase</link><guid isPermaLink="true">https://blog.spirokit.com/google-authentication-with-expo-supabase</guid><category><![CDATA[Expo]]></category><category><![CDATA[React Native]]></category><category><![CDATA[supabase]]></category><category><![CDATA[authentication]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Fri, 02 Jun 2023 10:50:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1685738935528/c37fe366-be89-4994-a3e5-dd77bb638b9c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While building a mobile app for a SaaS, chances are you’re going to deal with social login sooner or later, and sometimes, this can be a little bit overwhelming. </p>
<p>However, the benefits of providing these additional mechanisms can drastically improve your users’ experience with the app. You can also request permission to access certain information about your users that could allow you to further tailor the experience based on personal preferences. </p>
<p>Popular social providers such as Google, Facebook, Twitter, and more, follow a common open standard known as OAuth, so the steps required to integrate each provider are similar.</p>
<h1 id="heading-supabase-to-the-rescue">Supabase to the rescue</h1>
<p>Thankfully, if you are using Supabase as your database, you can easily add social login to your app,</p>
<p>In this article, we’ll focus on adding Google as our first social provider.</p>
<p>I highly recommend reading the previous article in the series first, which provides essential instructions for setting up your Supabase and Expo projects. Once you've gone through that article, you'll be fully prepared to dive into the upcoming content.</p>
<p>Here’s a link to the <a target="_blank" href="https://blog.spirokit.com/building-a-mobile-authentication-flow-for-your-saas-with-expo-and-supabase">previous post</a></p>
<h1 id="heading-creating-a-project-in-google-cloud-platform">Creating a project in Google Cloud Platform</h1>
<p>Before we can utilize Google as a social provider in your Expo app, it is essential to create a project in Google Cloud Platform. To set up your project, follow these steps:</p>
<ol>
<li>Visit <a target="_blank" href="http://cloud.google.com">cloud.google.com</a> and sign in.</li>
<li><p>Click on the dropdown at the top left corner.</p>
<p> <img src="https://i.imgur.com/g5WjURe.png" alt="Google Cloud Console - Home" /></p>
</li>
<li><p>Click on "<strong>New Project</strong>" at the top right corner.</p>
<p> <img src="https://i.imgur.com/9eJAafN.png" alt="Google Cloud Console - Select project" /></p>
</li>
<li><p>Fill in the form and click on "<strong>Create</strong>", and wait until the project is created. It could take a few seconds or even minutes.</p>
<blockquote>
<p>Warning: Once you confirm this form, you won’t be able to change your project id.</p>
</blockquote>
</li>
<li><p>Once the project creation is finished, you receive a notification like in the image below. Click “<strong>Select project</strong>” to set your new project as active, and you’ll be redirected to your new project’s dashboard.</p>
<p> <img src="https://i.imgur.com/UB3EccJ.png" alt="Google Cloud Console - Notification after finishing creating a project" /></p>
<p> <img src="https://i.imgur.com/TasjdWr.png" alt="Google Cloud Console - New project dashboard" /></p>
</li>
</ol>
<h1 id="heading-creating-the-required-oauth-keys">Creating the required OAuth Keys</h1>
<ol>
<li><p>Once you are in your project’s dashboard, use the search box at the top to search for “OAuth consent screen”, and select the first result under “Products and Pages”</p>
<p> <img src="https://i.imgur.com/nqomdXh.png" alt="Google Cloud Console - Searching for OAuth Consent Screen in the SearchBox" /></p>
</li>
<li><p>In the User Type section, select “<strong>External</strong>” and hit “<strong>Create</strong>” to confirm</p>
<p> <img src="https://i.imgur.com/oQCpnRv.png" alt="Setting type of User for the OAuth Consent screen" /></p>
</li>
<li><p>You’ll be redirected to the “<strong>Edit app registration</strong>” form. Make sure to review all the information and assets provided here, given that this information will be presented to your users during the login flow.</p>
</li>
<li><p>While following the process, make sure to define your scopes based on your specific needs. Here you can see the <a target="_blank" href="[https://developers.google.com/identity/protocols/oauth2/scopes?authuser=1](https://developers.google.com/identity/protocols/oauth2/scopes?authuser=1)">full list of available scopes</a>. Besides, add a few testing users so you can complete the auth flow in your app before publishing the Consent Screen.</p>
<blockquote>
<p>You can always come back and update your consent screen later. Also, make sure to publish the consent screen once your app is about to be submitted to the stores</p>
</blockquote>
<p> <img src="https://i.imgur.com/Pz1hA8H.png" alt="Option to Publish the OAuth Consent screen" /></p>
</li>
</ol>
<h1 id="heading-getting-your-supabase-callback-url">Getting your Supabase callback URL</h1>
<p>In the next step, we’ll need to create an <code>OAuth client ID</code> in Google Cloud. But first, we’ll need to get our callback URL from Supabase.</p>
<ol>
<li>Login into <a target="_blank" href="https://supabase.com/dashboard/sign-in">supabase</a>, and go to your project’s dashboard.</li>
<li><p>In the sidebar, click on the “Authentication” button</p>
<p> <img src="https://i.imgur.com/e9dR2lv.png" alt="Authentication button in Supabase Sidebar" /></p>
</li>
<li><p>Under the “<strong>Configuration</strong>” section, click on “<strong>Providers</strong>” and click on “<strong>Google</strong>” to expand the accordion.</p>
</li>
<li>Copy the provided <strong>Redirect URL.</strong></li>
</ol>
<h1 id="heading-creating-the-oauth-client-id">Creating the OAuth client ID</h1>
<ol>
<li>Back into cloud.google.com, visit your project dashboard.</li>
<li><p>Click on the hamburger menu at the top left corner, and navigate to “<strong>APIs &amp; Services</strong>” → “<strong>Credentials</strong>”</p>
<p> <img src="https://i.imgur.com/l58Ld4Y.png" alt="Access to credentials management in Google Cloud Console" /></p>
</li>
<li><p>Click the “+ Create credentials” button, and then select “<strong>OAuth client ID</strong>”</p>
<p> <img src="https://i.imgur.com/KUfUFiZ.png" alt="Access to create a new OAuth client ID in Google Cloud Console" /></p>
</li>
<li><p>Under “<strong>Application Type</strong>”, choose “<strong>Web application</strong>”. Fill in the name of your app, and click on “<strong>Add URI</strong>” under the “<strong>Authorized redirect URIs</strong>” section. Paste your Supabase redirect URL here.</p>
<p> <img src="https://i.imgur.com/h1UXWny.png" alt="Authorized redirect URIs section in Google Cloud Console" /></p>
</li>
<li><p>If everything goes right, you’ll get a “<strong>Client ID</strong>” and “<strong>Client Secret</strong>”. Make sure to store this information. You’ll need it in the next step.</p>
</li>
</ol>
<h1 id="heading-updating-your-google-tokens-in-your-supabase-project">Updating your Google tokens in your Supabase project</h1>
<ol>
<li><p>Go back to your Supabase project, and click the “Authentication” button in the sidebar</p>
<p> <img src="https://i.imgur.com/e9dR2lv.png" alt="Authentication Button in Supabase Sidebar" /></p>
</li>
<li><p>Select “<strong>Providers</strong>” → “<strong>Google</strong>”</p>
</li>
<li>Toggle the “<strong>Google Enabled</strong>” switch to <strong>ON</strong></li>
<li>Enter your Google tokens (Client id and Client Secret), and click on “<strong>Save</strong>”</li>
</ol>
<h1 id="heading-adding-social-login-to-our-expo-app">Adding social login to our Expo app</h1>
<p>In the previous post, we created a <code>SupabaseContext.tsx</code> and a <code>SupabaseProvider.tsx</code> file.</p>
<ol>
<li><p>Let’s update the context by adding the <code>getGoogleOAuthUrl</code> and <code>setOAuthSession</code> methods:</p>
<pre><code class="lang-diff">// context/SupabaseContext.tsx

type SupabaseContextProps = {
  ...
<span class="hljs-addition">+ getGoogleOAuthUrl: () =&gt; Promise&lt;string | null&gt;;</span>
<span class="hljs-addition">+ setOAuthSession: (tokens: {</span>
<span class="hljs-addition">+   access_token: string;</span>
<span class="hljs-addition">+   refresh_token: string;</span>
<span class="hljs-addition">+ }) =&gt; Promise&lt;void&gt;;</span>
};

export const SupabaseContext = createContext&lt;SupabaseContextProps&gt;({
  ...
<span class="hljs-addition">+ getGoogleOAuthUrl: async () =&gt; "",</span>
<span class="hljs-addition">+ setOAuthSession: async () =&gt; {},</span>
});
</code></pre>
</li>
<li><p>Now, we need to also update the provider to implement this new method. </p>
<p> A few things to mention here:</p>
<ul>
<li>We are setting <code>mysupabaseapp://auth</code> as a <em>redirect uri</em> while calling the <code>getGoogleOAuthUrl</code> method. You need to replace <code>mysupabaseapp</code> with your custom scheme. In the next step, I’ll show you how to setup the same redirect URL in your Supabase project so everything works as expected.</li>
<li>The <code>signInWithOAuth</code> method will return an object that contains the Supabase URL that we need to start the auth flow in the browser later.</li>
<li><p>The <code>setOAuthSession</code> method will allow us to persist the user session using Supabase Auth. In that way, the user won’t need to sign in again the next time.</p>
<p>A few things to mention here:</p>
</li>
</ul>
<ul>
<li>We are setting <code>mysupabaseapp://auth</code> as a <em>redirect uri</em> while calling the <code>getGoogleOAuthUrl</code> method. You need to replace <code>mysupabaseapp</code> with your custom scheme. In the next step, I’ll show you how to setup the same redirect URL in your Supabase project so everything works as expected.</li>
<li>The <code>signInWithOAuth</code> method will return an object that contains the Supabase URL that we need to start the auth flow in the browser later.</li>
<li>The <code>setOAuthSession</code> method will allow us to persist the user session using Supabase Auth. In that way, the user won’t need to sign in again the next time.</li>
</ul>
<pre><code class="lang-diff">// context/SupabaseProvider.tsx

export const SupabaseProvider = (props: SupabaseProviderProps) =&gt; {

 const supabase = createClient(
   ...
 );
 ...

<span class="hljs-addition">+ const getGoogleOAuthUrl = async (): Promise&lt;string | null&gt; =&gt; {</span>
<span class="hljs-addition">+   const result = await supabase.auth.signInWithOAuth({</span>
<span class="hljs-addition">+     provider: "google",</span>
<span class="hljs-addition">+     options: {</span>
<span class="hljs-addition">+       redirectTo: "mysupabaseapp://google-auth",</span>
<span class="hljs-addition">+     },</span>
<span class="hljs-addition">+   });</span>
<span class="hljs-addition">+   </span>
<span class="hljs-addition">+   return result.data.url;</span>
<span class="hljs-addition">+ };</span>

<span class="hljs-addition">+ const setOAuthSession = async (tokens: {</span>
<span class="hljs-addition">+   access_token: string;</span>
<span class="hljs-addition">+   refresh_token: string;</span>
<span class="hljs-addition">+ }) =&gt; {</span>
<span class="hljs-addition">+   const { data, error } = await supabase.auth.setSession({</span>
<span class="hljs-addition">+     access_token: tokens.access_token,</span>
<span class="hljs-addition">+     refresh_token: tokens.refresh_token,</span>
<span class="hljs-addition">+   });</span>
<span class="hljs-addition">+</span>
<span class="hljs-addition">+   if (error) throw error;</span>
<span class="hljs-addition">+</span>
<span class="hljs-addition">+   setLoggedIn(data.session !== null);</span>
<span class="hljs-addition">+ };</span>

 ...

 return (
   &lt;SupabaseContext.Provider
     value={{
         ...
<span class="hljs-addition">+       getGoogleOAuthUrl,</span>
<span class="hljs-addition">+       setOAuthSession</span>
     }}
   &gt;
     ...
   &lt;/SupabaseContext.Provider&gt;
 );
};
</code></pre>
</li>
<li><p>Install the <code>expo-web-browser</code> package by running the following command</p>
<pre><code class="lang-bash"> npx expo install expo-web-browser
</code></pre>
</li>
<li><p>Finally, we need to update our <code>LoginScreen.tsx</code> </p>
<ul>
<li>We need to add a “<strong>Sign in with Google</strong>” button.</li>
<li>We are using <code>expo-web-browser</code> to load an in-app browser for the login flow.</li>
<li>As described in the Expo docs, we call <code>WebBrowser.warmUpAsync()</code> to load the browser in the background before the user taps the button to improve the user experience. You can learn more about this <a target="_blank" href="https://docs.expo.dev/guides/authentication/#warming-the-browser">in this link</a></li>
<li>Once the user taps the button, the flow should look like this:<ul>
<li>We get the Supabase OAuth URL</li>
<li><code>openAuthSessionAsync</code> is called with the Supabase URL</li>
<li>The Google Consent Screen is shown.</li>
<li>Once the user completes the flow, the browser returns to the app with a URL</li>
<li>We extract the tokens from the URL.</li>
<li>Finally, we call <code>setOAuthSession</code> to persist the Supabase session.</li>
<li>Optional: We securely store the provider token for future use</li>
</ul>
</li>
<li>For the UI, we are using <a target="_blank" href="[https://spirokit.com/](https://spirokit.com/)">SpiroKit</a>, but feel free to use your own button.</li>
</ul>
<pre><code class="lang-diff"><span class="hljs-addition">+ import * as WebBrowser from "expo-web-browser";</span>
<span class="hljs-addition">+ import * as SecureStore from "expo-secure-store";</span>

const LoginScreen = () =&gt; {

 const { 
   login, 
<span class="hljs-addition">+  getGoogleOAuthUrl,</span>
<span class="hljs-addition">+  setOAuthSession </span>
 } = useSupabase();

 ...

<span class="hljs-addition">+ React.useEffect(() =&gt; {</span>
<span class="hljs-addition">+   WebBrowser.warmUpAsync();</span>
<span class="hljs-addition">+</span>
<span class="hljs-addition">+   return () =&gt; {</span>
<span class="hljs-addition">+     WebBrowser.coolDownAsync();</span>
<span class="hljs-addition">+   };</span>
<span class="hljs-addition">+ }, []);</span>

 ...

<span class="hljs-addition">+ const onSignInWithGoogle = async () =&gt; {</span>
<span class="hljs-addition">+   setLoading(true);</span>
<span class="hljs-addition">+   try {</span>
<span class="hljs-addition">+     const url = await getGoogleOAuthUrl();</span>
<span class="hljs-addition">+     if (!url) return;</span>
<span class="hljs-addition">+ </span>
<span class="hljs-addition">+     const result = await WebBrowser.openAuthSessionAsync(</span>
<span class="hljs-addition">+       url,</span>
<span class="hljs-addition">+       "mysupabaseapp://google-auth?",</span>
<span class="hljs-addition">+       {</span>
<span class="hljs-addition">+         showInRecents: true,</span>
<span class="hljs-addition">+       }</span>
<span class="hljs-addition">+     );</span>
<span class="hljs-addition">+ </span>
<span class="hljs-addition">+     if (result.type === "success") {</span>
<span class="hljs-addition">+       const data = extractParamsFromUrl(result.url);</span>
<span class="hljs-addition">+</span>
<span class="hljs-addition">+       if (!data.access_token || !data.refresh_token) return;</span>
<span class="hljs-addition">+</span>
<span class="hljs-addition">+       setOAuthSession({</span>
<span class="hljs-addition">+         access_token: data.access_token,</span>
<span class="hljs-addition">+         refresh_token: data.refresh_token,</span>
<span class="hljs-addition">+       });</span>
<span class="hljs-addition">+</span>
<span class="hljs-addition">+       // You can optionally store Google's access token if you need it later</span>
<span class="hljs-addition">+       SecureStore.setItemAsync(</span>
<span class="hljs-addition">+         "google-access-token",</span>
<span class="hljs-addition">+         JSON.stringify(data.provider_token)</span>
<span class="hljs-addition">+       );</span>
<span class="hljs-addition">+     }</span>
<span class="hljs-addition">+   } catch (error) {</span>
<span class="hljs-addition">+     // Handle error here</span>
<span class="hljs-addition">+     console.log(error);</span>
<span class="hljs-addition">+   } finally {</span>
<span class="hljs-addition">+     setLoading(false);</span>
<span class="hljs-addition">+   }</span>
<span class="hljs-addition">+ };</span>

<span class="hljs-addition">+ const extractParamsFromUrl = (url: string) =&gt; {</span>
<span class="hljs-addition">+   const params = new URLSearchParams(url.split("#")[1]);</span>
<span class="hljs-addition">+   const data = {</span>
<span class="hljs-addition">+     access_token: params.get("access_token"),</span>
<span class="hljs-addition">+     expires_in: parseInt(params.get("expires_in") || "0"),</span>
<span class="hljs-addition">+     refresh_token: params.get("refresh_token"),</span>
<span class="hljs-addition">+     token_type: params.get("token_type"),</span>
<span class="hljs-addition">+     provider_token: params.get("provider_token"),</span>
<span class="hljs-addition">+   };</span>
<span class="hljs-addition">+</span>
<span class="hljs-addition">+   return data;</span>
<span class="hljs-addition">+ };</span>

 return (
   &lt;KeyboardAvoidingView
     ...
   &gt;
     &lt;ScrollView contentContainerStyle={{ flexGrow: 1 }}&gt;
       &lt;VStack safeAreaTop padding={4} flex={1}&gt;
         &lt;VStack space={4} marginTop={5} width="full" flex={1}&gt;
             ...
<span class="hljs-addition">+           &lt;Button</span>
<span class="hljs-addition">+             isDisabled={loading}</span>
<span class="hljs-addition">+             onPress={() =&gt; onSignInWithGoogle()}</span>
<span class="hljs-addition">+             marginBottom={5}</span>
<span class="hljs-addition">+           &gt;</span>
<span class="hljs-addition">+             {loading ? "Loading..." : "Sign in with Google"}</span>
<span class="hljs-addition">+           &lt;/Button&gt;</span>
         &lt;/VStack&gt;
       &lt;/VStack&gt;
     &lt;/ScrollView&gt;
   &lt;/KeyboardAvoidingView&gt;
 );
};

export default LoginScreen;
</code></pre>
</li>
</ol>
<h1 id="heading-redirect-url-setup-in-supabase">Redirect URL setup in Supabase</h1>
<ol>
<li>Go to your project dashboard in Supabase.</li>
<li>In the sidebar, click the “<strong>Authentication</strong>” button.</li>
<li>Under the “<strong>Configuration</strong>” section, click on “<strong>URL Configuration</strong>”.</li>
<li><p>In the “<strong>Redirect URLs</strong>” section, click the “<strong>Add URL</strong>”.</p>
<p> <img src="https://i.imgur.com/v3Dbd63.png" alt="URL Redirection config in Supabase" /></p>
</li>
<li><p>Add your custom scheme. </p>
<ol>
<li>This should match with the scheme used in the previous step.</li>
</ol>
</li>
</ol>
<h1 id="heading-final-thoughts">Final thoughts</h1>
<p>If you are still here, congrats! Now you have an app with Social login. </p>
<p>It's worth mentioning that Supabase offers support for numerous providers, so if you require additional options, they have you covered.</p>
<p>By following the steps outlined in this article, you should be able to easily add more providers to your app. While each provider may have a few specific steps to follow, Supabase's documentation serves as an excellent starting point. To delve deeper, you can find more information <a target="_blank" href="https://supabase.com/docs/guides/auth/social-login">here</a></p>
<p>Once you get the tokens to interact with each provider, you should be able to update the Supabase provider to get the login URL. The rest of the flow should be reusable as is.</p>
<p>I’ll probably cover more about this in the future, so if you have any questions or require additional assistance, please do not hesitate to reach out.</p>
]]></content:encoded></item><item><title><![CDATA[Building a mobile authentication flow for your SaaS with Expo and Supabase]]></title><description><![CDATA[Expo is an invaluable tool when it comes to developing mobile apps, offering a rich SDK with a wide range of packages. With services like EAS Build, the process of publishing your app becomes effortless. However, if your goal is to create a SaaS (Sof...]]></description><link>https://blog.spirokit.com/building-a-mobile-authentication-flow-for-your-saas-with-expo-and-supabase</link><guid isPermaLink="true">https://blog.spirokit.com/building-a-mobile-authentication-flow-for-your-saas-with-expo-and-supabase</guid><category><![CDATA[Expo]]></category><category><![CDATA[React Native]]></category><category><![CDATA[supabase]]></category><category><![CDATA[authentication]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Wed, 24 May 2023 23:59:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1684972366147/23f436b6-9f72-42a3-ae89-55bcd770dd53.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Expo is an invaluable tool when it comes to developing mobile apps, offering a rich SDK with a wide range of packages. With services like EAS Build, the process of publishing your app becomes effortless. However, if your goal is to create a SaaS (Software as a Service), it becomes essential to have a backend for functionalities such as authentication and information storage. </p>
<p>In such cases, you may consider using Supabase as your backend solution. Supabase provides a seamless way to handle authentication, data storage, and more, complementing Expo's frontend capabilities to create a robust and scalable mobile app for a SaaS.</p>
<h1 id="heading-what-do-you-get-from-this-tutorial">What do you get from this tutorial?</h1>
<p><img src="https://i.imgur.com/2PxId3S.png" alt="https://i.imgur.com/2PxId3S.png" /></p>
<ul>
<li>A new Supabase project with everything configured to handle authentication.</li>
<li>A working Expo app integrated with Supabase.<ul>
<li>A global Supabase context with easy access to interact with the Supabase client</li>
<li>React-navigation setup for all the routes. You get IntelliSense for route names.</li>
<li>Four working screens (Login, Register, Forgot Password, Home)</li>
<li>Route security setup (only authenticated users can access the Home route).</li>
</ul>
</li>
<li>Login flow using Supabase auth.<ul>
<li>Your session info is securely persisted locally using Expo Secure Store.</li>
<li>Automatic refresh token flow</li>
</ul>
</li>
<li>Register flow</li>
<li>Forgot Password flow</li>
<li>Setup to read environment variables from a <code>.env</code> file.</li>
</ul>
<h1 id="heading-expo-supabase-template-is-now-available-with-spirokit">Expo-Supabase template is now available with SpiroKit!</h1>
<p>For this demo app, I'll be using <a target="_blank" href="https://bit.ly/3Ea1TDq">SpiroKit</a>, which is a React Native UI kit I built. Given that is a paid product, feel free to follow this tutorial with your own UI.</p>
<p><a target="_blank" href="https://bit.ly/3Ea1TDq"><img src="https://i.imgur.com/MyOHaEv.png" alt /></a></p>
<p>With SpiroKit, you can use our new <code>expo-supabase-typescript-template</code>. It includes everything you’ll see in this tutorial.</p>
<h1 id="heading-creating-a-new-supabase-project">Creating a new Supabase project</h1>
<p>Supabase is an open-source project that offers a range of functionalities for hosting projects in the cloud. As of the time of writing, it allows users to host up to 2 projects in their free tier. If you have exceeded this quota, an alternative option is to host your own instance on a dedicated server. However, for the sake of simplicity, we will focus on utilizing the cloud project setup in this tutorial. Setting up a dedicated server falls outside the scope of this tutorial and will not be covered in detail.</p>
<ol>
<li>Visit <a target="_blank" href="https://supabase.com">https://supabase.com</a> and sign up if you don’t have an account</li>
<li><p>Once your account is ready, create your first project:</p>
<p> <img src="https://i.imgur.com/s2q4Xep.png" alt="Supabase new project button" /></p>
</li>
<li><p>Choose a name for your project and a strong password. Make sure you are using the “Free” pricing plan if you are experimenting. You can update to a paid plan later if it’s required.</p>
<p> <img src="https://i.imgur.com/cZdL8FA.png" alt="Supabase new project form" /></p>
</li>
<li><p>Hit the “Create new project” to confirm. If everything goes right, you’ll be redirected to the home page of your new Supabase project. You’ll need to copy the Project URL and the API key (anon) later to connect your project with your Expo app</p>
<p> <img src="https://i.imgur.com/dIzdq8q.png" alt="Supabase tokens" /></p>
</li>
</ol>
<h1 id="heading-setting-up-authentication-on-supabase">Setting up authentication on Supabase</h1>
<p>Every Supabase project comes with a full Postgres database. We’ll be using it to support our basic authentication flow with email and password.</p>
<ol>
<li><p>From your supabase project dashboard, use the sidebar to visit the “SQL Editor” section</p>
<p> <img src="https://i.imgur.com/27QRG13.png" alt="Supabase SQL Editor" /></p>
</li>
<li><p>Under the “Quick Start” section, you’ll find the “User Management Starter” card. This will help us setup all the required tables for the auth flow.</p>
</li>
<li><p>You can click the “Run” button to execute the query as is, or you can make a few modifications before execution based on your needs. This script will perform the following tasks:</p>
<ol>
<li><strong>Create Table</strong>: Defines a table called <code>profiles</code> with various columns to store information about public profiles, such as ID, updated timestamp, username, full name, avatar URL, and website. It also includes a constraint to ensure that the username length is at least 3 characters.</li>
<li><strong>Set up Row Level Security (RLS)</strong>: Enables Row Level Security for the <code>profiles</code> table. RLS allows controlling access to individual rows based on policies. If you want lo learn more about this powerful feature, <a target="_blank" href="https://supabase.com/docs/guides/auth/row-level-security">visit the following link</a></li>
<li><strong>Create Policies</strong>: Defines three policies to control access to the <code>profiles</code> table:<ul>
<li>"<strong>Public profiles are viewable by everyone.</strong>": Grants read access to all rows in the <code>profiles</code> table.</li>
<li>"<strong>Users can insert their own profile.</strong>": Allows users to insert a row into the <code>profiles</code> table only if their authenticated user ID matches the "id" column.</li>
<li>"<strong>Users can update their own profile.</strong>": Permits users to update their own profile by matching their authenticated user ID with the "id" column.</li>
</ul>
</li>
<li><strong>Create a function and a Trigger</strong>: Sets up a trigger function named "handle_new_user" and a trigger named "on_auth_user_created" to automatically create a profile entry when a new user signs up via Supabase Auth (we’ll use this on our Expo app later). The trigger function extracts relevant information from the newly created user and inserts it into the <code>profiles</code> table.</li>
<li><strong>Set up Storage</strong>: Inserts a row into the <code>storage.buckets</code> table to define a bucket named "avatars" for storing avatar images.</li>
<li><p><strong>Create Storage Policies</strong>: Defines two policies to control access to the "avatars" bucket in storage:</p>
<ul>
<li>"Avatar images are publicly accessible.": Allows anyone to retrieve (select) objects from the "avatars" bucket.</li>
<li><p>"Anyone can upload an avatar.": Permits anyone to insert (upload) objects into the "avatars" bucket.</p>
<p>For simplicity, I’ll execute the script as is.</p>
</li>
</ul>
</li>
</ol>
</li>
<li><p>We can now visit the “Table Editor” section using the sidebar and check that the <code>profiles</code> table was created</p>
<p> <img src="https://i.imgur.com/vOjViLb.png" alt="Supabase - Check that the tables were created" /></p>
</li>
</ol>
<h1 id="heading-creating-an-expo-app">Creating an Expo app</h1>
<p>Now that we are done with Supabase, it’s time to create our Expo app.</p>
<p>Because I’m using <a target="_blank" href="https://spirokit.com">SpiroKit</a>, I’ll be using the Expo Starter template that already comes with everything setup and ready to use. If you choose to also use SpiroKit, make sure to <a target="_blank" href="https://maurocodes.gumroad.com/l/spirokit-figma-react-native">get your license</a> and follow the <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-installation--page">installation instructions</a> before running the following command:</p>
<pre><code class="lang-bash">npx create-spirokit-app --template expo-template-typescript
</code></pre>
<p>If you want to create a plain React Native app with Expo, run this command instead:</p>
<pre><code class="lang-bash">npx create-react-native-app -t with-typescript

<span class="hljs-comment"># This is optional if you want to add icons to your screens. </span>
<span class="hljs-comment"># It's already included with the SpiroKit template</span>
yarn add react-native-heroicons
</code></pre>
<h1 id="heading-setting-up-supabase-in-your-expo-app">Setting up Supabase in your Expo app</h1>
<ol>
<li><p>You will need to run the following command to install all the required packages</p>
<pre><code class="lang-bash"> yarn add @supabase/supabase-js @react-native-async-storage/async-storage react-native-url-polyfill

 <span class="hljs-comment"># Required for accessing env variables during development</span>
 yarn add -D babel-plugin-inline-dotenv 

 <span class="hljs-comment"># Required to encrypt the information in the device</span>
 npx expo install expo-secure-store
</code></pre>
</li>
<li><p>Because we are using <a target="_blank" href="https://docs.expo.dev/versions/latest/sdk/securestore">Expo Secure Store</a> later to store information, we will need to update our <code>app.json</code> file to avoid issues with App Store Connect during app submission. We will set the <code>usesNonExemptEncryption</code> option to <code>false</code> like this: <em>**</em></p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"expo"</span>: {
     <span class="hljs-attr">"ios"</span>: {
       <span class="hljs-attr">"config"</span>: {
         <span class="hljs-attr">"usesNonExemptEncryption"</span>: <span class="hljs-literal">false</span>
       }
     }
   }
 }
</code></pre>
<p> Setting this property automatically handles the compliance information prompt, as described in the <a target="_blank" href="https://docs.expo.dev/versions/latest/sdk/securestore/#exempting-encryption-prompt">Expo docs</a></p>
</li>
<li><p>Now, let’s create a <code>SupabaseContext</code> that will allow us to get interact with the supabase client from any screen of our app. Run the following commands to create the src folder, then the context folder inside, and finally, a few empty files to implement our context.</p>
<pre><code class="lang-bash"> mkdir src
 mkdir src/context
 touch ./src/context SupabaseContext.tsx
 touch ./src/context SupabaseProvider.tsx
 touch ./src/context useSupabase.tsx
</code></pre>
</li>
<li><p>The <code>SupabaseContext.tsx</code> will define the contract for the provider. We’ll add a <code>isLoggedIn</code> flag to easily check the session status, and a few methods to interact with Supabase auth service</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">import</span> { createContext } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

 <span class="hljs-keyword">type</span> SupabaseContextProps = {
   isLoggedIn: <span class="hljs-built_in">boolean</span>;
   login: <span class="hljs-function">(<span class="hljs-params">email: <span class="hljs-built_in">string</span>, password: <span class="hljs-built_in">string</span></span>) =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt;;
   register: <span class="hljs-function">(<span class="hljs-params">email: <span class="hljs-built_in">string</span>, password: <span class="hljs-built_in">string</span></span>) =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt;;
   forgotPassword: <span class="hljs-function">(<span class="hljs-params">email: <span class="hljs-built_in">string</span></span>) =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt;;
   logout: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt;;
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> SupabaseContext = createContext&lt;SupabaseContextProps&gt;({
   isLoggedIn: <span class="hljs-literal">false</span>,
   login: <span class="hljs-keyword">async</span> () =&gt; {},
   register: <span class="hljs-keyword">async</span> () =&gt; {},
   forgotPassword: <span class="hljs-keyword">async</span> () =&gt; {},
   logout: <span class="hljs-keyword">async</span> () =&gt; {},
 });
</code></pre>
</li>
<li><p>Then, add the following code to the <code>SupabaseProvider.tsx</code> file to initialize the Supabase client and implement all the required methods. We also need the <code>isNavigationReady</code> to load our navigation stack after the session info is available. That way, we can hide certain routes for anonymous users.</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">import</span> <span class="hljs-string">"react-native-url-polyfill/auto"</span>;
 <span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/supabase-js"</span>;
 <span class="hljs-keyword">import</span> React, { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
 <span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> SecureStore <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-secure-store"</span>;
 <span class="hljs-keyword">import</span> { SupabaseContext } <span class="hljs-keyword">from</span> <span class="hljs-string">"./SupabaseContext"</span>;

 <span class="hljs-comment">// We are using Expo Secure Store to persist session info</span>
 <span class="hljs-keyword">const</span> ExpoSecureStoreAdapter = {
   getItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
     <span class="hljs-keyword">return</span> SecureStore.getItemAsync(key);
   },
   setItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span>, value: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
     SecureStore.setItemAsync(key, value);
   },
   removeItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
     SecureStore.deleteItemAsync(key);
   },
 };

 <span class="hljs-keyword">type</span> SupabaseProviderProps = {
   children: JSX.Element | JSX.Element[];
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> SupabaseProvider = <span class="hljs-function">(<span class="hljs-params">props: SupabaseProviderProps</span>) =&gt;</span> {
   <span class="hljs-keyword">const</span> [isLoggedIn, setLoggedIn] = useState(<span class="hljs-literal">false</span>);
   <span class="hljs-keyword">const</span> [isNavigationReady, setNavigationReady] = useState(<span class="hljs-literal">false</span>);

   <span class="hljs-keyword">const</span> supabase = createClient(
     process.env.SUPABASE_URL,
     process.env.SUPABASE_ANON_KEY,
     {
       auth: {
         storage: ExpoSecureStoreAdapter,
         autoRefreshToken: <span class="hljs-literal">true</span>,
         persistSession: <span class="hljs-literal">true</span>,
         detectSessionInUrl: <span class="hljs-literal">false</span>,
       },
     }
   );

   <span class="hljs-keyword">const</span> login = <span class="hljs-keyword">async</span> (email: <span class="hljs-built_in">string</span>, password: <span class="hljs-built_in">string</span>) =&gt; {
     <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signInWithPassword({
       email,
       password,
     });
     <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
     setLoggedIn(<span class="hljs-literal">true</span>);
   };

   <span class="hljs-keyword">const</span> register = <span class="hljs-keyword">async</span> (email: <span class="hljs-built_in">string</span>, password: <span class="hljs-built_in">string</span>) =&gt; {
     <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signUp({
       email,
       password,
     });
     <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
   };

   <span class="hljs-keyword">const</span> forgotPassword = <span class="hljs-keyword">async</span> (email: <span class="hljs-built_in">string</span>) =&gt; {
     <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.resetPasswordForEmail(email);
     <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
   };

   <span class="hljs-keyword">const</span> logout = <span class="hljs-keyword">async</span> () =&gt; {
     <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signOut();
     <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
     setLoggedIn(<span class="hljs-literal">false</span>);
   };

   <span class="hljs-keyword">const</span> checkIfUserIsLoggedIn = <span class="hljs-keyword">async</span> () =&gt; {
     <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> supabase.auth.getSession();
     setLoggedIn(result.data.session !== <span class="hljs-literal">null</span>);
     setNavigationReady(<span class="hljs-literal">true</span>);
   };

   useEffect(<span class="hljs-function">() =&gt;</span> {
     checkIfUserIsLoggedIn();
   }, []);

   <span class="hljs-keyword">return</span> (
     &lt;SupabaseContext.Provider
       value={{ isLoggedIn, login, register, forgotPassword, logout }}
     &gt;
       {isNavigationReady ? props.children : <span class="hljs-literal">null</span>}
     &lt;/SupabaseContext.Provider&gt;
   );
 };
</code></pre>
</li>
<li><p>Finally, we’ll use the <code>useSupabase.tsx</code> file to create a convenient hook:</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
 <span class="hljs-keyword">import</span> { SupabaseContext } <span class="hljs-keyword">from</span> <span class="hljs-string">"./SupabaseContext"</span>;

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useSupabase = <span class="hljs-function">() =&gt;</span> React.useContext(SupabaseContext);
</code></pre>
</li>
<li><p>Because we are using TypeScript, we’ll also need to create a <code>app.d.ts</code> to define the types for our environment variables</p>
<pre><code class="lang-typescript"> touch app.d.ts
</code></pre>
<p> Now let’s add our environment variables to the declaration file</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// app.d.ts</span>
 <span class="hljs-keyword">declare</span> <span class="hljs-keyword">namespace</span> NodeJS {
   <span class="hljs-keyword">interface</span> ProcessEnv {
     SUPABASE_URL: <span class="hljs-built_in">string</span>;
     SUPABASE_ANON_KEY: <span class="hljs-built_in">string</span>;
   }
 }
</code></pre>
<blockquote>
<p>If you still have typescript errors on your hook, run the “reload window” command in VSCode, or just close and open the IDE to refresh everything.</p>
</blockquote>
</li>
<li><p>To properly get access to our environment variables during development, we need to update our <code>babel.config.js</code> file to add the “inline-dotenv” plugin. It should look like this:</p>
<pre><code class="lang-typescript"> <span class="hljs-built_in">module</span>.<span class="hljs-built_in">exports</span> = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">api</span>) </span>{
   api.cache(<span class="hljs-literal">true</span>);
   <span class="hljs-keyword">return</span> {
     presets: [<span class="hljs-string">"babel-preset-expo"</span>],
     plugins: [<span class="hljs-string">"inline-dotenv"</span>], <span class="hljs-comment">// -&gt; Required to read env variables</span>
   };
 };
</code></pre>
<p> We’ll also need to create the <code>.env</code> file in the root of our project and provide a value for the <code>SUPABASE_URL</code> and <code>SUPABASE_ANON_KEY</code> variables:</p>
<pre><code class="lang-typescript"> touch .env
</code></pre>
<pre><code class="lang-typescript"> SUPABASE_URL=<span class="hljs-string">"YOUR_SUPABASE_URL"</span>
 SUPABASE_ANON_KEY=<span class="hljs-string">"YOUR_SUPABASE_KEY"</span>
</code></pre>
<blockquote>
<p>If you already run your app before creating the <code>.env</code> file, please run the app with the <code>expo r -c</code> command to clear the expo cache. Otherwise, your environment variables won’t work. This is required every time the <code>.env</code> file is updated.</p>
</blockquote>
</li>
</ol>
<h1 id="heading-setting-up-react-navigation">Setting up React Navigation</h1>
<p>Because we are adding a few screens like <code>Login</code>, <code>Register</code>, <code>ForgotPassword</code>, and <code>Home</code>, we will need to add React Navigation to easily navigate through the screens.</p>
<ol>
<li><p>Let’s start by installing the required dependencies</p>
<pre><code class="lang-bash"> npx expo install @react-navigation/native @react-navigation/stack react-native-gesture-handler
</code></pre>
</li>
<li><p>I’ll create a <code>navigation</code> folder to store all the required components here. But feel free to choose a different location for your files</p>
<pre><code class="lang-bash"> mkdir navigation
 touch navigation/GlobalNavigation.tsx
</code></pre>
</li>
<li><p>We’ll also need to create a few dummy screens</p>
<pre><code class="lang-bash"> mkdir screens
 touch screens/LoginScreen.tsx
 touch screens/RegisterScreen.tsx
 touch screens/ForgotPasswordScreen.tsx
 touch screens/HomeScreen.tsx
</code></pre>
<p> For all the screens, let’s add a temp UI</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// screens/LoginScreen.tsx</span>
 <span class="hljs-keyword">import</span> { Box, LargeTitle } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;

 <span class="hljs-keyword">const</span> LoginScreen = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">return</span> (
     &lt;Box safeArea&gt;
       &lt;LargeTitle&gt;Login Screen&lt;/LargeTitle&gt;
     &lt;/Box&gt;
   );
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> LoginScreen;

 <span class="hljs-comment">// screens/RegisterScreen.tsx</span>
 <span class="hljs-keyword">import</span> { Box, LargeTitle } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;

 <span class="hljs-keyword">const</span> RegisterScreen = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">return</span> (
     &lt;Box safeArea&gt;
       &lt;LargeTitle&gt;Register Screen&lt;/LargeTitle&gt;
     &lt;/Box&gt;
   );
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> RegisterScreen;

 <span class="hljs-comment">// screens/ForgotPasswordScreen.tsx</span>
 <span class="hljs-keyword">import</span> { Box, LargeTitle } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;

 <span class="hljs-keyword">const</span> ForgotPasswordScreen = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">return</span> (
     &lt;Box safeArea&gt;
       &lt;LargeTitle&gt;Forgot Password Screen&lt;/LargeTitle&gt;
     &lt;/Box&gt;
   );
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ForgotPasswordScreen;

 <span class="hljs-comment">// screens/HomeScreen.tsx</span>
 <span class="hljs-keyword">import</span> { Box, LargeTitle } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;

 <span class="hljs-keyword">const</span> HomeScreen = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">return</span> (
     &lt;Box safeArea&gt;
       &lt;LargeTitle&gt;Home Screen&lt;/LargeTitle&gt;
     &lt;/Box&gt;
   );
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> HomeScreen;
</code></pre>
</li>
<li><p>Now that we have all our screens, we can go back to the <code>GlobalNavigation.tsx</code> file and add the Stack navigation with all our screens</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// navigation/GlobalNavigation.tsx</span>

 <span class="hljs-keyword">import</span> { NavigationContainer } <span class="hljs-keyword">from</span> <span class="hljs-string">"@react-navigation/native"</span>;
 <span class="hljs-keyword">import</span> { createStackNavigator } <span class="hljs-keyword">from</span> <span class="hljs-string">"@react-navigation/stack"</span>;
 <span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
 <span class="hljs-keyword">import</span> ForgotPasswordScreen <span class="hljs-keyword">from</span> <span class="hljs-string">"../screens/ForgotPasswordScreen"</span>;
 <span class="hljs-keyword">import</span> HomeScreen <span class="hljs-keyword">from</span> <span class="hljs-string">"../screens/HomeScreen"</span>;
 <span class="hljs-keyword">import</span> LoginScreen <span class="hljs-keyword">from</span> <span class="hljs-string">"../screens/LoginScreen"</span>;
 <span class="hljs-keyword">import</span> RegisterScreen <span class="hljs-keyword">from</span> <span class="hljs-string">"../screens/RegisterScreen"</span>;
 <span class="hljs-keyword">import</span> { useSupabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../context/useSupabase"</span>;

 <span class="hljs-keyword">const</span> Stack = createStackNavigator();

 <span class="hljs-keyword">const</span> GlobalNavigation = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-comment">// We check if the user is logged in</span>
   <span class="hljs-keyword">const</span> { isLoggedIn } = useSupabase();

   <span class="hljs-keyword">return</span> (
     &lt;NavigationContainer&gt;
       &lt;Stack.Navigator
         initialRouteName={isLoggedIn ? <span class="hljs-string">"Home"</span> : <span class="hljs-string">"Login"</span>}
         screenOptions={{ headerShown: <span class="hljs-literal">false</span> }}
       &gt;
         {<span class="hljs-comment">/* Only authenticated users can access the home */</span>}
         {isLoggedIn ? (
           &lt;Stack.Screen name=<span class="hljs-string">"Home"</span> component={HomeScreen} /&gt;
         ) : (
           &lt;&gt;
             &lt;Stack.Screen name=<span class="hljs-string">"Login"</span> component={LoginScreen} /&gt;
             &lt;Stack.Screen name=<span class="hljs-string">"Register"</span> component={RegisterScreen} /&gt;
             &lt;Stack.Screen
               name=<span class="hljs-string">"ForgotPassword"</span>
               component={ForgotPasswordScreen}
             /&gt;
           &lt;/&gt;
         )}
       &lt;/Stack.Navigator&gt;
     &lt;/NavigationContainer&gt;
   );
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> GlobalNavigation;
</code></pre>
</li>
<li><p>Then, we need to update our <code>App.tsx</code> file to add our supabase context and our new global navigation.</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">import</span> { SpiroKitProvider, usePoppins, useSpiroKitTheme } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;
 <span class="hljs-keyword">import</span> GlobalNavigation <span class="hljs-keyword">from</span> <span class="hljs-string">"./src/navigation/GlobalNavigation"</span>;
 <span class="hljs-keyword">import</span> { SupabaseProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">"./src/context/SupabaseProvider"</span>;

 <span class="hljs-keyword">const</span> myTheme = useSpiroKitTheme();

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
   <span class="hljs-keyword">const</span> fontLoaded = usePoppins();

   <span class="hljs-keyword">if</span> (!fontLoaded) <span class="hljs-keyword">return</span> &lt;&gt;&lt;/&gt;;

   <span class="hljs-keyword">return</span> (
     &lt;SpiroKitProvider theme={myTheme}&gt;
       &lt;SupabaseProvider&gt;
         &lt;GlobalNavigation&gt;&lt;/GlobalNavigation&gt;
       &lt;/SupabaseProvider&gt;
     &lt;/SpiroKitProvider&gt;
   );
 }
</code></pre>
</li>
<li><p>Finally, let’s create a <code>GlobalParamList.tsx</code> inside our navigation folder to add proper IntelliSense to our routes while using <code>navigation.navigate("routeName")</code> to move between screens</p>
<pre><code class="lang-bash"> touch navigation/GlobalParamList.tsx
</code></pre>
<pre><code class="lang-typescript"> <span class="hljs-keyword">import</span> { StackNavigationProp } <span class="hljs-keyword">from</span> <span class="hljs-string">"@react-navigation/stack"</span>;

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> GlobalParamList = {
   Home: <span class="hljs-literal">undefined</span>;
   Login: <span class="hljs-literal">undefined</span>;
   Register: <span class="hljs-literal">undefined</span>;
   ForgotPassword: <span class="hljs-literal">undefined</span>;
 };

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> ScreenNavigationProp = StackNavigationProp&lt;GlobalParamList&gt;;

 <span class="hljs-keyword">declare</span> <span class="hljs-built_in">global</span> {
   <span class="hljs-keyword">namespace</span> ReactNavigation {
     <span class="hljs-keyword">interface</span> RootParamList <span class="hljs-keyword">extends</span> GlobalParamList {}
   }
 }
</code></pre>
</li>
</ol>
<h1 id="heading-updating-the-login-screen">Updating the Login screen</h1>
<p>With the Supabase context set and our navigation stack in place, we need to update all the different screens. Let’s start with the Login with a few comments:</p>
<ul>
<li>We are using the <code>useSupabase</code> hook to call the <code>login</code> method</li>
<li>We wrap the screen with a <code>KeyboardAvoidingView</code> to prevent annoying issues with the keyboard (this applies to all our screens below)</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-comment">// screens/LoginScreen.tsx</span>

<span class="hljs-keyword">import</span> {
  Body,
  Button,
  Input,
  KeyboardAvoidingView,
  Pressable,
  TitleTwo,
  VStack,
  Image,
  Subhead,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { Platform, ScrollView } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { LockClosedIcon, MailIcon } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-heroicons/outline"</span>;
<span class="hljs-keyword">import</span> { useNavigation } <span class="hljs-keyword">from</span> <span class="hljs-string">"@react-navigation/native"</span>;
<span class="hljs-keyword">import</span> { useHeaderHeight } <span class="hljs-keyword">from</span> <span class="hljs-string">"@react-navigation/elements"</span>;
<span class="hljs-keyword">import</span> { useSupabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../context/useSupabase"</span>;

<span class="hljs-keyword">const</span> LoginScreen = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> navigation = useNavigation();
  <span class="hljs-keyword">const</span> height = useHeaderHeight();
  <span class="hljs-keyword">const</span> { login } = useSupabase();

  <span class="hljs-keyword">const</span> [email, setEmail] = React.useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [password, setPassword] = React.useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [loading, setLoading] = React.useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> onSignInTapped = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      setLoading(<span class="hljs-literal">true</span>);
      <span class="hljs-keyword">await</span> login(email, password);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    } <span class="hljs-keyword">finally</span> {
      setLoading(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;KeyboardAvoidingView
      flex={<span class="hljs-number">1</span>}
      keyboardVerticalOffset={height}
      backgroundColor={<span class="hljs-string">"primaryGray.100"</span>}
      behavior={Platform.OS === <span class="hljs-string">"ios"</span> ? <span class="hljs-string">"padding"</span> : <span class="hljs-string">"height"</span>}
    &gt;
      &lt;ScrollView contentContainerStyle={{ flexGrow: <span class="hljs-number">1</span> }}&gt;
        &lt;VStack safeAreaTop padding={<span class="hljs-number">4</span>} flex={<span class="hljs-number">1</span>}&gt;
          &lt;VStack space={<span class="hljs-number">4</span>} marginTop={<span class="hljs-number">5</span>} width=<span class="hljs-string">"full"</span> flex={<span class="hljs-number">1</span>}&gt;
            &lt;Image
              source={{ uri: <span class="hljs-string">"https://i.imgur.com/FawVClJ.png"</span> }}
              width=<span class="hljs-string">"full"</span>
              height={<span class="hljs-number">200</span>}
              alt=<span class="hljs-string">"Login icon"</span>
              resizeMode=<span class="hljs-string">"contain"</span>
            &gt;&lt;/Image&gt;
            &lt;TitleTwo fontWeight=<span class="hljs-string">"medium"</span>&gt;Sign <span class="hljs-keyword">in</span>&lt;/TitleTwo&gt;
            &lt;Input
              placeholder=<span class="hljs-string">"Enter your email"</span>
              IconLeftComponent={MailIcon}
              onChangeText={<span class="hljs-function">(<span class="hljs-params">text</span>) =&gt;</span> setEmail(text)}
            &gt;&lt;/Input&gt;
            &lt;Input
              placeholder=<span class="hljs-string">"Enter your password"</span>
              secureTextEntry={<span class="hljs-literal">true</span>}
              IconLeftComponent={LockClosedIcon}
              onChangeText={<span class="hljs-function">(<span class="hljs-params">text</span>) =&gt;</span> setPassword(text)}
            &gt;&lt;/Input&gt;
            &lt;Pressable onPress={<span class="hljs-function">() =&gt;</span> navigation.navigate(<span class="hljs-string">"ForgotPassword"</span>)}&gt;
              &lt;Subhead textAlign={<span class="hljs-string">"right"</span>} paddingBottom=<span class="hljs-string">"2"</span>&gt;
                Forgot Password?
              &lt;/Subhead&gt;
            &lt;/Pressable&gt;

            &lt;Button
              isDisabled={loading}
              onPress={<span class="hljs-function">() =&gt;</span> onSignInTapped()}
              marginBottom={<span class="hljs-number">5</span>}
            &gt;
              {loading ? <span class="hljs-string">"Loading..."</span> : <span class="hljs-string">"Sign in"</span>}
            &lt;/Button&gt;
          &lt;/VStack&gt;
        &lt;/VStack&gt;

        &lt;VStack padding={<span class="hljs-number">4</span>} backgroundColor={<span class="hljs-string">"white"</span>} safeAreaBottom&gt;
          &lt;Body textAlign={<span class="hljs-string">"center"</span>}&gt;
            Have an account?{<span class="hljs-string">" "</span>}
            &lt;Pressable onPress={<span class="hljs-function">() =&gt;</span> navigation.navigate(<span class="hljs-string">"Register"</span>)}&gt;
              &lt;Body fontWeight=<span class="hljs-string">"bold"</span> textDecorationLine=<span class="hljs-string">"underline"</span>&gt;
                Sign up
              &lt;/Body&gt;
            &lt;/Pressable&gt;
          &lt;/Body&gt;
        &lt;/VStack&gt;
      &lt;/ScrollView&gt;
    &lt;/KeyboardAvoidingView&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> LoginScreen;
</code></pre>
<h1 id="heading-updating-the-register-screen">Updating the Register screen</h1>
<p>With the Login screen done, let’s work on the register screen:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// screens/RegisterScreen.tsx</span>

<span class="hljs-keyword">import</span> {
  Body,
  Button,
  Input,
  KeyboardAvoidingView,
  Pressable,
  TitleTwo,
  VStack,
  Image,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { Platform, ScrollView } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { LockClosedIcon, MailIcon } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-heroicons/outline"</span>;

<span class="hljs-keyword">import</span> { useNavigation } <span class="hljs-keyword">from</span> <span class="hljs-string">"@react-navigation/native"</span>;
<span class="hljs-keyword">import</span> { useHeaderHeight } <span class="hljs-keyword">from</span> <span class="hljs-string">"@react-navigation/elements"</span>;
<span class="hljs-keyword">import</span> { useSupabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../context/useSupabase"</span>;

<span class="hljs-keyword">const</span> RegisterScreen = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> navigation = useNavigation();
  <span class="hljs-keyword">const</span> height = useHeaderHeight();

  <span class="hljs-keyword">const</span> [email, setEmail] = React.useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [password, setPassword] = React.useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [loading, setLoading] = React.useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> { register } = useSupabase();

  <span class="hljs-keyword">const</span> onSignUpTapped = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      setLoading(<span class="hljs-literal">true</span>);
      <span class="hljs-keyword">await</span> register(email, password);
      navigation.navigate(<span class="hljs-string">"Login"</span>);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    } <span class="hljs-keyword">finally</span> {
      setLoading(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;KeyboardAvoidingView
      flex={<span class="hljs-number">1</span>}
      keyboardVerticalOffset={height}
      backgroundColor={<span class="hljs-string">"primaryGray.100"</span>}
      behavior={Platform.OS === <span class="hljs-string">"ios"</span> ? <span class="hljs-string">"padding"</span> : <span class="hljs-string">"height"</span>}
    &gt;
      &lt;ScrollView contentContainerStyle={{ flexGrow: <span class="hljs-number">1</span> }}&gt;
        &lt;VStack
          safeAreaTop
          padding={<span class="hljs-number">4</span>}
          alignItems=<span class="hljs-string">"flex-start"</span>
          width=<span class="hljs-string">"full"</span>
          flex={<span class="hljs-number">1</span>}
        &gt;
          &lt;VStack space={<span class="hljs-number">4</span>} marginTop={<span class="hljs-number">5</span>} width=<span class="hljs-string">"full"</span> flex={<span class="hljs-number">1</span>}&gt;
            &lt;Image
              source={{ uri: <span class="hljs-string">"https://i.imgur.com/oNY0QGb.png"</span> }}
              width=<span class="hljs-string">"full"</span>
              height={<span class="hljs-number">200</span>}
              alt=<span class="hljs-string">"Register icon"</span>
              resizeMode=<span class="hljs-string">"contain"</span>
            &gt;&lt;/Image&gt;
            &lt;TitleTwo fontWeight=<span class="hljs-string">"medium"</span>&gt;Sign up&lt;/TitleTwo&gt;
            &lt;Input
              placeholder=<span class="hljs-string">"Enter your email"</span>
              IconLeftComponent={MailIcon}
              onChangeText={<span class="hljs-function">(<span class="hljs-params">text</span>) =&gt;</span> setEmail(text)}
            &gt;&lt;/Input&gt;
            &lt;Input
              placeholder=<span class="hljs-string">"Enter your password"</span>
              secureTextEntry={<span class="hljs-literal">true</span>}
              IconLeftComponent={LockClosedIcon}
              onChangeText={<span class="hljs-function">(<span class="hljs-params">text</span>) =&gt;</span> setPassword(text)}
            &gt;&lt;/Input&gt;
            &lt;Button
              isDisabled={loading}
              marginBottom={<span class="hljs-number">5</span>}
              onPress={<span class="hljs-function">() =&gt;</span> onSignUpTapped()}
            &gt;
              {loading ? <span class="hljs-string">"Loading..."</span> : <span class="hljs-string">"Sign up"</span>}
            &lt;/Button&gt;
          &lt;/VStack&gt;
        &lt;/VStack&gt;
        &lt;VStack padding={<span class="hljs-number">4</span>} safeAreaBottom&gt;
          &lt;Body textAlign={<span class="hljs-string">"center"</span>}&gt;
            If you have an account,{<span class="hljs-string">" "</span>}
            &lt;Pressable onPress={<span class="hljs-function">() =&gt;</span> navigation.navigate(<span class="hljs-string">"Login"</span>)}&gt;
              &lt;Body fontWeight=<span class="hljs-string">"bold"</span> textDecorationLine=<span class="hljs-string">"underline"</span>&gt;
                Sign <span class="hljs-keyword">in</span>
              &lt;/Body&gt;
            &lt;/Pressable&gt;
          &lt;/Body&gt;
        &lt;/VStack&gt;
      &lt;/ScrollView&gt;
    &lt;/KeyboardAvoidingView&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> RegisterScreen;
</code></pre>
<h1 id="heading-updating-the-forgot-password-screen">Updating the Forgot Password screen</h1>
<ul>
<li>We are using the <code>useSupabase</code> hook to call the <code>forgotPassword</code> method</li>
<li>We are using an <code>Alert</code> component to show a message after the recovery email was sent.</li>
<li>Feel free to improve the error handling in the <code>onSendTapped</code> to show a custom error if something goes wrong.</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-comment">// screens/ForgotPasswordScreen.tsx</span>

<span class="hljs-keyword">import</span> {
  Body,
  Button,
  Input,
  KeyboardAvoidingView,
  Pressable,
  TitleTwo,
  VStack,
  Image,
  Alert,
  TitleOne,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { Platform, ScrollView } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { MailIcon } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-heroicons/outline"</span>;

<span class="hljs-keyword">import</span> { useNavigation } <span class="hljs-keyword">from</span> <span class="hljs-string">"@react-navigation/native"</span>;
<span class="hljs-keyword">import</span> { useHeaderHeight } <span class="hljs-keyword">from</span> <span class="hljs-string">"@react-navigation/elements"</span>;
<span class="hljs-keyword">import</span> { useSupabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../context/useSupabase"</span>;

<span class="hljs-keyword">const</span> ForgotPasswordScreen = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> navigation = useNavigation();
  <span class="hljs-keyword">const</span> height = useHeaderHeight();

  <span class="hljs-keyword">const</span> [email, setEmail] = React.useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [loading, setLoading] = React.useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [showResultModal, setShowResultModal] = React.useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> { forgotPassword } = useSupabase();

  <span class="hljs-keyword">const</span> onFinishTapped = <span class="hljs-function">() =&gt;</span> {
    setShowResultModal(<span class="hljs-literal">false</span>);
    navigation.navigate(<span class="hljs-string">"Login"</span>);
  };

  <span class="hljs-keyword">const</span> onSendTapped = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      setLoading(<span class="hljs-literal">true</span>);
      <span class="hljs-keyword">await</span> forgotPassword(email);
      setShowResultModal(<span class="hljs-literal">true</span>);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    } <span class="hljs-keyword">finally</span> {
      setLoading(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;KeyboardAvoidingView
      flex={<span class="hljs-number">1</span>}
      keyboardVerticalOffset={height}
      backgroundColor={<span class="hljs-string">"primaryGray.100"</span>}
      behavior={Platform.OS === <span class="hljs-string">"ios"</span> ? <span class="hljs-string">"padding"</span> : <span class="hljs-string">"height"</span>}
    &gt;
      &lt;ScrollView contentContainerStyle={{ flexGrow: <span class="hljs-number">1</span> }}&gt;
        &lt;VStack
          safeAreaTop
          padding={<span class="hljs-number">4</span>}
          alignItems=<span class="hljs-string">"flex-start"</span>
          width=<span class="hljs-string">"full"</span>
          flex={<span class="hljs-number">1</span>}
        &gt;
          &lt;VStack space={<span class="hljs-number">4</span>} marginTop={<span class="hljs-number">5</span>} width=<span class="hljs-string">"full"</span> flex={<span class="hljs-number">1</span>}&gt;
            &lt;Image
              source={{ uri: <span class="hljs-string">"https://i.imgur.com/sDzRjS4.png"</span> }}
              width=<span class="hljs-string">"full"</span>
              alt=<span class="hljs-string">"Forgot password icon"</span>
              height={<span class="hljs-number">200</span>}
              resizeMode=<span class="hljs-string">"contain"</span>
            &gt;&lt;/Image&gt;
            &lt;TitleTwo fontWeight=<span class="hljs-string">"medium"</span>&gt;Forgot password?&lt;/TitleTwo&gt;
            &lt;Input
              placeholder=<span class="hljs-string">"Enter your email"</span>
              IconLeftComponent={MailIcon}
              onChangeText={<span class="hljs-function">(<span class="hljs-params">text</span>) =&gt;</span> setEmail(text)}
            &gt;&lt;/Input&gt;
            &lt;Button
              isDisabled={loading}
              marginBottom={<span class="hljs-number">5</span>}
              onPress={<span class="hljs-function">() =&gt;</span> onSendTapped()}
            &gt;
              {loading ? <span class="hljs-string">"Loading..."</span> : <span class="hljs-string">"Send"</span>}
            &lt;/Button&gt;
          &lt;/VStack&gt;
        &lt;/VStack&gt;
        &lt;VStack padding={<span class="hljs-number">4</span>} safeAreaBottom&gt;
          &lt;Body textAlign={<span class="hljs-string">"center"</span>}&gt;
            If you have an account,{<span class="hljs-string">" "</span>}
            &lt;Pressable onPress={<span class="hljs-function">() =&gt;</span> navigation.navigate(<span class="hljs-string">"Login"</span>)}&gt;
              &lt;Body fontWeight=<span class="hljs-string">"bold"</span> textDecorationLine=<span class="hljs-string">"underline"</span>&gt;
                Sign <span class="hljs-keyword">in</span>
              &lt;/Body&gt;
            &lt;/Pressable&gt;
          &lt;/Body&gt;
        &lt;/VStack&gt;
        &lt;Alert
          isVisible={showResultModal}
          onClose={<span class="hljs-function">() =&gt;</span> setShowResultModal(<span class="hljs-literal">false</span>)}
          TitleComponent={&lt;TitleOne&gt;Email sent&lt;/TitleOne&gt;}
          ConfirmButtonComponent={
            &lt;Button onPress={<span class="hljs-function">() =&gt;</span> onFinishTapped()}&gt;Ok&lt;/Button&gt;
          }
        &gt;&lt;/Alert&gt;
      &lt;/ScrollView&gt;
    &lt;/KeyboardAvoidingView&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ForgotPasswordScreen;
</code></pre>
<h1 id="heading-updating-the-home-screen">Updating the Home Screen</h1>
<hr />
<ul>
<li>Primarily used to test routing security and allow users to <code>logout</code></li>
<li>After logout, the Supabase Context is updated, and the Global Navigation will trigger a re-render. The user is automatically redirected to the <code>Login</code> screen.</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-comment">// screens/Home.tsx</span>

<span class="hljs-keyword">import</span> { Button, LargeTitle, Image, VStack } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;
<span class="hljs-keyword">import</span> { useSupabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../context/useSupabase"</span>;

<span class="hljs-keyword">const</span> HomeScreen = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> { logout } = useSupabase();
  <span class="hljs-keyword">return</span> (
    &lt;VStack space={<span class="hljs-number">8</span>} safeArea padding={<span class="hljs-number">4</span>} flex={<span class="hljs-number">1</span>} justifyContent=<span class="hljs-string">"center"</span>&gt;
      &lt;LargeTitle textAlign={<span class="hljs-string">"center"</span>}&gt;Welcome!&lt;/LargeTitle&gt;
      &lt;Image
        source={{ uri: <span class="hljs-string">"https://i.imgur.com/k78EnxY.png"</span> }}
        width=<span class="hljs-string">"full"</span>
        alt=<span class="hljs-string">"Hello icon"</span>
        height={<span class="hljs-number">200</span>}
        resizeMode=<span class="hljs-string">"contain"</span>
      &gt;&lt;/Image&gt;
      &lt;Button onPress={<span class="hljs-function">() =&gt;</span> logout()}&gt;Logout&lt;/Button&gt;
    &lt;/VStack&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> HomeScreen;
</code></pre>
<h1 id="heading-final-thoughts">Final thoughts</h1>
<p>That was an intense article! But I wanted to cover the entire flow. There are many things we could improve or add, but I’m thinking of building a series around it. We could add a working example retrieving information from Supabase db or include additional auth mechanisms.</p>
<p>I would love to hear your thoughts! Your input can help me shape the upcoming articles.</p>
]]></content:encoded></item><item><title><![CDATA[Bridging the gap between web & native with Expo router (May 2023)]]></title><description><![CDATA[Intro
Historically speaking, trying to handle navigation on a universal app (that targets web and mobile) was a pain in the ass.
Navigation on the web usually is quite simple, and Next.js did a fantastic job with its file system-based router.
In mobi...]]></description><link>https://blog.spirokit.com/bridging-the-gap-between-web-native-with-expo-router-may-2023</link><guid isPermaLink="true">https://blog.spirokit.com/bridging-the-gap-between-web-native-with-expo-router-may-2023</guid><category><![CDATA[React Native]]></category><category><![CDATA[Expo]]></category><category><![CDATA[UI]]></category><category><![CDATA[navigation]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Mauro Garcia]]></dc:creator><pubDate>Wed, 17 May 2023 22:38:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1668353479596/8b92eY6Fb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-intro">Intro</h2>
<p>Historically speaking, trying to handle navigation on a universal app (that targets web and mobile) was a pain in the ass.</p>
<p>Navigation on the web usually is quite simple, and Next.js did a fantastic job with its file system-based router.</p>
<p>In mobile land, things are not that simple. <a target="_blank" href="https://twitter.com/FernandoTheRojo">Fernando Rojo</a> did a fantastic job with <a target="_blank" href="https://solito.dev/">Solito</a>, which is a wrapper around React Navigation and Next.js that lets you share navigation code across platforms. This project gained a lot of traction, and the community started to work on solving the problem of navigation for universal apps.</p>
<h3 id="heading-introducing-expo-router">Introducing Expo Router</h3>
<p>I heard about Expo Router a few weeks ago, and I instantly loved the concept:
What if we could have something like Next.js file-system-based router, but for universal apps? That is Expo Router.</p>
<h3 id="heading-whats-the-big-deal-about-this-library">What's the big deal about this library?</h3>
<blockquote>
<p>"Expo Router brings the best routing concepts from the web to native iOS and Android apps. Every file in the app directory automatically becomes a route in your mobile navigation, making it easier than ever to build, maintain, and scale your project."</p>
</blockquote>
<p>If you've had to deal with deep linking on your mobile apps in the past, you already know that this is another major pain.</p>
<p>Expo Router was built on top of <a target="_blank" href="https://reactnavigation.org/">React Navigation</a>, and the entire deep linking system is automatically generated, meaning that you can share the same link on the web and mobile, and the deep link will automatically work 🤯.</p>
<p>No more weird mapping and matching routes.</p>
<p>There are tons of additional features like Offline support, but if you want to learn more about all these features, here's the <a target="_blank" href="https://expo.github.io/router/docs">official docs</a></p>
<blockquote>
<p>Given that this library is still in beta, some links may change</p>
</blockquote>
<hr />
<h2 id="heading-building-a-magazine-app">Building a magazine app</h2>
<p><img src="https://i.imgur.com/AyzZKZt.png" alt /></p>
<p>I wanted to test features like tab and stack navigation to get a first sense of how it feels to work on a real app using Expo Router, so I decided to build a very simple magazine app that shows a list of news and allows the user to navigate to each news to read more about it.</p>
<p>For this demo app, I'll be using <a target="_blank" href="https://bit.ly/3Ea1TDq">SpiroKit</a>, which is a React Native UI kit I built. Given that is a paid product, feel free to follow this tutorial with your own UI.</p>
<p><a target="_blank" href="https://bit.ly/3Ea1TDq"><img src="https://i.imgur.com/MyOHaEv.png" alt /></a></p>
<hr />
<h3 id="heading-project-setup">Project setup</h3>
<h4 id="heading-with-spirokit">With SpiroKit</h4>
<p>If you've decided to use SpiroKit, follow these steps to quickly generate a new expo project with SpiroKit and Expo Router:</p>
<ol>
<li><p>Get your SpiroKit license <a target="_blank" href="https://bit.ly/3Ea1TDq">here</a> and follow <a target="_blank" href="https://docs.spirokit.com/?path=/docs/getting-started-installation--page">this instructions</a> to get access to our private npm packages.</p>
</li>
<li><p>Create a new project using the template</p>
</li>
</ol>
<pre><code class="lang-sh">npx create-spirokit-app my-expo-router-app --template expo-router-template
</code></pre>
<h4 id="heading-with-your-own-ui">With your own UI</h4>
<p>Run the following command to create a new project with expo-router:</p>
<pre><code class="lang-sh">npx create-expo-app@latest --example with-router
</code></pre>
<h3 id="heading-first-use">First use</h3>
<p>After creating my first project using the expo template, I just run <code>yarn start</code>.</p>
<p>By default, the starter templates don't include the app folder, so there is nothing to show. But we will get a friendly welcome message that will let us create our first route by only clicking the “touch app/index.js” button</p>
<p><img src="https://i.imgur.com/xiKuQcv.png" alt="welcome message from Expo for an empty app without routes" /></p>
<p>After clicking the button, I instantly got an update on all my devices (both web and mobile)</p>
<p><img src="https://i.imgur.com/Lyp2qkc.png" alt="welcome message from Expo after creating the first route" /></p>
<p>After returning to my code, I confirmed that the new <code>app/index.js</code> file was created.</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// app/index.js</span>
<span class="hljs-keyword">import</span> { Link, Stack } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { StyleSheet, Text, View } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Page</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.container}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.main}</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.title}</span>&gt;</span>Hello World<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.subtitle}</span>&gt;</span>This is the first page of your app.<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">const</span> styles = StyleSheet.create({
  <span class="hljs-attr">container</span>: {
    <span class="hljs-attr">flex</span>: <span class="hljs-number">1</span>,
    <span class="hljs-attr">alignItems</span>: <span class="hljs-string">"center"</span>,
    <span class="hljs-attr">padding</span>: <span class="hljs-number">24</span>,
  },
  <span class="hljs-attr">main</span>: {
    <span class="hljs-attr">flex</span>: <span class="hljs-number">1</span>,
    <span class="hljs-attr">justifyContent</span>: <span class="hljs-string">"center"</span>,
    <span class="hljs-attr">maxWidth</span>: <span class="hljs-number">960</span>,
    <span class="hljs-attr">marginHorizontal</span>: <span class="hljs-string">"auto"</span>,
  },
  <span class="hljs-attr">title</span>: {
    <span class="hljs-attr">fontSize</span>: <span class="hljs-number">64</span>,
    <span class="hljs-attr">fontWeight</span>: <span class="hljs-string">"bold"</span>,
  },
  <span class="hljs-attr">subtitle</span>: {
    <span class="hljs-attr">fontSize</span>: <span class="hljs-number">36</span>,
    <span class="hljs-attr">color</span>: <span class="hljs-string">"#38434D"</span>,
  },
});
</code></pre>
<p>Given that Expo Router is file-system based, we'll need to create new directories and files based on our needs.</p>
<p>My magazine app will use a bottom tab navigation with 2 main sections:</p>
<ul>
<li>News</li>
<li>Settings</li>
</ul>
<p>At the same time, the "News" section will use a Stack navigator to allow us to navigate to the details page. More about this below.</p>
<p>Let's start building our app!</p>
<h3 id="heading-1-adding-the-tab-navigator">1. Adding the Tab navigator</h3>
<p>We need to add a tab navigator so we can navigate between the "news" and "settings" tabs.</p>
<p>Expo Router includes a feature called "Layout Routes". From the <a target="_blank" href="https://expo.github.io/router/docs/features/layout-routes">official docs</a>:</p>
<blockquote>
<p>"To render shared navigation elements like a header, tab bar, or drawer, you can use a Layout Route. If a directory contains a file named _layout.js, it will be used as the layout component for all the sibling files in the directory."</p>
</blockquote>
<p>Let's create our <code>app/_layout.js</code> and add the Tab navigation we need:</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// app/_layout.js</span>
<span class="hljs-keyword">import</span> { SpiroKitProvider, usePoppins, useSpiroKitTheme } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;
<span class="hljs-keyword">import</span> { Tabs } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> TabBarComponent <span class="hljs-keyword">from</span> <span class="hljs-string">"../components/TabBar"</span>;

<span class="hljs-comment">// Setting up some global preferences for theming</span>
<span class="hljs-keyword">const</span> theme = useSpiroKitTheme({
  <span class="hljs-attr">config</span>: {
    <span class="hljs-attr">colors</span>: {
      <span class="hljs-attr">primaryGray</span>: <span class="hljs-string">"coolGray"</span>,
      <span class="hljs-attr">primaryDark</span>: <span class="hljs-string">"coolDark"</span>,
    },
  },
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Layout</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> fontLoaded = usePoppins();

  <span class="hljs-keyword">if</span> (!fontLoaded) <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;&gt;</span><span class="hljs-tag">&lt;/&gt;</span></span>;
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">SpiroKitProvider</span> <span class="hljs-attr">theme</span>=<span class="hljs-string">{theme}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Tabs</span>
        <span class="hljs-attr">tabBar</span>=<span class="hljs-string">{(tabBarProps)</span> =&gt;</span> {
          console.log(tabBarProps);
          return <span class="hljs-tag">&lt;<span class="hljs-name">TabBarComponent</span> {<span class="hljs-attr">...tabBarProps</span>}&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">TabBarComponent</span>&gt;</span>;
        }}
        screenOptions={{ headerShown: false }}
      &gt;
        <span class="hljs-tag">&lt;<span class="hljs-name">Tabs.Screen</span>
          <span class="hljs-attr">name</span>=<span class="hljs-string">"index"</span>
          <span class="hljs-attr">options</span>=<span class="hljs-string">{{</span>
            // <span class="hljs-attr">Using</span> <span class="hljs-attr">a</span> <span class="hljs-attr">custom</span> <span class="hljs-attr">title</span> <span class="hljs-attr">to</span> <span class="hljs-attr">display</span> <span class="hljs-attr">in</span> <span class="hljs-attr">the</span> <span class="hljs-attr">tab</span> <span class="hljs-attr">bar</span> <span class="hljs-attr">icon</span>
            <span class="hljs-attr">title:</span> "<span class="hljs-attr">Home</span>",
          }}
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Tabs</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">SpiroKitProvider</span>&gt;</span></span>
  );
}
</code></pre>
<h3 id="heading-2-updating-the-appindexjs-file">2. Updating the <code>app/index.js</code> file</h3>
<p>With the tabs navigator in place, I wanted to have a welcome screen (<code>app/index.js</code>), with a button that redirects to the news section.</p>
<p>Expo Router provides many options to move between routes, but given that it's still in beta, some things may not be supported yet. In this case, I'm using the <code>useRouter</code> hook to move between routes.</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// app/index.js</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { HomeIcon } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-heroicons/outline"</span>;

<span class="hljs-keyword">import</span> { Button, Image, LargeTitle, VStack } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Page</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">// The useRouter hook allows us to navigate between routes</span>
  <span class="hljs-keyword">const</span> router = useRouter();
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">VStack</span>
        <span class="hljs-attr">space</span>=<span class="hljs-string">{4}</span>
        <span class="hljs-attr">justifyContent</span>=<span class="hljs-string">"center"</span>
        <span class="hljs-attr">alignItems</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">center</span>"}
        <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span>
        <span class="hljs-attr">backgroundColor</span>=<span class="hljs-string">{{</span>
          <span class="hljs-attr">linearGradient:</span> {
            <span class="hljs-attr">colors:</span> ["<span class="hljs-attr">primary.600</span>", "<span class="hljs-attr">emerald.800</span>"],
            <span class="hljs-attr">start:</span> [<span class="hljs-attr">0</span>, <span class="hljs-attr">1</span>],
            <span class="hljs-attr">end:</span> [<span class="hljs-attr">1</span>, <span class="hljs-attr">0</span>],
          },
        }}
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Image</span>
          <span class="hljs-attr">width</span>=<span class="hljs-string">{64}</span>
          <span class="hljs-attr">borderWidth</span>=<span class="hljs-string">{8}</span>
          <span class="hljs-attr">borderColor</span>=<span class="hljs-string">"primary.500"</span>
          <span class="hljs-attr">height</span>=<span class="hljs-string">{64}</span>
          <span class="hljs-attr">borderRadius</span>=<span class="hljs-string">"full"</span>
          <span class="hljs-attr">source</span>=<span class="hljs-string">{{</span>
            <span class="hljs-attr">uri:</span> "<span class="hljs-attr">https:</span>//<span class="hljs-attr">images.pexels.com</span>/<span class="hljs-attr">photos</span>/<span class="hljs-attr">1369476</span>/<span class="hljs-attr">pexels-photo-1369476.jpeg</span>?<span class="hljs-attr">auto</span>=<span class="hljs-string">compress&amp;cs</span>=<span class="hljs-string">tinysrgb&amp;w</span>=<span class="hljs-string">1260&amp;h</span>=<span class="hljs-string">750&amp;dpr</span>=<span class="hljs-string">1</span>",
          }}
        &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">Image</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">LargeTitle</span> <span class="hljs-attr">color</span>=<span class="hljs-string">"white"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">1</span>/<span class="hljs-attr">2</span>"} <span class="hljs-attr">textAlign</span>=<span class="hljs-string">"center"</span>&gt;</span>
          Welcome to magazine App
        <span class="hljs-tag">&lt;/<span class="hljs-name">LargeTitle</span>&gt;</span>

        {/* Here I'm using the onPress event to trigger the navigation */}
        {/* This won't work until we create the news route below */}
        <span class="hljs-tag">&lt;<span class="hljs-name">Button</span>
          <span class="hljs-attr">variant</span>=<span class="hljs-string">"secondary"</span>
          <span class="hljs-attr">textColor</span>=<span class="hljs-string">"white"</span>
          <span class="hljs-attr">colorMode</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">dark</span>"}
          <span class="hljs-attr">IconLeftComponent</span>=<span class="hljs-string">{HomeIcon}</span>
          <span class="hljs-attr">width</span>=<span class="hljs-string">"auto"</span>
          <span class="hljs-attr">onPress</span>=<span class="hljs-string">{()</span> =&gt;</span> router.push("news")}
        &gt;
          Home
        <span class="hljs-tag">&lt;/<span class="hljs-name">Button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">VStack</span>&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
  );
}
</code></pre>
<p>After these changes, the welcome screen should look like this:</p>
<p><img src="https://i.imgur.com/yQ31mVe.png" alt="Welcome screen after applying the UI updates" /></p>
<h3 id="heading-3-adding-the-news-and-settings-tabs">3. Adding the "News" and "Settings" tabs</h3>
<p>Let's start by creating the <code>app/news</code> and <code>app/settings</code> directories.</p>
<pre><code class="lang-sh">mkdir news
mkdir settings
</code></pre>
<p>Your project should look like this:</p>
<pre><code class="lang-bash">├── app
│   ├── index.js
│   ├── _layout.js
│   ├── news
│   │   ├── Empty directory
│   └── settings
│   │   ├── Empty directory
├── app.json
├── babel.config.js
├── index.js
├── package.json
├── README.md
└── yarn.lock
</code></pre>
<p>We'll also need to define which navigator to use on each tab. In this case, I decided to use stack navigators on each tab.
Let's create the <code>index.js</code> and <code>_layout.js</code> files inside "news" and "settings" directories:</p>
<pre><code>touch ./app/news/index.js ./app/news/_layout.js ./app/settings/index.js ./app/settings/_layout.js
</code></pre><p>Now, your project structure should look like this:</p>
<pre><code class="lang-bash">├── app
│   ├── index.js
│   ├── _layout.js
│   ├── news
│   │   ├── index.js
│   │   └── _layout.js
│   └── settings
│       ├── index.js
│       └── _layout.js
├── app.json
├── babel.config.js
├── index.js
├── package.json
├── README.md
└── yarn.lock
</code></pre>
<p>To add the stack navigators, add this to the <code>app/news/_layout.js</code> and <code>app/settings/_layout.js</code> files:</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// app/news/_layout.js</span>
<span class="hljs-comment">// app/settings/_layout.js</span>
<span class="hljs-keyword">import</span> { Stack } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Layout</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Stack</span> <span class="hljs-attr">screenOptions</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">headerShown:</span> <span class="hljs-attr">false</span> }}&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">Stack</span>&gt;</span></span>
  )
}
</code></pre>
<p>Next, let's customize the <code>app/news/index.js</code> and <code>app/settings/index.js</code> files to include a simple message:</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// app/news/index.js</span>
<span class="hljs-keyword">import</span> { Center, LargeTitle } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">News</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Center</span> <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">LargeTitle</span>&gt;</span>News route<span class="hljs-tag">&lt;/<span class="hljs-name">LargeTitle</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Center</span>&gt;</span></span>
  )
}

<span class="hljs-comment">// app/settings/index.js</span>
<span class="hljs-keyword">import</span> { Center, LargeTitle } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">News</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Center</span> <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">LargeTitle</span>&gt;</span>Settings route<span class="hljs-tag">&lt;/<span class="hljs-name">LargeTitle</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Center</span>&gt;</span></span>
  )
}
</code></pre>
<p>You should now be able to navigate between tabs 🎉</p>
<p>Finally, let's update the <code>app/_layout.js</code> file add a custom name for the "news" and "settings" tabs. We will add a custom tab bar component in the next step that will leverage this custom title:</p>
<pre><code class="lang-diff">&lt;Tabs.Screen
  name="index"
  options={{
    title: "Home",
  }}
/&gt;
<span class="hljs-addition">+&lt;Tabs.Screen</span>
<span class="hljs-addition">+  name="news"</span>
<span class="hljs-addition">+  options={{</span>
<span class="hljs-addition">+    // here you can setup a custom title for the tab</span>
<span class="hljs-addition">+    title: "News",</span>
<span class="hljs-addition">+  }}</span>
<span class="hljs-addition">+/&gt;</span>
<span class="hljs-addition">+&lt;Tabs.Screen</span>
<span class="hljs-addition">+  name="settings"</span>
<span class="hljs-addition">+  options={{</span>
<span class="hljs-addition">+    title: "Settings",</span>
<span class="hljs-addition">+  }}</span>
<span class="hljs-addition">+/&gt;</span>
</code></pre>
<h3 id="heading-4-adding-a-custom-tab-bar-component-optional">4. Adding a custom tab bar component (optional)</h3>
<p>Something really cool about Expo Router is that it will automatically add the required information so anyone can build a custom Tab Bar component on top of it.
I decided to take advantage of the TabBar component available in SpiroKit to assign a custom icon for each tab. This will iterate through all the available routes, which are dynamically generated by Expo Router, so we don't lose that benefit by implementing a custom bar.</p>
<p>run the following commands to create a new component for the tab bar:</p>
<pre><code class="lang-bash">mkdir components
touch ./components/TabBar.js
</code></pre>
<p>Then, add the following code to the new component</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { TabBar, Caption, Box, useColorModeValue } <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;
<span class="hljs-keyword">import</span> {
  DotsHorizontalIcon,
  HomeIcon,
  NewspaperIcon,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-heroicons/outline"</span>;
<span class="hljs-keyword">import</span> { Platform } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">const</span> TabBarComponent = <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> { navigation, state, descriptors } = props;
  <span class="hljs-keyword">const</span> isWeb = Platform.OS === <span class="hljs-string">"web"</span>;

  <span class="hljs-keyword">const</span> onTabPress = <span class="hljs-function">(<span class="hljs-params">isFocused, routeKey, routeName</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> event = navigation.emit({
      <span class="hljs-attr">type</span>: <span class="hljs-string">"tabPress"</span>,
      <span class="hljs-attr">target</span>: routeKey,
      <span class="hljs-attr">canPreventDefault</span>: <span class="hljs-literal">true</span>,
    });

    <span class="hljs-keyword">if</span> (!isFocused &amp;&amp; !event.defaultPrevented) {
      navigation.navigate(routeName);
    }
  };

  <span class="hljs-keyword">const</span> getIcon = <span class="hljs-function">(<span class="hljs-params">routeName</span>) =&gt;</span> {
    <span class="hljs-keyword">switch</span> (routeName) {
      <span class="hljs-keyword">case</span> <span class="hljs-string">"index"</span>:
        <span class="hljs-keyword">return</span> HomeIcon;

      <span class="hljs-keyword">case</span> <span class="hljs-string">"news"</span>:
        <span class="hljs-keyword">return</span> NewspaperIcon;

      <span class="hljs-keyword">case</span> <span class="hljs-string">"settings"</span>:
        <span class="hljs-keyword">return</span> DotsHorizontalIcon;

      <span class="hljs-keyword">default</span>:
        <span class="hljs-keyword">return</span> DotsHorizontalIcon;
    }
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Box</span>
      <span class="hljs-attr">width</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">full</span>"}
      <span class="hljs-attr">backgroundColor</span>=<span class="hljs-string">{useColorModeValue(</span>"<span class="hljs-attr">primaryGray.100</span>", "<span class="hljs-attr">primaryDark.1</span>")}
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Box</span>
        <span class="hljs-attr">maxWidth</span>=<span class="hljs-string">{isWeb</span> ? "<span class="hljs-attr">container.lg</span>" <span class="hljs-attr">:</span> "<span class="hljs-attr">full</span>"}
        <span class="hljs-attr">margin</span>=<span class="hljs-string">"auto"</span>
        <span class="hljs-attr">width</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">full</span>"}
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">TabBar</span>&gt;</span>
          {state.routes.map((route, index) =&gt; {
            const options = descriptors[route.key].options;
            if (options.drawerItemStyle?.display === "none") return;
            return (
              <span class="hljs-tag">&lt;<span class="hljs-name">TabBar.Tab</span>
                <span class="hljs-attr">onPress</span>=<span class="hljs-string">{()</span> =&gt;</span>
                  onTabPress(state.index === index, route.key, route.name)
                }
                key={route.key}
                IconComponent={getIcon(route.name)}
                LabelComponent={
                  options.title ? (
                    <span class="hljs-tag">&lt;<span class="hljs-name">Caption</span>&gt;</span>{options.title}<span class="hljs-tag">&lt;/<span class="hljs-name">Caption</span>&gt;</span>
                  ) : (
                    <span class="hljs-tag">&lt;<span class="hljs-name">Caption</span>&gt;</span>{route.name}<span class="hljs-tag">&lt;/<span class="hljs-name">Caption</span>&gt;</span>
                  )
                }
                isFocused={state.index === index}
              /&gt;
            );
          })}
        <span class="hljs-tag">&lt;/<span class="hljs-name">TabBar</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Box</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Box</span>&gt;</span></span>
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> TabBarComponent;
</code></pre>
<p>Finally, let's update the <code>app/_layout.js</code> to use our new TabBar</p>
<pre><code class="lang-diff">// app/_layout.js
...
import { SpiroKitProvider, usePoppins, useSpiroKitTheme } from "@spirokit/core";
import { Tabs } from "expo-router";
<span class="hljs-addition">+ import TabBarComponent from "../components/TabBar";</span>

// Setting up some global preferences for theming
const theme = useSpiroKitTheme({
  config: {
    colors: {
      primaryGray: "coolGray",
      primaryDark: "coolDark",
    },
  },
});

export default function Layout() {
  const fontLoaded = usePoppins();

  if (!fontLoaded) return &lt;&gt;&lt;/&gt;;
  return (
    &lt;SpiroKitProvider theme={theme}&gt;
      &lt;Tabs
<span class="hljs-addition">+       tabBar={(tabBarProps) =&gt; {</span>
<span class="hljs-addition">+         return &lt;TabBarComponent {...tabBarProps}&gt;&lt;/TabBarComponent&gt;;</span>
<span class="hljs-addition">+       }}</span>
        screenOptions={{ headerShown: false }}
      &gt;
        &lt;Tabs.Screen
          name="index"
          options={{
            title: "Home",
          }}
        /&gt;
        &lt;Tabs.Screen
          name="news"
          options={{
            title: "News",
          }}
        /&gt;
        &lt;Tabs.Screen
          name="settings"
          options={{
            title: "Settings",
          }}
        /&gt;
      &lt;/Tabs&gt;
    &lt;/SpiroKitProvider&gt;
  );
}
...
</code></pre>
<h3 id="heading-5-adding-ui-to-the-news-route">5. Adding UI to the "News" Route</h3>
<p>Let's add some UI to our <code>news</code> route. Don't worry if you are not using <a target="_blank" href="https://bit.ly/3Ea1TDq">SpiroKit</a>. The key takeaway here is that we'll use the <code>useRouter</code> hook from Expo Router to navigate to the news details route.</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// app/news/index.js</span>

<span class="hljs-keyword">import</span> {
  Button,
  VerticalCard,
  Badge,
  Avatar,
  TitleThree,
  Subhead,
  Image,
  Footnote,
  Box,
  HorizontalCard,
  VStack,
  HStack,
  LargeTitle,
  useColorModeValue,
  Pressable,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;
<span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { ScrollView } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { BellIcon, LightBulbIcon } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-heroicons/outline"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">News</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> router = useRouter();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Box</span>
      <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span>
      <span class="hljs-attr">backgroundColor</span>=<span class="hljs-string">{useColorModeValue(</span>"<span class="hljs-attr">white</span>", "<span class="hljs-attr">primaryDark.1</span>")}
      <span class="hljs-attr">safeArea</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ScrollView</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">VStack</span> <span class="hljs-attr">space</span>=<span class="hljs-string">{4}</span> <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span> <span class="hljs-attr">padding</span>=<span class="hljs-string">{4}</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">HStack</span>
            <span class="hljs-attr">space</span>=<span class="hljs-string">{4}</span>
            <span class="hljs-attr">justifyContent</span>=<span class="hljs-string">"space-between"</span>
            <span class="hljs-attr">alignItems</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">center</span>"}
          &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">LargeTitle</span>&gt;</span>News<span class="hljs-tag">&lt;/<span class="hljs-name">LargeTitle</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Button</span> <span class="hljs-attr">IconLeftComponent</span>=<span class="hljs-string">{BellIcon}</span> <span class="hljs-attr">size</span>=<span class="hljs-string">"sm"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"auto"</span>&gt;</span>
              Subscribe
            <span class="hljs-tag">&lt;/<span class="hljs-name">Button</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">HStack</span>&gt;</span>
          {/* We are using the push method to navigate to news details */}
          <span class="hljs-tag">&lt;<span class="hljs-name">Pressable</span> <span class="hljs-attr">onPress</span>=<span class="hljs-string">{()</span> =&gt;</span> router.push("/news/1234")}&gt;
            <span class="hljs-tag">&lt;<span class="hljs-name">MainTravelCard</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">MainTravelCard</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">Pressable</span>&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">SecondaryTravelCard</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">SecondaryTravelCard</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">FoodCard</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">FoodCard</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">VStack</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">ScrollView</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Box</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">const</span> MainTravelCard = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">VerticalCard</span>
      <span class="hljs-attr">BadgeComponent</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Badge</span>&gt;</span>Travel<span class="hljs-tag">&lt;/<span class="hljs-name">Badge</span>&gt;</span>}
      UserAvatarComponent={
        <span class="hljs-tag">&lt;<span class="hljs-name">Avatar</span>
          <span class="hljs-attr">alt</span>=<span class="hljs-string">"Siv Marko profile image"</span>
          <span class="hljs-attr">source</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">uri:</span> "<span class="hljs-attr">https:</span>//<span class="hljs-attr">i.imgur.com</span>/<span class="hljs-attr">pfR8Ytj.png</span>" }}
        &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">Avatar</span>&gt;</span>
      }
      userName="Siv Marko"
      TitleComponent={
        <span class="hljs-tag">&lt;<span class="hljs-name">TitleThree</span>&gt;</span>Resting place of Australia's last convict ship<span class="hljs-tag">&lt;/<span class="hljs-name">TitleThree</span>&gt;</span>
      }
      DescriptionComponent={
        <span class="hljs-tag">&lt;<span class="hljs-name">Subhead</span>&gt;</span>
          Wellington, New Zealand (CNN) - The storm that struck the Edwin Fox on
          February 1873 might sound dramatic.
        <span class="hljs-tag">&lt;/<span class="hljs-name">Subhead</span>&gt;</span>
      }
      DateComponent={<span class="hljs-tag">&lt;<span class="hljs-name">Footnote</span>&gt;</span>15th June 2021<span class="hljs-tag">&lt;/<span class="hljs-name">Footnote</span>&gt;</span>}
      AssetComponent={
        <span class="hljs-tag">&lt;<span class="hljs-name">Image</span>
          <span class="hljs-attr">source</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">uri:</span> "<span class="hljs-attr">https:</span>//<span class="hljs-attr">i.imgur.com</span>/<span class="hljs-attr">1lSpdz3.png</span>" }}
          <span class="hljs-attr">alt</span>=<span class="hljs-string">"Image of a ship"</span>
        &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">Image</span>&gt;</span>
      }
    &gt;<span class="hljs-tag">&lt;/<span class="hljs-name">VerticalCard</span>&gt;</span></span>
  );
};

<span class="hljs-keyword">const</span> SecondaryTravelCard = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">HorizontalCard</span>
      <span class="hljs-attr">UserAvatarComponent</span>=<span class="hljs-string">{</span>
        &lt;<span class="hljs-attr">Avatar</span>
          <span class="hljs-attr">alt</span>=<span class="hljs-string">"Kenny Grimes profile image"</span>
          <span class="hljs-attr">source</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">uri:</span> "<span class="hljs-attr">https:</span>//<span class="hljs-attr">i.imgur.com</span>/<span class="hljs-attr">mwax0m0.png</span>" }}
        &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">Avatar</span>&gt;</span>
      }
      userName="Kenny Grimes"
      TitleComponent={
        <span class="hljs-tag">&lt;<span class="hljs-name">TitleThree</span>&gt;</span>
          Emirates introduces digital health verification for UAE passengers
        <span class="hljs-tag">&lt;/<span class="hljs-name">TitleThree</span>&gt;</span>
      }
      DescriptionComponent={
        <span class="hljs-tag">&lt;<span class="hljs-name">Subhead</span>&gt;</span>
          Emirates and the Dubai Health Authority (DHA) have begun to implement
          full digital verification of Covid-19 medical records
        <span class="hljs-tag">&lt;/<span class="hljs-name">Subhead</span>&gt;</span>
      }
      DateComponent={<span class="hljs-tag">&lt;<span class="hljs-name">Footnote</span>&gt;</span>15th June 2021<span class="hljs-tag">&lt;/<span class="hljs-name">Footnote</span>&gt;</span>}
      AssetLeftComponent={
        <span class="hljs-tag">&lt;<span class="hljs-name">Image</span>
          <span class="hljs-attr">source</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">uri:</span> "<span class="hljs-attr">https:</span>//<span class="hljs-attr">i.imgur.com</span>/<span class="hljs-attr">EflHxyi.png</span>" }}
          <span class="hljs-attr">alt</span>=<span class="hljs-string">"Image of a ship"</span>
        &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">Image</span>&gt;</span>
      }
    &gt;<span class="hljs-tag">&lt;/<span class="hljs-name">HorizontalCard</span>&gt;</span></span>
  );
};

<span class="hljs-keyword">const</span> FoodCard = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">HorizontalCard</span>
      <span class="hljs-attr">UserAvatarComponent</span>=<span class="hljs-string">{</span>
        &lt;<span class="hljs-attr">Avatar</span>
          <span class="hljs-attr">alt</span>=<span class="hljs-string">"Paula Green profile image"</span>
          <span class="hljs-attr">source</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">uri:</span> "<span class="hljs-attr">https:</span>//<span class="hljs-attr">i.imgur.com</span>/<span class="hljs-attr">Vbzbh6Z.png</span>" }}
        &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">Avatar</span>&gt;</span>
      }
      userName="Paula Green"
      TitleComponent={
        <span class="hljs-tag">&lt;<span class="hljs-name">TitleThree</span>&gt;</span>
          The Best Marinara Sauce You Can Get At The Store
        <span class="hljs-tag">&lt;/<span class="hljs-name">TitleThree</span>&gt;</span>
      }
      DateComponent={<span class="hljs-tag">&lt;<span class="hljs-name">Footnote</span>&gt;</span>15th June 2021<span class="hljs-tag">&lt;/<span class="hljs-name">Footnote</span>&gt;</span>}
      AssetRightComponent={LightBulbIcon}
    &gt;<span class="hljs-tag">&lt;/<span class="hljs-name">HorizontalCard</span>&gt;</span></span>
  );
};
</code></pre>
<p>Expected result:
<img src="https://i.imgur.com/IkV5Lnc.png" alt="News home screen finished" /></p>
<h3 id="heading-5-adding-ui-to-the-settings-route-optional">5. Adding UI to the "Settings" Route (Optional)</h3>
<p>I wanted to use the <code>settings</code> route to test that dark mode is propagated through the rest of the routes.</p>
<p>Let's customize the UI to add a switch that allows us to toggle dark mode:</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// app/settings/index.js</span>
<span class="hljs-keyword">import</span> {
  Body,
  Box,
  HStack,
  LargeTitle,
  Switch,
  useColorModeValue,
  VStack,
  useColorMode,
  Button,
  Input,
  Subhead,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;
<span class="hljs-keyword">import</span> { LogoutIcon, UserIcon, LinkIcon } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-heroicons/outline"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Settings</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> { toggleColorMode, colorMode } = useColorMode();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Box</span> <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Box</span>
        <span class="hljs-attr">safeAreaTop</span>
        <span class="hljs-attr">justifyContent</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">flex-end</span>"}
        <span class="hljs-attr">padding</span>=<span class="hljs-string">{4}</span>
        <span class="hljs-attr">backgroundColor</span>=<span class="hljs-string">{useColorModeValue(</span>"<span class="hljs-attr">primary.500</span>", "<span class="hljs-attr">primary.300</span>")}
        <span class="hljs-attr">minHeight</span>=<span class="hljs-string">{32}</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">LargeTitle</span> <span class="hljs-attr">color</span>=<span class="hljs-string">{useColorModeValue(</span>"<span class="hljs-attr">white</span>", "<span class="hljs-attr">primaryGray.900</span>")}&gt;</span>
          Settings
        <span class="hljs-tag">&lt;/<span class="hljs-name">LargeTitle</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Box</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">Box</span>
        <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span>
        <span class="hljs-attr">backgroundColor</span>=<span class="hljs-string">{useColorModeValue(</span>"<span class="hljs-attr">white</span>", "<span class="hljs-attr">primaryDark.1</span>")}
        <span class="hljs-attr">padding</span>=<span class="hljs-string">{4}</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">VStack</span> <span class="hljs-attr">space</span>=<span class="hljs-string">{4}</span> <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">HStack</span> <span class="hljs-attr">justifyContent</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">space-between</span>"} <span class="hljs-attr">alignItems</span>=<span class="hljs-string">"center"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Body</span> <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span>&gt;</span>Dark mode<span class="hljs-tag">&lt;/<span class="hljs-name">Body</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Switch</span> <span class="hljs-attr">onValueChange</span>=<span class="hljs-string">{toggleColorMode}</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">Switch</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">HStack</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Input</span>
            <span class="hljs-attr">IconLeftComponent</span>=<span class="hljs-string">{UserIcon}</span>
            <span class="hljs-attr">LabelComponent</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Subhead</span>&gt;</span>Name<span class="hljs-tag">&lt;/<span class="hljs-name">Subhead</span>&gt;</span>}
            defaultValue="Mauro"
          &gt;<span class="hljs-tag">&lt;/<span class="hljs-name">Input</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Input</span>
            <span class="hljs-attr">IconLeftComponent</span>=<span class="hljs-string">{UserIcon}</span>
            <span class="hljs-attr">LabelComponent</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Subhead</span>&gt;</span>Lastname<span class="hljs-tag">&lt;/<span class="hljs-name">Subhead</span>&gt;</span>}
            defaultValue="Garcia"
          &gt;<span class="hljs-tag">&lt;/<span class="hljs-name">Input</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Input</span>
            <span class="hljs-attr">IconLeftComponent</span>=<span class="hljs-string">{LinkIcon}</span>
            <span class="hljs-attr">isDisabled</span>
            <span class="hljs-attr">LabelComponent</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Subhead</span>&gt;</span>Twitter handle<span class="hljs-tag">&lt;/<span class="hljs-name">Subhead</span>&gt;</span>}
            defaultValue="https://www.twitter.com/mauro_codes"
          &gt;<span class="hljs-tag">&lt;/<span class="hljs-name">Input</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">VStack</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">Button</span> <span class="hljs-attr">IconLeftComponent</span>=<span class="hljs-string">{LogoutIcon}</span>&gt;</span>Logout<span class="hljs-tag">&lt;/<span class="hljs-name">Button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Box</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Box</span>&gt;</span></span>
  );
}
</code></pre>
<p>If everything goes well, we should now be able to toggle between light and dark mode</p>
<p><img src="https://i.imgur.com/DoEdyVP.png" alt="settings route on light and dark mode" /></p>
<h3 id="heading-6-adding-a-dynamic-route-for-the-news-details-screen">6. Adding a dynamic route for the news details screen</h3>
<p>From the <a target="_blank" href="https://expo.github.io/router/docs/features/dynamic-routes">Expo docs</a>:</p>
<blockquote>
<p>"Dynamic routes match any unmatched path at a given segment level. For example, /blog/[id] is a dynamic route. The variable part ([id]) is called a "slug"."</p>
</blockquote>
<p>We are going to use this pattern to navigate from the "news" route to the news details ("news"/[id]).</p>
<p>Remember, Expo Router is based on your file system, so let's start by creating a new file for this dynamic route:</p>
<pre><code class="lang-bash">touch ./app/news/\[id\].js
</code></pre>
<p>Inside our new <code>[id].js</code> file, let's add some UI and see how we can access the <code>id</code> param.</p>
<blockquote>
<p>Given this is a demo, I'm using hardcoded data. In real life, we would use the id from the URL to request the information using <code>fetch</code> or <code>axios</code>.</p>
</blockquote>
<pre><code class="lang-jsx"><span class="hljs-comment">// app/news/[id].js</span>
<span class="hljs-keyword">import</span> { useLocalSearchParams, useRouter, useSegments } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { Platform, ScrollView } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> {
  Avatar,
  Subhead,
  Image,
  Box,
  VStack,
  HStack,
  LargeTitle,
  useColorModeValue,
  ZStack,
  Body,
  Button,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@spirokit/core"</span>;
<span class="hljs-keyword">import</span> { ChevronLeftIcon } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-heroicons/outline"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NewsDetails</span>(<span class="hljs-params">props</span>) </span>{
  <span class="hljs-comment">// Extracting the id param from the route</span>
  <span class="hljs-keyword">const</span> { id } = useLocalSearchParams();

  <span class="hljs-comment">// This should be replaced by real data coming from an external API</span>
  <span class="hljs-keyword">const</span> content = loremIpsum;

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Box</span> <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span> <span class="hljs-attr">backgroundColor</span>=<span class="hljs-string">{useColorModeValue(</span>"<span class="hljs-attr">white</span>", "<span class="hljs-attr">primaryDark.1</span>")}&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Header</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">Header</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ScrollView</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">VStack</span> <span class="hljs-attr">space</span>=<span class="hljs-string">{4}</span> <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span> <span class="hljs-attr">padding</span>=<span class="hljs-string">{4}</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Body</span>&gt;</span>{id}<span class="hljs-tag">&lt;/<span class="hljs-name">Body</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Body</span>&gt;</span>{content}<span class="hljs-tag">&lt;/<span class="hljs-name">Body</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">VStack</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">ScrollView</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Box</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">const</span> Header = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> router = useRouter();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ZStack</span> <span class="hljs-attr">minHeight</span>=<span class="hljs-string">{56}</span> <span class="hljs-attr">overflow</span>=<span class="hljs-string">"hidden"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"full"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Image</span>
          <span class="hljs-attr">height</span>=<span class="hljs-string">{56}</span>
          <span class="hljs-attr">width</span>=<span class="hljs-string">"full"</span>
          <span class="hljs-attr">resizeMode</span>=<span class="hljs-string">"cover"</span>
          <span class="hljs-attr">source</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">uri:</span> "<span class="hljs-attr">https:</span>//<span class="hljs-attr">i.imgur.com</span>/<span class="hljs-attr">1lSpdz3.png</span>" }}
          <span class="hljs-attr">alt</span>=<span class="hljs-string">"Image of a ship"</span>
        &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">Image</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">VStack</span>
          <span class="hljs-attr">justifyContent</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">space-between</span>"}
          <span class="hljs-attr">backgroundColor</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">black:alpha.40</span>"}
          <span class="hljs-attr">padding</span>=<span class="hljs-string">{4}</span>
          <span class="hljs-attr">width</span>=<span class="hljs-string">"full"</span>
          <span class="hljs-attr">height</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">full</span>"}
        &gt;</span>
          {Platform.OS === "web" ? (
            <span class="hljs-tag">&lt;<span class="hljs-name">Button</span>
              <span class="hljs-attr">size</span>=<span class="hljs-string">"sm"</span>
              <span class="hljs-attr">width</span>=<span class="hljs-string">"auto"</span>
              <span class="hljs-attr">alignSelf</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">flex-start</span>"}
              <span class="hljs-attr">onPress</span>=<span class="hljs-string">{()</span> =&gt;</span> router.back()}
              IconLeftComponent={ChevronLeftIcon}
            &gt;<span class="hljs-tag">&lt;/<span class="hljs-name">Button</span>&gt;</span>
          ) : null}
          <span class="hljs-tag">&lt;<span class="hljs-name">LargeTitle</span> <span class="hljs-attr">numberOfLines</span>=<span class="hljs-string">{3}</span> <span class="hljs-attr">color</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">white</span>"}&gt;</span>
            Resting place of Australia's last convict ship
          <span class="hljs-tag">&lt;/<span class="hljs-name">LargeTitle</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">VStack</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">ZStack</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">AuthorLine</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">AuthorLine</span>&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
  );
};

<span class="hljs-keyword">const</span> AuthorLine = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">HStack</span>
      <span class="hljs-attr">padding</span>=<span class="hljs-string">{4}</span>
      <span class="hljs-attr">justifyContent</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">space-between</span>"}
      <span class="hljs-attr">alignItems</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">center</span>"}
      <span class="hljs-attr">space</span>=<span class="hljs-string">{4}</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">HStack</span> <span class="hljs-attr">space</span>=<span class="hljs-string">{4}</span> <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span> <span class="hljs-attr">alignItems</span>=<span class="hljs-string">"center"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Avatar</span>
          <span class="hljs-attr">size</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">sm</span>"}
          <span class="hljs-attr">alt</span>=<span class="hljs-string">"Siv Marko profile image"</span>
          <span class="hljs-attr">source</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">uri:</span> "<span class="hljs-attr">https:</span>//<span class="hljs-attr">i.imgur.com</span>/<span class="hljs-attr">pfR8Ytj.png</span>" }}
        &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">Avatar</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Body</span>&gt;</span>Siv Marko<span class="hljs-tag">&lt;/<span class="hljs-name">Body</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">HStack</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Subhead</span> <span class="hljs-attr">flex</span>=<span class="hljs-string">{1}</span> <span class="hljs-attr">textAlign</span>=<span class="hljs-string">"right"</span>&gt;</span>
        15th June 2021
      <span class="hljs-tag">&lt;/<span class="hljs-name">Subhead</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">HStack</span>&gt;</span></span>
  );
};

<span class="hljs-keyword">const</span> loremIpsum = <span class="hljs-string">`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus blandit faucibus justo, at eleifend sapien pellentesque ut. Curabitur ultrices eget arcu sit amet luctus. Mauris accumsan ut mauris eget pharetra. Quisque dignissim sed leo sed condimentum. Nullam ligula nisi, pellentesque sit amet lacus eget, malesuada tempus lorem. Pellentesque fringilla erat a faucibus semper. Sed posuere tristique vulputate.

Nam convallis tempor dictum. Donec maximus nisl a tempor condimentum. Fusce egestas velit id ante consectetur feugiat. Nam non ligula metus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce efficitur tellus leo, non vehicula lorem ornare id. Vivamus enim mauris, volutpat ut luctus semper, hendrerit eu dolor. Nunc a sapien ac ex tempus tempor. Cras odio augue, porta vitae venenatis id, sagittis at tortor. Etiam id tristique tortor, in eleifend dui. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.`</span>;
</code></pre>
<p>If you reload your app, you should now be able to navigate to the news details screen.</p>
<p><img src="https://i.imgur.com/K58tn15.png" alt="news details screen finished" /></p>
<p>Note that you didn't have to add any additional configuration to use this last route. Given that we already setup the stack navigator for the "news" directory, every child automatically becomes a valid route ✨✨</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congrats! If you are still here, you managed to build a small app that leverages a few of the available features on Expo Router.
This is just the beginning. We are in the early days of this library, so everything is changing really fast.
If you enjoyed this article, don't hesitate to leave a comment or reach out to me on Twitter. My DM's are always open.</p>
<p>I would love to hear your thoughts!
Happy coding!</p>
]]></content:encoded></item></channel></rss>