<?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[Leon Vogt]]></title><description><![CDATA[I write mainly about Hotwire and Hotwire Native. Tutorials, tips and tricks, and insights into the Hotwire ecosystem.]]></description><link>https://leonvogt.com</link><generator>RSS for Node</generator><lastBuildDate>Fri, 17 Apr 2026 10:28:53 GMT</lastBuildDate><atom:link href="https://leonvogt.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Camera Access With Hotwire Native]]></title><description><![CDATA[In this blog post we will look at how to access the camera and show a camera feed in Hotwire Native.
Before we start, if you are only interested in capturing a photo or selecting a photo from the gallery, good news: It's already built-in into Hotwire...]]></description><link>https://leonvogt.com/camera-access-with-hotwire-native</link><guid isPermaLink="true">https://leonvogt.com/camera-access-with-hotwire-native</guid><category><![CDATA[hotwire]]></category><category><![CDATA[hotwire-native]]></category><dc:creator><![CDATA[Leon Vogt]]></dc:creator><pubDate>Tue, 11 Feb 2025 11:45:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738510125787/983c5299-0b20-4f21-8e2d-e6f60136c29e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this blog post we will look at how to access the camera and show a camera feed in Hotwire Native.</p>
<p>Before we start, if you are only interested in capturing a photo or selecting a photo from the gallery, good news: It's already built-in into Hotwire Native 🎉<br />You can just use an input field with the type <code>file</code> like this:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"file"</span> <span class="hljs-attr">accept</span>=<span class="hljs-string">"image/*"</span>&gt;</span>

<span class="hljs-comment">&lt;!-- Or go directly to the "Take Photo" mode --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"file"</span> <span class="hljs-attr">accept</span>=<span class="hljs-string">"image/*"</span> <span class="hljs-attr">capture</span>=<span class="hljs-string">"camera"</span>&gt;</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738611076914/faa42875-38b0-417f-b258-ccb175df9488.png" alt /></p>
<p>Note: It works out of the box for Android. But for iOS you need to add a usage description to your <a target="_blank" href="https://developer.apple.com/documentation/bundleresources/managing-your-app-s-information-property-list">Info.plist</a> for NSCameraUsageDescription (Privacy - Camera Usage Description), where you explain why the app needs camera access.</p>
<p>In this article however we will focus on accessing the camera and show an inline camera feed in the app.</p>
<p>This can be useful for things like:</p>
<ul>
<li><p>QR-Code scanning</p>
</li>
<li><p>Document scanning</p>
</li>
<li><p>Any sort of object detection</p>
</li>
</ul>
<h3 id="heading-web">Web</h3>
<p>The most basic snippet to access the camera feed in the browser looks like this:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"camera-access"</span>&gt;</span>Open Camera<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">video</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">video</span>&gt;</span>
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#camera-access"</span>).addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> constraints = {
    <span class="hljs-attr">audio</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">video</span>: <span class="hljs-literal">true</span>,
  };
  navigator.mediaDevices
    .getUserMedia(constraints)
    .then(<span class="hljs-function">(<span class="hljs-params">mediaStream</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> video = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"video"</span>);
      video.srcObject = mediaStream;
      video.onloadedmetadata = <span class="hljs-function">() =&gt;</span> {
        video.play();
      };
    })
});
</code></pre>
<p>Two gotchas when working with getUserMedia:</p>
<ul>
<li><p>You need to have a <code>onloadedmetadata</code> callback, otherwise you might see the camera access notification light flip to green to show the camera is enabled, but you might not be able to see the feed at all</p>
</li>
<li><p>Camera access is only allowed on HTTPS urls. If you are using a HTTP url, you will get a <code>NotAllowedError: Permission denied</code> error</p>
</li>
</ul>
<p>There are many ways to set up local HTTPS. Personally, I like using <a target="_blank" href="https://ngrok.com/">ngrok</a> since I'm very lazy and it's super easy to use.</p>
<p>Before we dive in further, it will be quite handy later on in this article to have the above code in a Stimulus controller.<br />The code we work with in the following will look like this:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>

<span class="hljs-comment">// Connects to data-controller="camera"</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  <span class="hljs-keyword">static</span> targets = [<span class="hljs-string">"video"</span>]

  startCamera() {
    navigator.mediaDevices.getUserMedia(<span class="hljs-built_in">this</span>.videoOptions).then(<span class="hljs-function">(<span class="hljs-params">mediaStream</span>) =&gt;</span> {
      <span class="hljs-built_in">this</span>.videoTarget.srcObject = mediaStream
      <span class="hljs-built_in">this</span>.videoTarget.onloadedmetadata = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">this</span>.videoTarget.play()
      }
    })
  }

  <span class="hljs-keyword">get</span> <span class="hljs-title">videoOptions</span>() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">audio</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">video</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-comment">// Note: You can also specify the camera you want to use</span>
      <span class="hljs-comment">// video: { facingMode: "environment" }</span>
    }
  }
}
</code></pre>
<p>This controller can be used like this:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"camera"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"click-&gt;camera#startCamera"</span>&gt;</span>Open Camera<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">video</span> <span class="hljs-attr">data-camera-target</span>=<span class="hljs-string">"video"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">video</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>Now that we have the web implementation set up, let’s move on to the native integrations.</p>
<h3 id="heading-ios">iOS</h3>
<p>First, you need to add usage descriptions to your <code>Info.plist</code> file like mentioned above.<br />For the camera, you need to add <code>NSCameraUsageDescription</code> and explain why you need camera access.</p>
<p>Next you need to allow the <code>allowsInlineMediaPlayback</code> property in the WebView configuration. Otherwise you won't see the camera feed.<br />Wherever you configure your <a target="_blank" href="https://native.hotwired.dev/ios/configuration">Hotwire settings</a>:</p>
<pre><code class="lang-swift"><span class="hljs-type">Hotwire</span>.config.makeCustomWebView = { configuration <span class="hljs-keyword">in</span>
    configuration.allowsInlineMediaPlayback = <span class="hljs-literal">true</span>
    <span class="hljs-keyword">return</span> <span class="hljs-type">WKWebView</span>(frame: .zero, configuration: configuration)
}
</code></pre>
<p>If you try to access the camera now (through an HTTPS url), iOS will ask you for camera access, and after granting it, you should see the camera feed in your app.<br />There is only one catch: After closing and reopening the app, you'll be asked for camera access again.<br />We need to explicitly set a custom WKUIDelegate that automatically grants media capture permissions, so the user isn’t prompted every time the app is reopened—after they have granted permission initially.</p>
<pre><code class="lang-swift">@preconcurrency <span class="hljs-keyword">import</span> WebKit
<span class="hljs-keyword">import</span> HotwireNative

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomWKUIController</span>: <span class="hljs-title">WKUIController</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">webView</span><span class="hljs-params">(
        <span class="hljs-number">_</span> webView: WKWebView,
        requestMediaCapturePermissionFor origin: WKSecurityOrigin,
        initiatedByFrame frame: WKFrameInfo,
        type: WKMediaCaptureType,
        decisionHandler: @escaping <span class="hljs-params">(WKPermissionDecision)</span></span></span> -&gt; <span class="hljs-type">Void</span>) {
            decisionHandler(.grant)
        }
}
</code></pre>
<p>Note: We subclass Hotwire Native's <code>WKUIController</code> here to avoid losing the built-in <code>alert()</code> and <code>prompt()</code> functionality that Hotwire provides. Thanks to <a target="_blank" href="https://x.com/joemasilotti">Joe Masilotti</a> for pointing this out!</p>
<p>HotwireNative has a public function to set our custom WKUIDelegate.<br />So wherever we configure our Navigator, we can then set our custom WKUIDelegate like this:</p>
<pre><code class="lang-diff">private init() {
    configureHotwire()
    self.navigator = Navigator()
<span class="hljs-addition">+   self.navigator.webkitUIDelegate = CustomWKUIController(delegate: navigator)</span>
}
</code></pre>
<p>Done 🎉</p>
<p>Very little code is needed to get the camera feed working in iOS.<br />The Android part takes a bit more code.</p>
<h3 id="heading-android">Android</h3>
<p>Let’s start by telling Android we wanna have access to camera at some point.<br />This is done by adding the following two lines to the <code>AndroidManifest.xml</code> file:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.CAMERA"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">uses-feature</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.hardware.camera"</span> <span class="hljs-attr">android:required</span>=<span class="hljs-string">"false"</span> /&gt;</span>
</code></pre>
<p>Next we need to allow permission request coming from the WebView. In contrast to iOS, the WebView doesn't propagate the permission request to the app.<br />We need to handle that manually.<br />Luckily, it's pretty easy to do so. We just need to create a custom <code>WebChromeClient</code> and override the <code>onPermissionRequest</code> method.</p>
<p>Let's create a custom Chrome client that always grants the camera permission:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomChromeClient</span></span>(session: Session): HotwireWebChromeClient(session) {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onPermissionRequest</span><span class="hljs-params">(request: <span class="hljs-type">PermissionRequest</span>?)</span></span> {
        <span class="hljs-comment">// If permission request is for camera access</span>
        <span class="hljs-keyword">if</span> (request?.resources?.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE) == <span class="hljs-literal">true</span>) {
            <span class="hljs-comment">// Always grant permission</span>
            <span class="hljs-comment">// Depending on the android camera permission, the browser will still get a permission denied error if the user has not granted permission</span>
            request.grant(request.resources)
        }
    }
}
</code></pre>
<p>To use this Chrome client, we can simply override the <code>createWebChromeClient</code> method in our <code>WebFragment</code>:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">open</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebFragment</span> : <span class="hljs-type">HotwireWebFragment</span></span>() {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">createWebChromeClient</span><span class="hljs-params">()</span></span>: HotwireWebChromeClient {
        <span class="hljs-keyword">return</span> CustomChromeClient(navigator.session)
    }
}
</code></pre>
<p>You might be wondering why we always grant the camera permission and whether this is a safe thing to do.<br />Well, when you test the above code, you'll see that the camera feed still won't show up even though we granted the camera permission.<br />Only thing you see is a permission denied error in the JS console.</p>
<p>Here comes the tricky part of camera access on Android:<br />In a perfect world, we would ask the user for the camera permission inside the <code>onPermissionRequest</code> method and respond the user's decision to Chrome.</p>
<p>But by the time the <code>onPermissionRequest</code> method is called, it’s already too late to ask the user for camera permission. As far as I know, there is no way to request camera permission, wait for the user’s response, and then grant or deny the permission request.<br />This is due to the async nature of Android and that we get the permission response at a completely different place in the code. You can learn more about <a target="_blank" href="https://developer.android.com/training/permissions/requesting">Android runtime permissions here</a>.</p>
<p>There are multiple solutions for that. All of them involve asking the user <strong>before</strong> the <code>onPermissionRequest</code> method gets called.<br />You could for example ask for camera permission in the MainActivity, right after the app starts.<br />That way, when Chrome asks for the camera permission, the user has either already granted it and everything works, or if they denied it, you'll get the JS permission denied error.</p>
<p>This approach might be fine for some apps, but I don't like it very much.</p>
<p>The reason is that we <strong>request permissions without the user knowing why we need them</strong>. They never interacted with the camera button, yet they are still asked for access<br />This isn’t as user-friendly as how iOS handles it; only after the user interacts with the camera button do they get asked for camera permission.</p>
<p>One other thing I've done in the past, is asking for the camera permission when the WebView that includes the camera feature gets loaded. (Via a small Bridge component)<br />It looked something like this:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"native--bridge"</span> <span class="hljs-attr">data-native--bridge-action-value</span>=<span class="hljs-string">"askForPermission"</span> <span class="hljs-attr">data-native--bridge-payload-value</span>=<span class="hljs-string">"&lt;%= { permission: 'camera' }.to_json %&gt;"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"qr-scanner"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"click-&gt;qr-scanner#start"</span>&gt;</span>
      Start QR-Scanner
    <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>That way, the user at least doesn't get ask right after the app starts. He will be asked when he navigates to a page that needs the camera.<br />Still not the best user experience. Maybe the camera feature is only a small part of the page.</p>
<p>The goal should be to ask for the camera permission when the user interacts with the camera button.</p>
<p>To accomplish this, we can create a <a target="_blank" href="https://native.hotwired.dev/reference/bridge-components">bridge component</a> called “permissions”. When the user interacts with the camera button, we ask for the camera permission.<br />After we received the user decision we either start the actual camera feed through <code>getUserMedia</code> or show some sort of permission denied message.</p>
<p>The JS part of the bridge component looks like this:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { BridgeComponent } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/hotwire-native-bridge"</span>

<span class="hljs-keyword">const</span> PERMISSIONS = {
  <span class="hljs-attr">CAMERA</span>: <span class="hljs-string">"CAMERA"</span>,
}

<span class="hljs-comment">// Connects to data-controller="native--permissions"</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BridgeComponent</span> </span>{
  <span class="hljs-keyword">static</span> component = <span class="hljs-string">"permissions"</span>

  checkPermissions({ <span class="hljs-attr">params</span>: { permission } }) {
    <span class="hljs-keyword">const</span> sanitizedPermission = PERMISSIONS[permission]
    <span class="hljs-keyword">if</span> (!sanitizedPermission) {
      <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">`Unknown permission: <span class="hljs-subst">${permission}</span>`</span>)
      <span class="hljs-keyword">return</span>
    }

    <span class="hljs-built_in">this</span>.send(<span class="hljs-string">"checkPermissions"</span>, { <span class="hljs-attr">permission</span>: sanitizedPermission }, <span class="hljs-function">(<span class="hljs-params">message</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> granted = message.data.granted

      <span class="hljs-comment">// Dispatches a "result" event with the granted status</span>
      <span class="hljs-built_in">this</span>.dispatch(<span class="hljs-string">"result"</span>, { <span class="hljs-attr">granted</span>: granted })

      <span class="hljs-comment">// If an action only needs to be taken if the permission is granted,</span>
      <span class="hljs-comment">// we can directly listen for the "granted" or "denied" events</span>
      <span class="hljs-keyword">if</span> (granted) {
        <span class="hljs-built_in">this</span>.dispatch(<span class="hljs-string">"granted"</span>)
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-built_in">this</span>.dispatch(<span class="hljs-string">"denied"</span>)
      }
    })
  }
}
</code></pre>
<p>The native counterpart of the Bridge component:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PermissionsComponent</span></span>(
    name: String,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> hotwireDelegate: BridgeDelegate&lt;HotwireDestination&gt;
) : BridgeComponent&lt;HotwireDestination&gt;(name, hotwireDelegate) {

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> fragment: Fragment
        <span class="hljs-keyword">get</span>() = hotwireDelegate.destination.fragment

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onReceive</span><span class="hljs-params">(message: <span class="hljs-type">Message</span>)</span></span> {
        <span class="hljs-keyword">when</span> (message.event) {
            <span class="hljs-string">"checkPermissions"</span> -&gt; checkPermissions(message)
            <span class="hljs-keyword">else</span> -&gt; Log.w(<span class="hljs-string">"PermissionsComponent"</span>, <span class="hljs-string">"Unknown event for message: <span class="hljs-variable">$message</span>"</span>)
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">checkPermissions</span><span class="hljs-params">(message: <span class="hljs-type">Message</span>)</span></span> {
        <span class="hljs-keyword">val</span> <span class="hljs-keyword">data</span> = message.<span class="hljs-keyword">data</span>&lt;MessageData&gt;() ?: <span class="hljs-keyword">return</span>

        <span class="hljs-keyword">val</span> permission = Permissions.fromString(<span class="hljs-keyword">data</span>.permission) ?: run {
            Log.w(<span class="hljs-string">"PermissionsComponent"</span>, <span class="hljs-string">"Unknown permission: <span class="hljs-subst">${data.permission}</span>"</span>)
            <span class="hljs-keyword">return</span>
        }

        <span class="hljs-keyword">if</span> (hasPermission(permission.permissionString)) {
            replyTo(<span class="hljs-string">"checkPermissions"</span>, PermissionResultData(<span class="hljs-literal">true</span>))
        } <span class="hljs-keyword">else</span> {
            (fragment <span class="hljs-keyword">as</span>? PermissionRequester)?.requestPermission(permission.permissionString) { isGranted -&gt;
                replyTo(<span class="hljs-string">"checkPermissions"</span>, PermissionResultData(isGranted))
            } ?: Log.w(<span class="hljs-string">"PermissionsComponent"</span>, <span class="hljs-string">"Fragment does not implement PermissionRequester"</span>)
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">hasPermission</span><span class="hljs-params">(permission: <span class="hljs-type">String</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">return</span> ContextCompat.checkSelfPermission(
            fragment.requireContext(),
            permission
        ) == PackageManager.PERMISSION_GRANTED
    }

    <span class="hljs-meta">@Serializable</span>
    <span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MessageData</span></span>(
        <span class="hljs-meta">@SerialName(<span class="hljs-meta-string">"permission"</span>)</span> <span class="hljs-keyword">val</span> permission: String
    )

    <span class="hljs-meta">@Serializable</span>
    <span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PermissionResultData</span></span>(
        <span class="hljs-meta">@SerialName(<span class="hljs-meta-string">"granted"</span>)</span> <span class="hljs-keyword">val</span> granted: <span class="hljs-built_in">Boolean</span>
    )

    <span class="hljs-keyword">sealed</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Permissions</span></span>(<span class="hljs-keyword">val</span> permissionString: String) {
        <span class="hljs-keyword">data</span> <span class="hljs-keyword">object</span> Camera : Permissions(Manifest.permission.CAMERA)

        <span class="hljs-keyword">companion</span> <span class="hljs-keyword">object</span> {
            <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">fromString</span><span class="hljs-params">(permission: <span class="hljs-type">String</span>)</span></span>: Permissions? {
                <span class="hljs-keyword">return</span> <span class="hljs-keyword">when</span> (permission) {
                    <span class="hljs-string">"CAMERA"</span> -&gt; Camera
                    <span class="hljs-keyword">else</span> -&gt; <span class="hljs-literal">null</span>
                }
            }
        }
    }
}
</code></pre>
<p>This Permission component can't ask for permissions directly. It needs some help from either an activity or fragment.<br />In the code above you'll see that is uses a <code>PermissionRequester</code> interface that is expected to be implemented by the fragment that includes the WebView.</p>
<p>The Interface can be very simple:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">PermissionRequester</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">requestPermission</span><span class="hljs-params">(permission: <span class="hljs-type">String</span>, callback: (<span class="hljs-type">Boolean</span>) -&gt; <span class="hljs-type">Unit</span>)</span></span>
}
</code></pre>
<p>Now we only need to implement the <code>requestPermission</code> method in our <code>WebFragment</code>:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">open</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebFragment</span> : <span class="hljs-type">HotwireWebFragment</span></span>(), PermissionRequester {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> permissionCallback: ((<span class="hljs-built_in">Boolean</span>) -&gt; <span class="hljs-built_in">Unit</span>)? = <span class="hljs-literal">null</span>

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: <span class="hljs-built_in">Boolean</span> -&gt;
        permissionCallback?.invoke(isGranted)
        permissionCallback = <span class="hljs-literal">null</span>
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">requestPermission</span><span class="hljs-params">(permission: <span class="hljs-type">String</span>, callback: (<span class="hljs-type">Boolean</span>) -&gt; <span class="hljs-type">Unit</span>)</span></span> {
        permissionCallback = callback
        requestPermissionLauncher.launch(permission)
    }
    ...
}
</code></pre>
<p>That's quite a bit of Android code. But the general idea is quite simple. The bridge component reaches out to the Fragment to ask for the camera permission. The Fragment asks the user for the permission and informs the bridge component about the user's decision.<br />This user decision then gets sent back to the WebView.</p>
<p>In the web we can now listen for the <code>granted</code> or <code>denied</code> event and act accordingly.</p>
<p>We would use it in the HTML like this:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"native--permissions camera"</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"native--permissions:granted-&gt;camera#startCamera"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"click-&gt;native--permissions#checkPermissions"</span> <span class="hljs-attr">data-native--permissions-permission-param</span>=<span class="hljs-string">"CAMERA"</span>&gt;</span>Open Camera<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">video</span> <span class="hljs-attr">data-camera-target</span>=<span class="hljs-string">"video"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">video</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>That's it. Now you can ask for permissions on the Android level and inform the WebView about the current decision.<br />It's build in a way, that you can easily extend it with more permissions. Just add a new permission to the <code>Permissions</code> sealed class and ask for it in the web.<br />It's more complex than the iOS solution, but at least the user experience is the same.</p>
<p>But there is one catch…</p>
<h2 id="heading-ios-again">iOS (again)</h2>
<p>We now have two different ways of starting the camera feed. The iOS that handles the camera permission request so we can start the camera feed directly, and the Android way that asks for the camera permission before we can start the camera feed.</p>
<p>There are two basic ways to handle this. We could either handle the difference in the web like this:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"native--permissions camera"</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"native--permissions:granted-&gt;camera#startCamera"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> action = ios_app? ? <span class="hljs-string">"camera#startCamera"</span> : <span class="hljs-string">"native--permissions#checkPermissions"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"&lt;%=</span></span></span><span class="ruby"> action </span><span class="xml"><span class="hljs-tag"><span class="hljs-string">%&gt;"</span> <span class="hljs-attr">data-native--permissions-permission-param</span>=<span class="hljs-string">"CAMERA"</span>&gt;</span>Open Camera<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">video</span> <span class="hljs-attr">data-camera-target</span>=<span class="hljs-string">"video"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">video</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
<p>Or we can create a similar permission component in iOS, so our web code stays the same.</p>
<p>The iOS permission component could look like this:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> Foundation
<span class="hljs-keyword">import</span> HotwireNative
<span class="hljs-keyword">import</span> AVFoundation

<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PermissionsComponent</span>: <span class="hljs-title">BridgeComponent</span> </span>{
    <span class="hljs-keyword">override</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">var</span> <span class="hljs-title">name</span>: <span class="hljs-title">String</span> </span>{ <span class="hljs-string">"permissions"</span> }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">onReceive</span><span class="hljs-params">(message: Message)</span></span> {
        <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> event = <span class="hljs-type">Event</span>(rawValue: message.event) <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">return</span>
        }

        <span class="hljs-keyword">switch</span> event {
        <span class="hljs-keyword">case</span> .checkPermissions:
            handleCheckPermissions(message: message)
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">handleCheckPermissions</span><span class="hljs-params">(message: Message)</span></span> {
        <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> data: <span class="hljs-type">MessageData</span> = message.data() <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> }
        <span class="hljs-keyword">let</span> permission = <span class="hljs-type">Permissions</span>.fromString(data.permission)

        <span class="hljs-keyword">switch</span> permission {
        <span class="hljs-keyword">case</span> .camera:
            handleCameraPermission(message: message)
        <span class="hljs-keyword">case</span> .unknown:
            <span class="hljs-built_in">print</span>(<span class="hljs-string">"PermissionsComponent - Unknown permission: \(permission)"</span>)
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">handleCameraPermission</span><span class="hljs-params">(message: Message)</span></span> {
        <span class="hljs-keyword">let</span> status = <span class="hljs-type">AVCaptureDevice</span>.authorizationStatus(<span class="hljs-keyword">for</span>: .video)

        <span class="hljs-keyword">switch</span> status {
        <span class="hljs-keyword">case</span> .authorized:
            reply(to: <span class="hljs-type">Event</span>.checkPermissions.rawValue, with: <span class="hljs-type">PermissionResultData</span>(granted: <span class="hljs-literal">true</span>))
        <span class="hljs-keyword">case</span> .denied, .restricted:
            reply(to: <span class="hljs-type">Event</span>.checkPermissions.rawValue, with: <span class="hljs-type">PermissionResultData</span>(granted: <span class="hljs-literal">false</span>))
        <span class="hljs-keyword">case</span> .notDetermined:
            <span class="hljs-comment">// Camera access has not been requested yet</span>
            <span class="hljs-type">AVCaptureDevice</span>.requestAccess(<span class="hljs-keyword">for</span>: .video) { granted <span class="hljs-keyword">in</span>
                <span class="hljs-type">DispatchQueue</span>.main.async {
                    <span class="hljs-keyword">self</span>.reply(to: <span class="hljs-type">Event</span>.checkPermissions.rawValue, with: <span class="hljs-type">PermissionResultData</span>(granted: granted))
                }
            }
        @unknown <span class="hljs-keyword">default</span>:
            reply(to: <span class="hljs-type">Event</span>.checkPermissions.rawValue, with: <span class="hljs-type">PermissionResultData</span>(granted: <span class="hljs-literal">false</span>))
        }
    }
}

<span class="hljs-comment">// MARK: Events</span>

<span class="hljs-keyword">private</span> <span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">PermissionsComponent</span> </span>{
    <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Event</span>: <span class="hljs-title">String</span> </span>{
        <span class="hljs-keyword">case</span> checkPermissions
    }
}

<span class="hljs-comment">// MARK: Message data</span>

<span class="hljs-keyword">private</span> <span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">PermissionsComponent</span> </span>{
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">MessageData</span>: <span class="hljs-title">Decodable</span> </span>{
        <span class="hljs-keyword">let</span> permission: <span class="hljs-type">String</span>
    }

    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">PermissionResultData</span>: <span class="hljs-title">Encodable</span> </span>{
        <span class="hljs-keyword">let</span> granted: <span class="hljs-type">Bool</span>
    }

    <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Permissions</span> </span>{
        <span class="hljs-keyword">case</span> camera
        <span class="hljs-keyword">case</span> unknown

        <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">fromString</span><span class="hljs-params">(<span class="hljs-number">_</span> permission: String)</span></span> -&gt; <span class="hljs-type">Permissions</span> {
            <span class="hljs-keyword">switch</span> permission.lowercased() {
            <span class="hljs-keyword">case</span> <span class="hljs-string">"camera"</span>:
                <span class="hljs-keyword">return</span> .camera
            <span class="hljs-keyword">default</span>:
                <span class="hljs-keyword">return</span> .unknown
            }
        }
    }
}
</code></pre>
<p>Done. Now we have a similar permission component in iOS and Android and can handle the camera permission request in the same way in the web.</p>
<p>In this post, we covered how to access the camera feed in Hotwire Native, both in the web and native environments.<br />Ensuring a smooth user experience on both iOS and Android.</p>
<p>If you have any questions or run into problems, don’t hesitate to reach out!</p>
<p>And you can find the full code in <a target="_blank" href="https://github.com/leonvogt/example-42/pull/3">this Pull Request</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Hotwire Native - Switch Environments]]></title><description><![CDATA[One thing I have wanted for a while in my Hotwire Native apps is the ability to switch environments (stage, production, etc.) without having to rebuild the appThis can be useful for things like:

Testing different environments without having to rebui...]]></description><link>https://leonvogt.com/hotwire-native-switch-environments</link><guid isPermaLink="true">https://leonvogt.com/hotwire-native-switch-environments</guid><category><![CDATA[hotwire-native]]></category><category><![CDATA[hotwire]]></category><dc:creator><![CDATA[Leon Vogt]]></dc:creator><pubDate>Mon, 20 Jan 2025 15:57:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737385372822/1ee8d3ad-92e6-4612-ba19-297ec1b3b816.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One thing I have wanted for a while in my Hotwire Native apps is the ability to switch environments (stage, production, etc.) without having to rebuild the app<br />This can be useful for things like:</p>
<ul>
<li><p>Testing different environments without having to rebuild the app.</p>
</li>
<li><p>Using the currently published app in a different environment.</p>
</li>
</ul>
<p>Note: I don't go into detail on how to create a Hotwire Native app.<br />There are great resources available for that, like <a target="_blank" href="https://masilotti.com/articles/">Joe Masilotti's Blog</a> or <a target="_blank" href="https://williamkennedy.ninja/posts/">William Kennedy</a>.<br />Or more recently, the awesome <a target="_blank" href="https://masilotti.com/hotwire-native-for-rails-developers/">Hotwire Native Book by Joe Masilotti</a>.</p>
<h3 id="heading-web">Web</h3>
<p>The basic idea is to display a list of environments with their respective URLs.<br />When the user selects an environment, we will send the selected URL via a JS Bridge Component to the Hotwire Native app.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"native--base-url"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"click-&gt;native--base-url#updateBaseURL"</span> <span class="hljs-attr">data-native--base-url-url-param</span>=<span class="hljs-string">"http://192.168.1.42:3000"</span>&gt;</span>Local<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"click-&gt;native--base-url#updateBaseURL"</span> <span class="hljs-attr">data-native--base-url-url-param</span>=<span class="hljs-string">"https://example.com"</span>&gt;</span>Production<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>Note: In a real-world scenario, you may want to store the possible URLs somewhere, iterate over them and highlight which one is currently selected. But for the sake of simplicity, this will do for now.</p>
<p><strong>Bridge Component:</strong></p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { BridgeComponent } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/hotwire-native-bridge"</span>

<span class="hljs-comment">// Connects to data-controller="native--base-url"</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BridgeComponent</span> </span>{
  <span class="hljs-keyword">static</span> component = <span class="hljs-string">"base-url"</span>

  updateBaseURL({ <span class="hljs-attr">params</span>: { url } }) {
    <span class="hljs-built_in">this</span>.send(<span class="hljs-string">"updateBaseURL"</span>, { url })
  }
}
</code></pre>
<p>On the Hotwire Native side, we need to handle the message from the JS Bridge and save the selected URL so that we can use it as the base URL for upcoming requests. To store the URL in Android we use <a target="_blank" href="https://developer.android.com/reference/android/content/SharedPreferences">SharedPreferences</a>, and for iOS, we can use <a target="_blank" href="https://developer.apple.com/documentation/foundation/userdefaults">UserDefaults</a>.<br />Let's take a look at the code for both platforms.</p>
<h2 id="heading-hotwire-native-android">Hotwire Native Android</h2>
<p>First some preparation, we need a way to store the passed URL between app sessions.<br />Here's how a simple class for accessing SharedPreferences might look:</p>
<p><strong>SharedPreferencesAccess.kt:</strong></p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">object</span> SharedPreferencesAccess {
  <span class="hljs-keyword">const</span> <span class="hljs-keyword">val</span> SHARED_PREFERENCES_FILE_KEY = <span class="hljs-string">"MobileAppData"</span>
  <span class="hljs-keyword">const</span> <span class="hljs-keyword">val</span> BASE_URL_KEY = <span class="hljs-string">"BaseURL"</span>

  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setBaseURL</span><span class="hljs-params">(context: <span class="hljs-type">Context</span>, baseURL: <span class="hljs-type">String</span>)</span></span> {
    <span class="hljs-keyword">val</span> editor = getPreferences(context).edit()
    editor.putString(BASE_URL_KEY, baseURL)
    editor.apply()
  }

  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getBaseURL</span><span class="hljs-params">(context: <span class="hljs-type">Context</span>?)</span></span>: String {
    <span class="hljs-keyword">return</span> getPreferences(context!!).getString(BASE_URL_KEY, <span class="hljs-string">""</span>) ?: <span class="hljs-string">""</span>
  }

  <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getPreferences</span><span class="hljs-params">(context: <span class="hljs-type">Context</span>)</span></span>: SharedPreferences {
    <span class="hljs-keyword">return</span> context.getSharedPreferences(SHARED_PREFERENCES_FILE_KEY, Context.MODE_PRIVATE)
  }
}
</code></pre>
<p>After that, we need a way to build our URLs based on the selected environment and whether we have a saved URL or not<br />I use a viewmodel called EndpointModel for that:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EndpointModel</span></span>(application: Application):AndroidViewModel(application) {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> baseURL: String

    <span class="hljs-keyword">init</span> {
        <span class="hljs-keyword">this</span>.baseURL = loadBaseURL()
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setBaseURL</span><span class="hljs-params">(url: <span class="hljs-type">String</span>)</span></span> {
        <span class="hljs-keyword">this</span>.baseURL = url
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">loadBaseURL</span><span class="hljs-params">()</span></span>: String {
        <span class="hljs-keyword">val</span> savedURL = SharedPreferencesAccess.getBaseURL(getApplication&lt;Application&gt;().applicationContext)

        <span class="hljs-comment">// Here is the basic idea of this article.   </span>
        <span class="hljs-comment">// If we have a saved URL, we use it. </span>
        <span class="hljs-keyword">if</span> (savedURL.isNotEmpty()) {
            <span class="hljs-keyword">return</span> savedURL
        }

        <span class="hljs-comment">// Otherwise we use the default URL based on the build type.   </span>
        <span class="hljs-keyword">if</span> (BuildConfig.DEBUG) {
            <span class="hljs-keyword">return</span> LOCAL_URL
        }
        <span class="hljs-keyword">return</span> PRODUCTION_URL
    }

    <span class="hljs-keyword">val</span> startURL: String
        <span class="hljs-keyword">get</span>() { <span class="hljs-keyword">return</span> <span class="hljs-string">"<span class="hljs-variable">$baseURL</span>/home"</span> }

    <span class="hljs-keyword">val</span> pathConfigurationURL: String
        <span class="hljs-keyword">get</span>() {<span class="hljs-keyword">return</span> <span class="hljs-string">"<span class="hljs-variable">$baseURL</span>/api/v1/android/path_configuration.json"</span>}
}
</code></pre>
<p>The used constants are defined in a separate file called Constants.kt.</p>
<p><strong>Constants.kt:</strong></p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">const</span> <span class="hljs-keyword">val</span> PRODUCTION_URL = <span class="hljs-string">"https://myapp.com"</span>
<span class="hljs-keyword">const</span> <span class="hljs-keyword">val</span> LOCAL_URL = <span class="hljs-string">"http://192.168.1.42:3000"</span>
</code></pre>
<p>Ok, so far so good.<br />We have stored the selected URL and have a way to build our URLs based on if we have a saved URL or not.<br />Now we simply need to tell Hotwire Native to use the selected URL as the start location.</p>
<p>Per default, Hotwire Native expects a <code>startLocation</code> to be defined in the MainActivity.<br />To access the endpointModel, we have to initialize it first. This can be done in our “MainApplication” class (In the Hotwire Native Demo project, this class is called `DemoApplication`):</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainApplication</span> : <span class="hljs-type">Application</span></span>() {
    <span class="hljs-keyword">val</span> endpointModel: EndpointModel <span class="hljs-keyword">by</span> lazy {
        ViewModelProvider.AndroidViewModelFactory.getInstance(<span class="hljs-keyword">this</span>)
            .create(EndpointModel::<span class="hljs-keyword">class</span>.java)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-keyword">fun</span> onCreate() {
        <span class="hljs-keyword">super</span>.onCreate()

        // Load the path configuration
        Hotwire.loadPathConfiguration(
            context = this,
            location = PathConfiguration.Location(
                assetFilePath = <span class="hljs-string">"json/configuration.json"</span>,
                remoteFileUrl = endpointModel.pathConfigurationURL
            )
        )
    }
}
</code></pre>
<p>With the endpointModel in place, we can use it to define the <code>startLocation</code> in the MainActivity:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> : <span class="hljs-type">HotwireActivity</span></span>() {
    <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> endpointModel: EndpointModel

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
        <span class="hljs-keyword">this</span>.endpointModel = (application <span class="hljs-keyword">as</span> MainApplication).endpointModel

        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">navigatorConfigurations</span><span class="hljs-params">()</span></span> = listOf(
        NavigatorConfiguration(
            name = <span class="hljs-string">"main"</span>,
            startLocation = endpointModel.startURL,
            navigatorHostId = R.id.main_nav_host
        )
    )
}
</code></pre>
<p>Now we have all the pieces in place to dynamically switch between different environments in our Hotwire Native app.<br />The only thing missing is the Bridge Component which handles the message from the web and updates the base URL:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseURLComponent</span></span>(
    name: String,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> hotwireDelegate: BridgeDelegate&lt;HotwireDestination&gt;
) : BridgeComponent&lt;HotwireDestination&gt;(name, hotwireDelegate) {

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> fragment: Fragment
        <span class="hljs-keyword">get</span>() = hotwireDelegate.destination.fragment

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onReceive</span><span class="hljs-params">(message: <span class="hljs-type">Message</span>)</span></span> {
        <span class="hljs-keyword">when</span> (message.event) {
            <span class="hljs-string">"updateBaseURL"</span> -&gt; updateBaseURL(message)
            <span class="hljs-keyword">else</span> -&gt; Log.w(<span class="hljs-string">"BaseURLComponent"</span>, <span class="hljs-string">"Unknown event for message: <span class="hljs-variable">$message</span>"</span>)
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">updateBaseURL</span><span class="hljs-params">(message: <span class="hljs-type">Message</span>)</span></span> {
        <span class="hljs-keyword">val</span> <span class="hljs-keyword">data</span> = message.<span class="hljs-keyword">data</span>&lt;MessageData&gt;() ?: <span class="hljs-keyword">return</span>
        <span class="hljs-keyword">val</span> url = <span class="hljs-keyword">data</span>.url

        <span class="hljs-comment">// Save the new base URL to SharedPreferences</span>
        SharedPreferencesAccess.setBaseURL(fragment.requireContext(), url)

        <span class="hljs-comment">// Apply the new base URL and reset the navigators</span>
        <span class="hljs-keyword">val</span> mainActivity = fragment.activity <span class="hljs-keyword">as</span>? MainActivity
        mainActivity?.endpointModel?.setBaseURL(url)
        mainActivity?.delegate?.resetNavigators()
    }

    <span class="hljs-meta">@Serializable</span>
    <span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MessageData</span></span>(
        <span class="hljs-meta">@SerialName(<span class="hljs-meta-string">"url"</span>)</span> <span class="hljs-keyword">val</span> url: String
    )
}
</code></pre>
<p>Done 🎉<br />This should outline all the steps needed to switch between different environments in a Hotwire Native Android app.<br />But there is one catch. When you try this, you may notice that environment switch only works when you restart the app.<br />Problem is that Hotwire Native doesn't recognize the new base URL as an 'in-app navigation' and will open the URLs with the new base URL in an external browser.<br />This is because the <code>AppNavigationRouteDecisionHandler</code> doesn't know about the new base URL. Not sure if this is expected behavior or a bug in Hotwire Native Android to be honest. But we easily can fix this by adding a custom <code>Router.RouteDecisionHandler</code>. You can learn more about route handlers at the <a target="_blank" href="https://native.hotwired.dev/android/reference#handling-url-routes">official documentation</a>.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NavigationDecisionHandler</span> : <span class="hljs-type">Router.RouteDecisionHandler {</span></span>
    <span class="hljs-keyword">override</span> <span class="hljs-keyword">val</span> name = <span class="hljs-string">"app-navigation-router"</span>

    <span class="hljs-keyword">override</span> <span class="hljs-keyword">val</span> decision = Router.Decision.NAVIGATE

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">matches</span><span class="hljs-params">(
        location: <span class="hljs-type">String</span>,
        configuration: <span class="hljs-type">NavigatorConfiguration</span>
    )</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">val</span> baseURL = MainApplication().endpointModel.homeURL
        <span class="hljs-keyword">return</span> baseURL.toUri().host == location.toUri().host
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">handle</span><span class="hljs-params">(
        location: <span class="hljs-type">String</span>,
        configuration: <span class="hljs-type">NavigatorConfiguration</span>,
        activity: <span class="hljs-type">HotwireActivity</span>
    )</span></span> {
        <span class="hljs-comment">// No-op</span>
    }
}
</code></pre>
<p>The only missing piece now, is to inform Hotwire Native about our new router and register it together with the default routers we wanna use. This can be done in the MainApplication.kt file, where we have the other Hotwire Native configuration as well:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// Register route decision handlers</span>
<span class="hljs-comment">// https://native.hotwired.dev/android/reference#handling-url-routes</span>
Hotwire.registerRouteDecisionHandlers(
    NavigationDecisionHandler(),
    AppNavigationRouteDecisionHandler(),
    BrowserRouteDecisionHandler()
)
</code></pre>
<p>Aaand now we are done 🎉🎉.<br />This time for real.<br />The iOS side is a bit easier, I promise.</p>
<h2 id="heading-hotwire-native-ios">Hotwire Native iOS</h2>
<p>The key idea is the same: save the selected URL and use it as the base URL for upcoming requests. A basic UserDefaultsAccess class could look like this:</p>
<p><strong>UserDefaultsAccess:</strong></p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> Foundation

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserDefaultsAccess</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">let</span> <span class="hljs-type">KEY_BASE_URL</span> = <span class="hljs-string">"BaseURL"</span>

  <span class="hljs-keyword">private</span> <span class="hljs-keyword">init</span>(){}

  <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">setBaseURL</span><span class="hljs-params">(url: String)</span></span> {
    <span class="hljs-type">UserDefaults</span>.standard.<span class="hljs-keyword">set</span>(url, forKey: <span class="hljs-type">KEY_BASE_URL</span>)
  }

  <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getBaseURL</span><span class="hljs-params">()</span></span> -&gt; <span class="hljs-type">String</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-type">UserDefaults</span>.standard.string(forKey: <span class="hljs-type">KEY_BASE_URL</span>) ?? <span class="hljs-string">""</span>
  }
}
</code></pre>
<p>Now we need a way to build our URL’s based on the selected environment and whether we have a saved URL or not.<br />Similar to the Android side, I created a class called Endpoint that contains this logic.</p>
<p><strong>Endpoint:</strong></p>
<pre><code class="lang-swift"><span class="hljs-comment">// Learn more about this Endpoint class at this Joe Masilotti's Blog post: https://masilotti.com/turbo-ios/tips-and-tricks/</span>
<span class="hljs-keyword">import</span> Foundation

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Endpoint</span> </span>{
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">let</span> instance = <span class="hljs-type">Endpoint</span>()

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> baseURL: <span class="hljs-type">URL</span> {
        <span class="hljs-keyword">let</span> baseURL = <span class="hljs-type">UserDefaultsAccess</span>.getBaseURL()

        <span class="hljs-comment">// Same as in Android.</span>
        <span class="hljs-comment">// If we have a saved URL, we use it.</span>
        <span class="hljs-keyword">if</span> !baseURL.isEmpty {
            <span class="hljs-keyword">return</span> <span class="hljs-type">URL</span>(string: baseURL)!
        }

        <span class="hljs-comment">// Otherwise we use the default URL based on the current environment.</span>
        <span class="hljs-keyword">switch</span> <span class="hljs-type">Environment</span>.current {
        <span class="hljs-keyword">case</span> .development:
            <span class="hljs-keyword">return</span> <span class="hljs-type">URL</span>(string: <span class="hljs-string">"http://192.168.1.42:3000"</span>)!
        <span class="hljs-keyword">case</span> .production:
            <span class="hljs-keyword">return</span> <span class="hljs-type">URL</span>(string: <span class="hljs-string">"https://myapp.com"</span>)!
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">init</span>() {}

    <span class="hljs-keyword">var</span> start: <span class="hljs-type">URL</span> {
        <span class="hljs-keyword">return</span> baseURL.appendingPathComponent(<span class="hljs-string">"/home"</span>)
    }

    <span class="hljs-keyword">var</span> pathConfiguration: <span class="hljs-type">URL</span> {
        <span class="hljs-keyword">return</span> baseURL.appendingPathComponent(<span class="hljs-string">"/api/v1/ios/path_configuration.json"</span>)
    }
}
</code></pre>
<p><strong>Wherever you make the initial request:</strong></p>
<pre><code class="lang-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">didStart</span><span class="hljs-params">()</span></span> {
  navigator.route(<span class="hljs-type">Endpoint</span>.instance.start)
}
</code></pre>
<p>With that in place, we can implement the Bridge Component that handles the message from the web and updates the base URL.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> Foundation
<span class="hljs-keyword">import</span> HotwireNative
<span class="hljs-keyword">import</span> UIKit

<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseURLComponent</span>: <span class="hljs-title">BridgeComponent</span> </span>{
    <span class="hljs-keyword">override</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">var</span> <span class="hljs-title">name</span>: <span class="hljs-title">String</span> </span>{ <span class="hljs-string">"base-url"</span> }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">onReceive</span><span class="hljs-params">(message: Message)</span></span> {
        <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> event = <span class="hljs-type">Event</span>(rawValue: message.event) <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">return</span>
        }

        <span class="hljs-keyword">switch</span> event {
        <span class="hljs-keyword">case</span> .updateBaseURL:
            handleupdateBaseURL(message: message)
        }
    }

    <span class="hljs-comment">// MARK: Private</span>

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">handleupdateBaseURL</span><span class="hljs-params">(message: Message)</span></span> {
        <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> data: <span class="hljs-type">MessageData</span> = message.data() <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> }
        <span class="hljs-keyword">let</span> url = data.url
        <span class="hljs-type">UserDefaultsAccess</span>.setBaseURL(url: url)
        <span class="hljs-type">HotwireCentral</span>.instance.resetNavigator()
    }
}

<span class="hljs-comment">// MARK: Events</span>

<span class="hljs-keyword">private</span> <span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">BaseURLComponent</span> </span>{
    <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Event</span>: <span class="hljs-title">String</span> </span>{
        <span class="hljs-keyword">case</span> updateBaseURL
    }
}

<span class="hljs-comment">// MARK: Message data</span>

<span class="hljs-keyword">private</span> <span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">BaseURLComponent</span> </span>{
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">MessageData</span>: <span class="hljs-title">Decodable</span> </span>{
        <span class="hljs-keyword">let</span> url: <span class="hljs-type">String</span>
    }
}
</code></pre>
<p>Almost finished. The Bridge Component calls <code>resetNavigator</code> to apply the new base URL and reset the navigators. The job of <code>resetNavigator</code> is to set the new PathConfiguration properties, based on the new base URL, and reinitialize the Navigator to apply the new settings.</p>
<p>This function doesn't exist yet, so we need to add it to the HotwireCentral class:</p>
<pre><code class="lang-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">resetNavigator</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">self</span>.pathConfiguration = <span class="hljs-type">PathConfiguration</span>(sources: [
        .server(<span class="hljs-type">Endpoint</span>.instance.pathConfiguration)
    ])
    <span class="hljs-keyword">self</span>.navigator = <span class="hljs-type">Navigator</span>(pathConfiguration: pathConfiguration)
    navigator.route(<span class="hljs-type">Endpoint</span>.instance.start)
}
</code></pre>
<p>That's it! 🎉<br />This way you can easily switch between different environments in your Hotwire Native app without having to rebuild the app.</p>
<p>You can find the full code from this post in this PR: <a target="_blank" href="https://github.com/leonvogt/example-42/pull/1">https://github.com/leonvogt/example-42/pull/1</a></p>
<p>If you have any questions, feedback or ideas on how to improve this approach, feel free to reach out!</p>
]]></content:encoded></item></channel></rss>