<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://mikedalton.co/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mikedalton.co/" rel="alternate" type="text/html" /><updated>2026-03-02T05:38:59+00:00</updated><id>https://mikedalton.co/feed.xml</id><title type="html">Mike Dalton</title><subtitle>Software developer from Philadelphia.</subtitle><author><name>Mike Dalton</name></author><entry><title type="html">Implementing OAuth in Hotwire Native apps with Bridge Components</title><link href="https://mikedalton.co/2026/01/26/hotwire-native-oauth-bridge-component/" rel="alternate" type="text/html" title="Implementing OAuth in Hotwire Native apps with Bridge Components" /><published>2026-01-26T00:00:00+00:00</published><updated>2026-01-26T00:00:00+00:00</updated><id>https://mikedalton.co/2026/01/26/hotwire-native-oauth-bridge-component</id><content type="html" xml:base="https://mikedalton.co/2026/01/26/hotwire-native-oauth-bridge-component/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>OAuth is a common technique for registering and authenticating users on web and mobile apps.
Many interactions in a Hotwire-based Rails app work without requiring native code but that’s not the case for OAuth.</p>

<p>Hotwire Native uses embedded web views to navigate the application.
This works well for most web pages but is considered insecure by Google and other OAuth providers.<sup id="fnref:googleoauthblogpost"><a href="#fn:googleoauthblogpost" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<blockquote>
  <p>…[Google] introduced a new secure browser policy prohibiting Google OAuth requests in embedded browser libraries commonly referred to as embedded webviews. All embedded webviews will be blocked…<sup id="fnref:googleoauthblogpost:1"><a href="#fn:googleoauthblogpost" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>
</blockquote>

<p>If we try to use a web view, Google tells us they’ve blocked this user agent.</p>

<p><img src="/assets/images/hotwire-native-oauth-bridge-component/oauth-google-access-blocked.png" alt="Google OAuth Blocking Web View Access" width="250" /></p>

<p>Instead of using a web view, its recommended to use a system browser.<sup id="fnref:usesystembrowser"><a href="#fn:usesystembrowser" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<p>This post describes how to implement OAuth in a Hotwire Native application using a minimum of native code.
We will first show how to implement OAuth in a Rails app and then show how to extend it to support Hotwire Native.</p>

<h2 id="web-implementation">Web Implementation</h2>

<video autoplay="" controls="" playsinline="" style="display: block; margin: 0 auto;" loading="lazy"><source src="/assets/videos/hotwire-native-oauth-bridge-component/web.webm" type="video/webm" />Your browser does not support the video tag.
</video>

<p class="video-caption">Web-based OAuth Sign In</p>

<p>Before we implement OAuth on our iOS and Android apps, we need to implement it in our web app.</p>

<p>It’s common to use libraries like <a href="https://github.com/heartcombo/devise">Devise</a> and <a href="https://github.com/omniauth/omniauth">Omniauth</a> for authentication and OAuth but here we’re going to use the <a href="https://guides.rubyonrails.org/security.html#authentication">Rails authentication generator</a> and use the OAuth libraries directly instead of Omniauth.
The primary reason for this is that the Apple Omniauth library does not seem to be actively maintained and monkey-patching is required to get it to work.<sup id="fnref:omniauth-apple-issues"><a href="#fn:omniauth-apple-issues" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>
It’s also easier to troubleshoot issues that arise if we are implementing the code ourselves.</p>

<p>The following sequence diagram describes the web OAuth process we’ll implement.</p>

<pre><code class="language-mermaid">sequenceDiagram
    actor User
    participant Browser
    participant Rails
    participant Apple

    User-&gt;&gt;Browser: click "Sign in with ..."
    Browser-&gt;&gt;Rails: POST /apple_oauth_sessions
    Rails--&gt;&gt;Browser: 302 https://appleid.apple.com/auth/authorize...
    Browser-&gt;&gt;Apple: GET https://appleid.apple.com/auth/authorize...
    Apple--&gt;&gt;Browser: 200 OK
    User-&gt;&gt;Browser: fill in credentials
    Browser-&gt;&gt;Apple: POST https://appleid.apple.com/appleauth/auth/oauth/authorize
    Apple--&gt;&gt;Browser: 200 OK
    Browser-&gt;&gt;Rails: POST /apple_oauth_sessions/callback
    Note over Browser,Rails: Authenticate user
    Rails--&gt;&gt;Browser: 302 /entries
    Browser-&gt;&gt;Rails: GET /entries
    Rails--&gt;&gt;Browser: 200 OK
</code></pre>

<p>The only Gem we will need is <a href="https://github.com/jwt/ruby-jwt">JWT</a> to parse the JSON web token we receive from the OAuth provider.
Add the following to your <code class="language-plaintext highlighter-rouge">Gemfile</code> and run <code class="language-plaintext highlighter-rouge">bundle install</code>.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">gem</span> <span class="s2">"jwt"</span><span class="p">,</span> <span class="s2">"~&gt; 3.1.2"</span></code></pre><figcaption class="code-caption">Gemfile</figcaption></figure>

<p>We will need to create a new controller with two actions.
The first action will redirect the user to the OAuth provider.
The second action will be called by the OAuth provider and it will register and/or sign in the user.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">resource</span> <span class="ss">:apple_oauth_sessions</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[ create ]</span> <span class="k">do</span>
  <span class="n">collection</span> <span class="k">do</span>
    <span class="n">post</span> <span class="ss">:callback</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre><figcaption class="code-caption">config/routes.rb</figcaption></figure>

<p>The create action builds the OAuth provider’s authorization URL using:</p>

<ol>
  <li>a client ID you get from the provider</li>
  <li>a callback URL that is called after the user authentications with the provider</li>
  <li>a state and nonce you generate and store for verification later.</li>
</ol>

<p>The state and nonce cookies for Apple’s OAuth implementation must be <code class="language-plaintext highlighter-rouge">SameSite: None; Secure</code> since Apple implicitly triggers a new session by using a POST callback instead of a GET callback.</p>

<p>The result of the action is an external redirect to the authorization URL.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">AppleOauthSessionsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="n">allow_unauthenticated_access</span>

  <span class="k">def</span> <span class="nf">create</span>
    <span class="n">nonce</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">urlsafe_base64</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span>
    <span class="n">state</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">hex</span><span class="p">(</span><span class="mi">24</span><span class="p">)</span> <span class="o">+</span> <span class="s2">":"</span> <span class="o">+</span> <span class="n">params</span><span class="p">[</span><span class="ss">:platform</span><span class="p">]</span>
    <span class="n">redirect_uri</span> <span class="o">=</span> <span class="n">callback_apple_oauth_sessions_url</span>

    <span class="n">cookies</span><span class="p">.</span><span class="nf">encrypted</span><span class="p">[</span><span class="ss">:apple_oauth_state</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">same_site: :none</span><span class="p">,</span>
      <span class="ss">expires: </span><span class="mi">1</span><span class="p">.</span><span class="nf">hour</span><span class="p">.</span><span class="nf">from_now</span><span class="p">,</span>
      <span class="ss">secure: </span><span class="kp">true</span><span class="p">,</span>
      <span class="ss">value: </span><span class="n">state</span>
    <span class="p">}</span>
    <span class="n">cookies</span><span class="p">.</span><span class="nf">encrypted</span><span class="p">[</span><span class="ss">:apple_oauth_nonce</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">same_site: :none</span><span class="p">,</span>
      <span class="ss">expires: </span><span class="mi">1</span><span class="p">.</span><span class="nf">hour</span><span class="p">.</span><span class="nf">from_now</span><span class="p">,</span>
      <span class="ss">secure: </span><span class="kp">true</span><span class="p">,</span>
      <span class="ss">value: </span><span class="n">nonce</span>
    <span class="p">}</span>

    <span class="n">oauth_client</span> <span class="o">=</span> <span class="no">AppleOauthClient</span><span class="p">.</span><span class="nf">new</span>
    <span class="n">authorization_url</span> <span class="o">=</span> <span class="n">oauth_client</span><span class="p">.</span><span class="nf">authorization_url</span><span class="p">(</span>
      <span class="ss">state: </span><span class="n">state</span><span class="p">,</span>
      <span class="ss">nonce: </span><span class="n">nonce</span><span class="p">,</span>
      <span class="ss">redirect_uri: </span><span class="n">redirect_uri</span>
    <span class="p">)</span>

    <span class="n">redirect_to</span> <span class="n">authorization_url</span><span class="p">,</span> <span class="ss">allow_other_host: </span><span class="kp">true</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre><figcaption class="code-caption">app/controllers/apple_oauth_sessions_controller.rb</figcaption></figure>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">AppleOauthClient</span>
  <span class="no">AUTHORIZE_URL</span> <span class="o">=</span> <span class="s2">"https://appleid.apple.com/auth/authorize"</span>

  <span class="k">def</span> <span class="nf">authorization_url</span><span class="p">(</span><span class="n">state</span><span class="p">:,</span> <span class="n">nonce</span><span class="p">:,</span> <span class="n">redirect_uri</span><span class="p">:)</span>
    <span class="n">query_string</span> <span class="o">=</span> <span class="p">{</span>
      <span class="n">client_id</span><span class="p">:,</span>
      <span class="n">nonce</span><span class="p">:,</span>
      <span class="n">redirect_uri</span><span class="p">:,</span>
      <span class="ss">response_mode: </span><span class="s2">"form_post"</span><span class="p">,</span>
      <span class="ss">response_type: </span><span class="s2">"code"</span><span class="p">,</span>
      <span class="ss">scope: </span><span class="s2">"email name"</span><span class="p">,</span>
      <span class="ss">state:
    </span><span class="p">}.</span><span class="nf">to_query</span>

    <span class="s2">"</span><span class="si">#{</span><span class="no">AUTHORIZE_URL</span><span class="si">}</span><span class="s2">?</span><span class="si">#{</span><span class="n">query_string</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">client_id</span>
    <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:apple</span><span class="p">,</span> <span class="ss">:service_identifier</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre><figcaption class="code-caption">app/models/apple_oauth_client.rb</figcaption></figure>

<p>The callback action is called by the OAuth provider after the user is authenticated by the provider.
It starts by verifying the authenticity of the request.
If the request is authentic, the user is created and signed in.
If the request is not authentic, the user is redirected to the login page.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">AppleOauthSessionsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="n">skip_before_action</span> <span class="ss">:verify_authenticity_token</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span> <span class="ss">:callback</span> <span class="p">]</span>
  <span class="n">allow_unauthenticated_access</span>
  <span class="n">before_action</span> <span class="ss">:verify_oauth_state</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span> <span class="ss">:callback</span> <span class="p">]</span>
  <span class="n">before_action</span> <span class="ss">:verify_oauth_nonce</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span> <span class="ss">:callback</span> <span class="p">]</span>

  <span class="k">def</span> <span class="nf">callback</span>
    <span class="n">user_info</span> <span class="o">=</span> <span class="n">authenticate_with_apple</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">create_user</span><span class="p">(</span><span class="n">user_info</span><span class="p">)</span>
    <span class="k">unless</span> <span class="n">user</span><span class="p">.</span><span class="nf">persisted?</span>
      <span class="n">redirect_to</span> <span class="n">new_session_path</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Unable to sign in. Please try again."</span>
      <span class="k">return</span>
    <span class="k">end</span>
    
    <span class="n">sign_in_and_redirect_user</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
  <span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">e</span>
    <span class="n">redirect_to</span> <span class="n">new_session_path</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Unable to sign in. Please try again."</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">authenticate_with_apple</span>
    <span class="n">oauth_client</span> <span class="o">=</span> <span class="no">AppleOauthClient</span><span class="p">.</span><span class="nf">new</span>
    <span class="n">oauth_client</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span>
      <span class="ss">code: </span><span class="n">params</span><span class="p">[</span><span class="ss">:code</span><span class="p">],</span>
      <span class="ss">redirect_uri: </span><span class="n">callback_apple_oauth_sessions_url</span><span class="p">,</span>
      <span class="ss">nonce: </span><span class="n">stored_nonce</span>
    <span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">create_user</span><span class="p">(</span><span class="n">user_info</span><span class="p">)</span>
    <span class="no">OauthUserService</span><span class="p">.</span><span class="nf">find_or_create</span><span class="p">(</span>
      <span class="ss">oauth_provider: :apple</span><span class="p">,</span>
      <span class="ss">current_user: </span><span class="n">authenticated?</span> <span class="p">?</span> <span class="n">current_user</span> <span class="p">:</span> <span class="kp">nil</span><span class="p">,</span>
      <span class="ss">uid: </span><span class="n">user_info</span><span class="p">[</span><span class="ss">:uid</span><span class="p">],</span>
      <span class="ss">email: </span><span class="n">user_info</span><span class="p">[</span><span class="ss">:email</span><span class="p">]</span>
    <span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">verify_oauth_state</span>
    <span class="n">stored_state</span> <span class="o">=</span> <span class="n">cookies</span><span class="p">.</span><span class="nf">encrypted</span><span class="p">[</span><span class="ss">:apple_oauth_state</span><span class="p">]</span>
    <span class="n">cookies</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="ss">:apple_oauth_state</span><span class="p">)</span>
    <span class="k">unless</span> <span class="n">params</span><span class="p">[</span><span class="ss">:state</span><span class="p">].</span><span class="nf">present?</span> <span class="o">&amp;&amp;</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">SecurityUtils</span><span class="p">.</span><span class="nf">secure_compare</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:state</span><span class="p">],</span> <span class="n">stored_state</span><span class="p">)</span>
      <span class="n">redirect_to</span> <span class="n">new_session_path</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Invalid request. Please try again."</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">verify_oauth_nonce</span>
    <span class="k">unless</span> <span class="n">stored_nonce</span><span class="p">.</span><span class="nf">present?</span>
      <span class="n">redirect_to</span> <span class="n">new_session_path</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Invalid request. Please try again."</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">sign_in_and_redirect_user</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
    <span class="n">start_new_session_for</span> <span class="n">user</span>
    <span class="n">redirect_to</span> <span class="n">after_authentication_url</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">stored_nonce</span>
    <span class="k">return</span> <span class="vi">@stored_nonce</span> <span class="k">if</span> <span class="k">defined?</span><span class="p">(</span><span class="vi">@stored_nonce</span><span class="p">)</span>
    <span class="vi">@stored_nonce</span> <span class="o">=</span> <span class="n">cookies</span><span class="p">.</span><span class="nf">encrypted</span><span class="p">[</span><span class="ss">:apple_oauth_nonce</span><span class="p">]</span>
    <span class="n">cookies</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="ss">:apple_oauth_nonce</span><span class="p">)</span>
    <span class="vi">@stored_nonce</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre><figcaption class="code-caption">app/controllers/apple_oauth_sessions_controller.rb</figcaption></figure>

<p><code class="language-plaintext highlighter-rouge">AppleOauthClient#authenticate</code> is called by the controller action above to exchange the code received from the provider for a JWT which contains the authenticated user’s information like their email address.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">AppleOauthClient</span>
  <span class="no">TOKEN_URL</span> <span class="o">=</span> <span class="s2">"https://appleid.apple.com/auth/token"</span>
  <span class="no">KEYS_URL</span> <span class="o">=</span> <span class="s2">"https://appleid.apple.com/auth/keys"</span>

  <span class="k">def</span> <span class="nf">authenticate</span><span class="p">(</span><span class="n">code</span><span class="p">:,</span> <span class="n">redirect_uri</span><span class="p">:,</span> <span class="n">nonce</span><span class="p">:)</span>
    <span class="n">tokens</span> <span class="o">=</span> <span class="n">exchange_code_for_tokens</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">redirect_uri</span><span class="p">)</span>

    <span class="k">unless</span> <span class="n">tokens</span> <span class="o">&amp;&amp;</span> <span class="n">tokens</span><span class="p">[</span><span class="s2">"id_token"</span><span class="p">]</span>
      <span class="k">raise</span> <span class="no">AuthenticationError</span><span class="p">,</span> <span class="s2">"Failed to exchange code for tokens"</span>
    <span class="k">end</span>

    <span class="n">user_info</span> <span class="o">=</span> <span class="n">decode_id_token</span><span class="p">(</span><span class="n">tokens</span><span class="p">[</span><span class="s2">"id_token"</span><span class="p">])</span>

    <span class="c1"># Verify nonce matches to prevent replay attacks</span>
    <span class="k">unless</span> <span class="n">user_info</span><span class="p">[</span><span class="s2">"nonce"</span><span class="p">]</span> <span class="o">==</span> <span class="n">nonce</span>
      <span class="k">raise</span> <span class="no">AuthenticationError</span><span class="p">,</span> <span class="s2">"Nonce verification failed"</span>
    <span class="k">end</span>

    <span class="p">{</span>
      <span class="ss">uid: </span><span class="n">user_info</span><span class="p">[</span><span class="s2">"sub"</span><span class="p">],</span>
      <span class="ss">email: </span><span class="n">user_info</span><span class="p">[</span><span class="s2">"email"</span><span class="p">]</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">class</span> <span class="nc">AuthenticationError</span> <span class="o">&lt;</span> <span class="no">StandardError</span><span class="p">;</span> <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">client_id</span>
    <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:apple</span><span class="p">,</span> <span class="ss">:service_identifier</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">generate_client_secret</span>
    <span class="c1"># Apple requires a JWT signed with your private key</span>
    <span class="n">private_key</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">PKey</span><span class="o">::</span><span class="no">EC</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:apple</span><span class="p">,</span> <span class="ss">:private_key</span><span class="p">))</span>

    <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">kid: </span><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:apple</span><span class="p">,</span> <span class="ss">:key_id</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="n">claims</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">iss: </span><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:apple</span><span class="p">,</span> <span class="ss">:team_id</span><span class="p">),</span>
      <span class="ss">iat: </span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">to_i</span><span class="p">,</span>
      <span class="ss">exp: </span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">to_i</span> <span class="o">+</span> <span class="mi">86400</span> <span class="o">*</span> <span class="mi">180</span><span class="p">,</span> <span class="c1"># 180 days</span>
      <span class="ss">aud: </span><span class="s2">"https://appleid.apple.com"</span><span class="p">,</span>
      <span class="ss">sub: </span><span class="n">client_id</span>
    <span class="p">}</span>

    <span class="no">JWT</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">claims</span><span class="p">,</span> <span class="n">private_key</span><span class="p">,</span> <span class="s2">"ES256"</span><span class="p">,</span> <span class="n">headers</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">exchange_code_for_tokens</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">redirect_uri</span><span class="p">)</span>
    <span class="n">client_secret</span> <span class="o">=</span> <span class="n">generate_client_secret</span>

    <span class="n">response</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">post_form</span><span class="p">(</span>
      <span class="no">URI</span><span class="p">(</span><span class="no">TOKEN_URL</span><span class="p">),</span>
      <span class="n">client_id</span><span class="p">:,</span>
      <span class="n">client_secret</span><span class="p">:,</span>
      <span class="n">code</span><span class="p">:,</span>
      <span class="ss">grant_type: </span><span class="s2">"authorization_code"</span><span class="p">,</span>
      <span class="ss">redirect_uri:
    </span><span class="p">)</span>

    <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">body</span><span class="p">)</span> <span class="k">if</span> <span class="n">response</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Net</span><span class="o">::</span><span class="no">HTTPSuccess</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">decode_id_token</span><span class="p">(</span><span class="n">id_token</span><span class="p">)</span>
    <span class="n">jwks</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="no">URI</span><span class="p">(</span><span class="no">KEYS_URL</span><span class="p">)),</span> <span class="ss">symbolize_names: </span><span class="kp">true</span><span class="p">)</span>
    <span class="n">jwks_keys</span> <span class="o">=</span> <span class="n">jwks</span><span class="p">[</span><span class="ss">:keys</span><span class="p">]</span>
    <span class="no">JWT</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="n">id_token</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="kp">true</span><span class="p">,</span> <span class="p">{</span> <span class="ss">jwks: </span><span class="p">{</span> <span class="ss">keys: </span><span class="n">jwks_keys</span> <span class="p">},</span> <span class="ss">algorithm: </span><span class="s2">"RS256"</span> <span class="p">}).</span><span class="nf">first</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre><figcaption class="code-caption">app/models/apple_oauth_client.rb</figcaption></figure>

<p>We need to to add a form and a Sign in with Apple button to the sign in page.</p>

<figure class="highlight"><pre><code class="language-erb" data-lang="erb"><span class="cp">&lt;%=</span> <span class="n">form_with</span><span class="p">(</span>
  <span class="ss">url: </span><span class="n">apple_oauth_sessions_path</span><span class="p">,</span> 
  <span class="ss">method: :post</span><span class="p">,</span> 
  <span class="ss">data: </span><span class="p">{</span> 
    <span class="ss">turbo: </span><span class="kp">false</span>
  <span class="p">})</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">submit</span> <span class="s2">"Sign in with Apple"</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"btn-outline w-full"</span> <span class="cp">%&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span></code></pre><figcaption class="code-caption">app/views/shared/_sign_in_with_apple.html.erb</figcaption></figure>

<p>Setting <code class="language-plaintext highlighter-rouge">data-turbo="false"</code> is important.
If we don’t do this, Turbo will perform the form submission over JavaScript and OAuth providers will block the request as a security measure.</p>

<p><img src="/assets/images/hotwire-native-oauth-bridge-component/cors-policy-error.webp" alt="CORS Policy Error" />
<em>CORS Policy Error when using Turbo</em></p>

<p>Sign in with Apple is now working on the web.</p>

<h2 id="hotwire-native-implementation">Hotwire Native implementation</h2>

<video autoplay="" controls="" playsinline="" style="display: block; margin: 0 auto;" loading="lazy"><source src="/assets/videos/hotwire-native-oauth-bridge-component/ios.webm" type="video/webm" />Your browser does not support the video tag.
</video>

<p class="video-caption">Native OAuth Sign In</p>

<p>By default, Hotwire Native apps use a web view.
This is a problem for OAuth since web views are disallowed for security reasons.
Instead, we need to use a system browser.</p>

<p>We will create a brige component that is reponsbile for starting the OAuth process in a system browser.
The bridge component will be given two values: a start path and a token authentication path.
The bridge component will intercept the “Sign in with …” form submission.
Instead of directly redirecting the user to the OAuth provider, the bridge component will send a message to the native bridge component.
The native bridge component will start a system browser and within it navigate to the start path provided by the bridge component.
The start page will have a form on it that automatically submits when the page loads.
This form will redirect the user to the OAuth provider within the system browser.</p>

<p>After the user fills in their credentials on the OAuth provider’s site, the OAuth provider will call the callback endpoint.
We cannot sign in the user during this callback since the callback runs inside the system browser and we want the user to be authenticated within the web view.
Instead, the callback will redirect the user to the native app using a custom scheme URL along with a signed token representing the user.
When the app is opened by the custom scheme URL, we’ll have the app close the system browser and call the token authentication endpoint provided to the bridge component from within the web view.
The user will now be authenticated within the web view and can use the app as an authenticated user.</p>

<p>The following sequence diagram describes this process.</p>

<pre><code class="language-mermaid">sequenceDiagram
    actor User
    participant WKWebView
    participant SFSafariViewController
    participant Rails
    participant Apple

    User-&gt;&gt;WKWebView: click "Sign in with ..."
    WKWebView-&gt;&gt;SFSafariViewController: launch
    SFSafariViewController-&gt;&gt;Rails: GET /apple_oauth_sessions/new
    Rails--&gt;&gt;SFSafariViewController: 200 OK
    SFSafariViewController-&gt;&gt;Rails: POST /apple_oauth_sessions
    Rails--&gt;&gt;SFSafariViewController: 302 https://appleid.apple.com/auth/authorize...
    SFSafariViewController-&gt;&gt;Apple: GET https://appleid.apple.com/auth/authorize...
    Apple--&gt;&gt;SFSafariViewController: 200 OK
    User-&gt;&gt;SFSafariViewController: provide Apple credentials
    SFSafariViewController-&gt;&gt;Apple: POST https://appleid.apple.com/appleauth/auth/oauth/authorize
    Apple--&gt;&gt;SFSafariViewController: 200 OK
    SFSafariViewController-&gt;&gt;Rails: POST /apple_oauth_sessions/callback
    Note over SFSafariViewController,Rails: Generate signed token
    Rails--&gt;&gt;SFSafariViewController: 302 rssreader://auth-callback
    SFSafariViewController-&gt;&gt;WKWebView: GET rssreader://auth-callback
    WKWebView-&gt;&gt;Rails: GET /apple_oauth_sessions/authenticate_by_token
    Note over WKWebView,Rails: Authenticate by signed token
    Rails--&gt;&gt;WKWebView: 302 /entries
    WKWebView-&gt;&gt;Rails: GET /entries
    Rails--&gt;&gt;WKWebView: 200 OK
</code></pre>

<p>We’ll start by creating the Sign in with OAuth bridge component.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="k">import</span> <span class="p">{</span> <span class="nx">BridgeComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/hotwire-native-bridge</span><span class="dl">"</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="nc">extends</span> <span class="nx">BridgeComponent</span> <span class="p">{</span>
  <span class="kd">static</span> <span class="nx">component</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">sign-in-with-oauth</span><span class="dl">"</span>
  <span class="kd">static</span> <span class="nx">values</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">startPath</span><span class="p">:</span> <span class="nb">String</span>
  <span class="p">}</span>

  <span class="nf">interceptSubmit</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">event</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">()</span>

    <span class="kd">const</span> <span class="nx">startPath</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">startPathValue</span>
    <span class="k">this</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="dl">"</span><span class="s2">click</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="nx">startPath</span> <span class="p">})</span>
  <span class="p">}</span>
<span class="p">}</span></code></pre><figcaption class="code-caption">app/javascript/controllers/bridge/sign_in_with_oauth_controller.js</figcaption></figure>

<p>Then we’ll update the Sign in with OAuth form that we created earlier to intercept the form submission.
Since this controller is a bridge component, it will only take affect on native apps and the form submission will be a normal form submission on the web.</p>

<figure class="highlight"><pre><code class="language-erb" data-lang="erb"><span class="cp">&lt;%=</span> <span class="n">form_with</span><span class="p">(</span>
  <span class="ss">url: </span><span class="n">apple_oauth_sessions_path</span><span class="p">,</span> 
  <span class="ss">method: :post</span><span class="p">,</span>
  <span class="ss">data: </span><span class="p">{</span> 
<span class="hll-green">    <span class="ss">controller: </span><span class="s2">"bridge--sign-in-with-oauth"</span><span class="p">,</span> 
</span><span class="hll-green">    <span class="ss">action: </span><span class="s2">"submit-&gt;bridge--sign-in-with-oauth#interceptSubmit"</span><span class="p">,</span> 
</span><span class="hll-green">    <span class="ss">bridge__sign_in_with_oauth_start_path_value: </span><span class="n">new_apple_oauth_sessions_path</span>
</span>  <span class="p">})</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">submit</span> <span class="s2">"Sign in with Apple"</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"btn-outline w-full"</span> <span class="cp">%&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span></code></pre><figcaption class="code-caption">app/views/shared/_sign_in_with_apple.html.erb</figcaption></figure>

<p>The bridge component needs to know the path for the start page so we’ll create that next.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">resource</span> <span class="ss">:apple_oauth_sessions</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[ new ... ]</span></code></pre><figcaption class="code-caption">config/routes.rb</figcaption></figure>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">AppleOauthSessionsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
<span class="hll-green">  <span class="k">def</span> <span class="nf">new</span>
</span><span class="hll-green">    <span class="n">render</span> <span class="ss">:new</span><span class="p">,</span> <span class="ss">layout: </span><span class="kp">false</span>
</span><span class="hll-green">  <span class="k">end</span>
</span><span class="k">end</span></code></pre><figcaption class="code-caption">app/controllers/apple_oauth_sessions_controller.rb</figcaption></figure>

<figure class="highlight"><pre><code class="language-erb" data-lang="erb"><span class="cp">&lt;%=</span> <span class="n">form_with</span><span class="p">(</span>
  <span class="ss">url: </span><span class="n">apple_oauth_sessions_path</span><span class="p">,</span>
  <span class="ss">method: :post</span><span class="p">,</span>
  <span class="ss">data: </span><span class="p">{</span>
    <span class="ss">controller: </span><span class="s2">"form-submit"</span><span class="p">,</span>
    <span class="ss">turbo: </span><span class="kp">false</span>
  <span class="p">})</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">hidden_field</span> <span class="ss">:platform</span><span class="p">,</span> <span class="ss">value: </span><span class="n">params</span><span class="p">[</span><span class="ss">:platform</span><span class="p">]</span> <span class="cp">%&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span></code></pre><figcaption class="code-caption">app/views/apple_oauth_sessions/new.html.erb</figcaption></figure>

<p>We’ll add a Stimulus controller to the form to submit it automatically when the page loads.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/stimulus</span><span class="dl">"</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="nc">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
  <span class="nf">connect</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nf">requestSubmit</span><span class="p">()</span>
  <span class="p">}</span>
<span class="p">}</span></code></pre><figcaption class="code-caption">app/javascript/controllers/form_submit_controller.js</figcaption></figure>

<p>This native bridge component will receive the “click” event sent by the Stimulus controller, launch the system browser and navigate to the start path within it.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">class</span> <span class="kt">SignInWithOauthComponent</span><span class="p">:</span> <span class="kt">BridgeComponent</span> <span class="p">{</span>
    <span class="o">...</span>

    <span class="kd">private</span> <span class="k">var</span> <span class="nv">safariViewController</span><span class="p">:</span> <span class="kt">SFSafariViewController</span><span class="p">?</span>

    <span class="k">override</span> <span class="kd">func</span> <span class="nf">onReceive</span><span class="p">(</span><span class="nv">message</span><span class="p">:</span> <span class="kt">Message</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">event</span> <span class="o">=</span> <span class="kt">Event</span><span class="p">(</span><span class="nv">rawValue</span><span class="p">:</span> <span class="n">message</span><span class="o">.</span><span class="n">event</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>

        <span class="k">switch</span> <span class="n">event</span> <span class="p">{</span>
        <span class="k">case</span> <span class="o">.</span><span class="nv">click</span><span class="p">:</span>
            <span class="nf">onClick</span><span class="p">(</span><span class="nv">message</span><span class="p">:</span> <span class="n">message</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">onClick</span><span class="p">(</span><span class="nv">message</span><span class="p">:</span> <span class="kt">Message</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">data</span><span class="p">:</span> <span class="kt">MessageData</span> <span class="o">=</span> <span class="n">message</span><span class="o">.</span><span class="nf">data</span><span class="p">(),</span>
              <span class="k">let</span> <span class="nv">startUrl</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">string</span><span class="p">:</span> <span class="s">"</span><span class="se">\(</span><span class="n">baseUrl</span><span class="se">)\(</span><span class="n">data</span><span class="o">.</span><span class="n">startPath</span><span class="se">)</span><span class="s">"</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
        
        <span class="nf">launchSafariViewController</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="n">startUrl</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">launchSafariViewController</span><span class="p">(</span><span class="n">with</span> <span class="nv">url</span><span class="p">:</span> <span class="kt">URL</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">safariVC</span> <span class="o">=</span> <span class="kt">SFSafariViewController</span><span class="p">(</span><span class="nv">url</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span>
        <span class="n">safariVC</span><span class="o">.</span><span class="n">modalPresentationStyle</span> <span class="o">=</span> <span class="o">.</span><span class="n">pageSheet</span>
        <span class="k">self</span><span class="o">.</span><span class="n">safariViewController</span> <span class="o">=</span> <span class="n">safariVC</span>

        <span class="n">viewController</span><span class="o">.</span><span class="nf">present</span><span class="p">(</span><span class="n">safariVC</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">private</span> <span class="kd">extension</span> <span class="kt">SignInWithOauthComponent</span> <span class="p">{</span>
    <span class="kd">enum</span> <span class="kt">Event</span><span class="p">:</span> <span class="kt">String</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">click</span>
    <span class="p">}</span>

    <span class="kd">struct</span> <span class="kt">MessageData</span><span class="p">:</span> <span class="kt">Decodable</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">startPath</span><span class="p">:</span> <span class="kt">String</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre><figcaption class="code-caption">SignInWithOauthComponent.swift</figcaption></figure>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">@main</span>
<span class="kd">class</span> <span class="kt">AppDelegate</span><span class="p">:</span> <span class="kt">UIResponder</span><span class="p">,</span> <span class="kt">UIApplicationDelegate</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">application</span><span class="p">(</span>
      <span class="o">...</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="p">{</span>
        <span class="o">...</span>
        
<span class="hll-green">        <span class="kt">Hotwire</span><span class="o">.</span><span class="nf">registerBridgeComponents</span><span class="p">([</span>
</span><span class="hll-green">            <span class="kt">SignInWithOauthComponent</span><span class="o">.</span><span class="k">self</span><span class="p">,</span>
</span><span class="hll-green">        <span class="p">])</span>
</span>        
        <span class="o">...</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre><figcaption class="code-caption">AppDelegate.swift</figcaption></figure>

<p>If we test the OAuth process in our native app, we should see the system browser launch and navigate to the OAuth provider’s authorization page.
We can fill in our credentials and we’ll be redirected back to site but we’ll only be authenticated within the system browser.
We need to be authenticated within the web view.</p>

<p>We need to update the callback action to redirect to the app’s custom scheme URL if the callback is coming from the native app.
A signed token representing the user is included in the redirect URL so the native app can authenticate the user.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">AppleOauthSessionsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="o">...</span>

  <span class="k">def</span> <span class="nf">callback</span>
    <span class="n">user_info</span> <span class="o">=</span> <span class="n">authenticate_with_apple</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">create_user</span><span class="p">(</span><span class="n">user_info</span><span class="p">)</span>
    <span class="k">unless</span> <span class="n">user</span><span class="p">.</span><span class="nf">persisted?</span>
      <span class="n">redirect_to</span> <span class="n">new_session_path</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Unable to sign in. Please try again."</span>
      <span class="k">return</span>
    <span class="k">end</span>

<span class="hll-green">    <span class="n">platform</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:state</span><span class="p">].</span><span class="nf">split</span><span class="p">(</span><span class="s2">":"</span><span class="p">).</span><span class="nf">last</span>
</span><span class="hll-green">    <span class="k">if</span> <span class="n">platform</span> <span class="o">==</span> <span class="s2">"native"</span>
</span><span class="hll-green">      <span class="n">token</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">signed_id</span><span class="p">(</span><span class="ss">purpose: :native_auth</span><span class="p">,</span> <span class="ss">expires_in: </span><span class="mi">5</span><span class="p">.</span><span class="nf">minutes</span><span class="p">)</span>
</span><span class="hll-green">      <span class="n">redirect_to</span> <span class="s2">"rssreader://auth-callback?token=</span><span class="si">#{</span><span class="n">token</span><span class="si">}</span><span class="s2">&amp;platform=</span><span class="si">#{</span><span class="n">platform</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="ss">allow_other_host: </span><span class="kp">true</span>
</span><span class="hll-green">    <span class="k">else</span>
</span><span class="hll-green">      <span class="n">sign_in_and_redirect_user</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
</span><span class="hll-green">    <span class="k">end</span>
</span>  <span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">e</span>
    <span class="n">redirect_to</span> <span class="n">new_session_path</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Unable to sign in. Please try again."</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre><figcaption class="code-caption">app/controllers/apple_oauth_sessions_controller.rb</figcaption></figure>

<p>The native app needs to know what the token authentication path is so we include that in the bridge component.</p>

<figure class="highlight"><pre><code class="language-erb" data-lang="erb"><span class="cp">&lt;%=</span> <span class="n">form_with</span><span class="p">(</span>
  <span class="ss">url: </span><span class="n">apple_oauth_sessions_path</span><span class="p">,</span> 
  <span class="ss">method: :post</span><span class="p">,</span>
  <span class="ss">data: </span><span class="p">{</span> 
    <span class="ss">controller: </span><span class="s2">"bridge--sign-in-with-oauth"</span><span class="p">,</span> 
    <span class="ss">action: </span><span class="s2">"submit-&gt;bridge--sign-in-with-oauth#interceptSubmit"</span><span class="p">,</span> 
    <span class="ss">bridge__sign_in_with_oauth_start_path_value: </span><span class="n">new_apple_oauth_sessions_path</span><span class="p">(</span><span class="ss">platform: </span><span class="s2">"native"</span><span class="p">),</span>
<span class="hll-green">    <span class="ss">bridge__sign_in_with_oauth_token_auth_path_value: </span><span class="n">authenticate_by_token_apple_oauth_sessions_path</span>
</span>  <span class="p">})</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">submit</span> <span class="s2">"Sign in with Apple"</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"btn-outline w-full"</span> <span class="cp">%&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span></code></pre><figcaption class="code-caption">app/views/apple_oauth_sessions/new.html.erb</figcaption></figure>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="k">import</span> <span class="p">{</span> <span class="nx">BridgeComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/hotwire-native-bridge</span><span class="dl">"</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="nc">extends</span> <span class="nx">BridgeComponent</span> <span class="p">{</span>
  <span class="kd">static</span> <span class="nx">component</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">sign-in-with-oauth</span><span class="dl">"</span>
  <span class="kd">static</span> <span class="nx">values</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">startPath</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="hll-green">    <span class="na">tokenAuthPath</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
</span>  <span class="p">}</span>

  <span class="nf">interceptSubmit</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">event</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">()</span>

    <span class="kd">const</span> <span class="nx">startPath</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">startPathValue</span>
<span class="hll-green">    <span class="kd">const</span> <span class="nx">tokenAuthPath</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">tokenAuthPathValue</span>
</span>    <span class="k">this</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="dl">"</span><span class="s2">click</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="nx">startPath</span><span class="p">,</span> <span class="nx">tokenAuthPath</span> <span class="p">})</span>
  <span class="p">}</span>
<span class="p">}</span></code></pre><figcaption class="code-caption">app/javascript/controllers/bridge/sign_in_with_oauth_controller.js</figcaption></figure>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">class</span> <span class="kt">SignInWithOauthComponent</span><span class="p">:</span> <span class="kt">BridgeComponent</span> <span class="p">{</span>
    <span class="o">...</span>

<span class="hll-green">    <span class="kd">private</span> <span class="k">var</span> <span class="nv">tokenAuthPath</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span>
</span>
    <span class="k">override</span> <span class="kd">func</span> <span class="nf">onReceive</span><span class="p">(</span><span class="nv">message</span><span class="p">:</span> <span class="kt">Message</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">event</span> <span class="o">=</span> <span class="kt">Event</span><span class="p">(</span><span class="nv">rawValue</span><span class="p">:</span> <span class="n">message</span><span class="o">.</span><span class="n">event</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>

        <span class="k">switch</span> <span class="n">event</span> <span class="p">{</span>
        <span class="k">case</span> <span class="o">.</span><span class="nv">click</span><span class="p">:</span>
            <span class="nf">onClick</span><span class="p">(</span><span class="nv">message</span><span class="p">:</span> <span class="n">message</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">onClick</span><span class="p">(</span><span class="nv">message</span><span class="p">:</span> <span class="kt">Message</span><span class="p">)</span> <span class="p">{</span>
        <span class="o">...</span>

<span class="hll-green">        <span class="k">self</span><span class="o">.</span><span class="n">tokenAuthPath</span> <span class="o">=</span> <span class="n">data</span><span class="o">.</span><span class="n">tokenAuthPath</span>
</span>
        <span class="nf">launchSafariViewController</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="n">startUrl</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="o">...</span>
<span class="p">}</span>

<span class="kd">private</span> <span class="kd">extension</span> <span class="kt">SignInWithOauthComponent</span> <span class="p">{</span>
    <span class="kd">enum</span> <span class="kt">Event</span><span class="p">:</span> <span class="kt">String</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">click</span>
    <span class="p">}</span>

    <span class="kd">struct</span> <span class="kt">MessageData</span><span class="p">:</span> <span class="kt">Decodable</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">startPath</span><span class="p">:</span> <span class="kt">String</span>
<span class="hll-green">        <span class="k">let</span> <span class="nv">tokenAuthPath</span><span class="p">:</span> <span class="kt">String</span>
</span>    <span class="p">}</span>
<span class="p">}</span></code></pre><figcaption class="code-caption">SignInWithOauthComponent.swift</figcaption></figure>

<p>The token authentication action will take the token, lookup the corresponding user and sign in the user.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">resource</span> <span class="ss">:apple_oauth_sessions</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[ ... ]</span> <span class="k">do</span>
<span class="hll-green">  <span class="n">collection</span> <span class="k">do</span>
</span><span class="hll-green">    <span class="n">get</span> <span class="ss">:authenticate_by_token</span>
</span><span class="hll-green">  <span class="k">end</span>
</span><span class="k">end</span></code></pre><figcaption class="code-caption">config/routes.rb</figcaption></figure>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">AppleOauthSessionsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="o">...</span>

<span class="hll-green">  <span class="k">def</span> <span class="nf">authenticate_by_token</span>
</span><span class="hll-green">    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_signed</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:token</span><span class="p">],</span> <span class="ss">purpose: :native_auth</span><span class="p">)</span>
</span><span class="hll-green">    <span class="k">if</span> <span class="n">user</span>
</span><span class="hll-green">      <span class="n">sign_in_and_redirect_user</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
</span><span class="hll-green">    <span class="k">else</span>
</span><span class="hll-green">      <span class="n">redirect_to</span> <span class="n">welcome_path</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Unable to sign in. Please try again."</span>
</span><span class="hll-green">    <span class="k">end</span>
</span><span class="hll-green">  <span class="k">end</span>
</span><span class="k">end</span></code></pre><figcaption class="code-caption">app/controllers/apple_oauth_sessions_controller.rb</figcaption></figure>

<p>We need the native component to close the system browser and navigate to the token authentication path when the callback is received.
We’ll achieve this by adding an observer when the system browser is launched.
When the callback endpoint redirects back to the app via the custom scheme URL, we’ll trigger the observer’s callback function which will close the system browser and navigate to the token authentication path in the web view.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">class</span> <span class="kt">SignInWithOauthComponent</span><span class="p">:</span> <span class="kt">BridgeComponent</span> <span class="p">{</span>
    <span class="o">...</span>

    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">launchSafariViewController</span><span class="p">(</span><span class="n">with</span> <span class="nv">url</span><span class="p">:</span> <span class="kt">URL</span><span class="p">)</span> <span class="p">{</span>
        <span class="o">...</span>

        <span class="kt">NotificationCenter</span><span class="o">.</span><span class="k">default</span><span class="o">.</span><span class="nf">addObserver</span><span class="p">(</span>
            <span class="k">self</span><span class="p">,</span>
            <span class="nv">selector</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">handleAuthCompletion</span><span class="p">),</span>
            <span class="nv">name</span><span class="p">:</span> <span class="o">.</span><span class="n">signInWithOauthCompleted</span><span class="p">,</span>
            <span class="nv">object</span><span class="p">:</span> <span class="kc">nil</span>
        <span class="p">)</span>
   <span class="p">}</span>

    <span class="kd">@objc</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">handleAuthCompletion</span><span class="p">(</span><span class="n">_</span> <span class="nv">notification</span><span class="p">:</span> <span class="kt">Notification</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">NotificationCenter</span><span class="o">.</span><span class="k">default</span><span class="o">.</span><span class="nf">removeObserver</span><span class="p">(</span><span class="k">self</span><span class="p">,</span> <span class="nv">name</span><span class="p">:</span> <span class="o">.</span><span class="n">signInWithOauthCompleted</span><span class="p">,</span> <span class="nv">object</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span>

        <span class="k">let</span> <span class="nv">token</span> <span class="o">=</span> <span class="n">notification</span><span class="o">.</span><span class="n">userInfo</span><span class="p">?[</span><span class="s">"token"</span><span class="p">]</span> <span class="k">as?</span> <span class="kt">String</span>

        <span class="n">safariViewController</span><span class="p">?</span><span class="o">.</span><span class="nf">dismiss</span><span class="p">(</span><span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="k">in</span>
            <span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="n">safariViewController</span> <span class="o">=</span> <span class="kc">nil</span>
            <span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="nf">authenticateWithToken</span><span class="p">(</span><span class="n">token</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">authenticateWithToken</span><span class="p">(</span><span class="n">_</span> <span class="nv">token</span><span class="p">:</span> <span class="kt">String</span><span class="p">?)</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">webViewController</span> <span class="o">=</span> <span class="n">delegate</span><span class="p">?</span><span class="o">.</span><span class="n">destination</span> <span class="k">as?</span> <span class="kt">HotwireWebViewController</span><span class="p">,</span>
              <span class="k">let</span> <span class="nv">webView</span> <span class="o">=</span> <span class="n">webViewController</span><span class="o">.</span><span class="n">visitableView</span><span class="o">.</span><span class="n">webView</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>

        <span class="k">if</span> <span class="k">let</span> <span class="nv">token</span> <span class="o">=</span> <span class="n">token</span><span class="p">,</span> <span class="k">let</span> <span class="nv">tokenAuthPath</span> <span class="o">=</span> <span class="n">tokenAuthPath</span> <span class="p">{</span>
            <span class="k">guard</span> <span class="k">let</span> <span class="nv">tokenLoginUrl</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">string</span><span class="p">:</span> <span class="s">"</span><span class="se">\(</span><span class="n">baseUrl</span><span class="se">)\(</span><span class="n">tokenAuthPath</span><span class="se">)</span><span class="s">"</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>

            <span class="k">var</span> <span class="nv">components</span> <span class="o">=</span> <span class="kt">URLComponents</span><span class="p">(</span><span class="nv">url</span><span class="p">:</span> <span class="n">tokenLoginUrl</span><span class="p">,</span> <span class="nv">resolvingAgainstBaseURL</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span>
            <span class="n">components</span><span class="p">?</span><span class="o">.</span><span class="n">queryItems</span> <span class="o">=</span> <span class="p">[</span><span class="kt">URLQueryItem</span><span class="p">(</span><span class="nv">name</span><span class="p">:</span> <span class="s">"token"</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="n">token</span><span class="p">)]</span>

            <span class="k">if</span> <span class="k">let</span> <span class="nv">url</span> <span class="o">=</span> <span class="n">components</span><span class="p">?</span><span class="o">.</span><span class="n">url</span> <span class="p">{</span>
                <span class="n">webView</span><span class="o">.</span><span class="nf">load</span><span class="p">(</span><span class="kt">URLRequest</span><span class="p">(</span><span class="nv">url</span><span class="p">:</span> <span class="n">url</span><span class="p">))</span>
            <span class="p">}</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">webView</span><span class="o">.</span><span class="nf">reload</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="o">...</span>

<span class="kd">extension</span> <span class="kt">Notification</span><span class="o">.</span><span class="kt">Name</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">let</span> <span class="nv">signInWithOauthCompleted</span> <span class="o">=</span> <span class="kt">Notification</span><span class="o">.</span><span class="kt">Name</span><span class="p">(</span><span class="s">"signInWithOauthCompleted"</span><span class="p">)</span>
<span class="p">}</span></code></pre><figcaption class="code-caption">SignInWithOauthComponent.swift</figcaption></figure>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">extension</span> <span class="kt">SceneController</span><span class="p">:</span> <span class="kt">UIWindowSceneDelegate</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">scene</span><span class="p">(</span>
       <span class="o">...</span>
    <span class="p">)</span> <span class="p">{</span>
        <span class="o">...</span>

        <span class="k">if</span> <span class="k">let</span> <span class="nv">urlContext</span> <span class="o">=</span> <span class="n">connectionOptions</span><span class="o">.</span><span class="n">urlContexts</span><span class="o">.</span><span class="n">first</span> <span class="p">{</span>
            <span class="nf">handleIncomingURL</span><span class="p">(</span><span class="n">urlContext</span><span class="o">.</span><span class="n">url</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">scene</span><span class="p">(</span><span class="n">_</span> <span class="nv">scene</span><span class="p">:</span> <span class="kt">UIScene</span><span class="p">,</span> <span class="n">openURLContexts</span> <span class="kt">URLContexts</span><span class="p">:</span> <span class="kt">Set</span><span class="o">&lt;</span><span class="kt">UIOpenURLContext</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">url</span> <span class="o">=</span> <span class="kt">URLContexts</span><span class="o">.</span><span class="n">first</span><span class="p">?</span><span class="o">.</span><span class="n">url</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
        <span class="nf">handleIncomingURL</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">handleIncomingURL</span><span class="p">(</span><span class="n">_</span> <span class="nv">url</span><span class="p">:</span> <span class="kt">URL</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">host</span> <span class="o">=</span> <span class="n">url</span><span class="o">.</span><span class="n">host</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>

        <span class="k">switch</span> <span class="n">host</span> <span class="p">{</span>
        <span class="k">case</span> <span class="s">"auth-callback"</span><span class="p">:</span>
            <span class="k">let</span> <span class="nv">components</span> <span class="o">=</span> <span class="kt">URLComponents</span><span class="p">(</span><span class="nv">url</span><span class="p">:</span> <span class="n">url</span><span class="p">,</span> <span class="nv">resolvingAgainstBaseURL</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span>
            <span class="k">let</span> <span class="nv">token</span> <span class="o">=</span> <span class="n">components</span><span class="p">?</span><span class="o">.</span><span class="n">queryItems</span><span class="p">?</span><span class="o">.</span><span class="nf">first</span><span class="p">(</span><span class="nv">where</span><span class="p">:</span> <span class="p">{</span> <span class="nv">$0</span><span class="o">.</span><span class="n">name</span> <span class="o">==</span> <span class="s">"token"</span> <span class="p">})?</span><span class="o">.</span><span class="n">value</span>

            <span class="kt">NotificationCenter</span><span class="o">.</span><span class="k">default</span><span class="o">.</span><span class="nf">post</span><span class="p">(</span>
                <span class="nv">name</span><span class="p">:</span> <span class="o">.</span><span class="n">signInWithOauthCompleted</span><span class="p">,</span>
                <span class="nv">object</span><span class="p">:</span> <span class="kc">nil</span><span class="p">,</span>
                <span class="nv">userInfo</span><span class="p">:</span> <span class="n">token</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">?</span> <span class="p">[</span><span class="s">"token"</span><span class="p">:</span> <span class="n">token</span><span class="o">!</span><span class="p">]</span> <span class="p">:</span> <span class="kc">nil</span>
            <span class="p">)</span>
        <span class="k">default</span><span class="p">:</span>
           <span class="o">...</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre><figcaption class="code-caption">SceneController.swift</figcaption></figure>

<p>The OAuth sign in process is now working in the Hotwire Native app.
The code can be found on Github <a href="https://github.com/kcdragon/hotwire-native-talk-rss-reader">here</a>.</p>

<h2 id="whats-next">What’s Next</h2>

<p>This post only covers OAuth for Apple.
A follow up post will describe how to implement the same approach described here in an Hotwire Native Android app with Google Sign In.</p>

<p>Reach out to me on my <a href="/socials">socials</a> if you have any questions or feedback.
I’m especially curious about alternative approaches implementing OAuth in Hotwire Native apps.</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:googleoauthblogpost">
      <p><a href="https://developers.googleblog.com/upcoming-security-changes-to-googles-oauth-20-authorization-endpoint-in-embedded-webviews/" target="_blank">Upcoming security changes to Google’s OAuth 2.0 authorization endpoint in embedded webviews</a> <a href="#fnref:googleoauthblogpost" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:googleoauthblogpost:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:usesystembrowser">
      <p><a href="https://www.oauth.com/oauth2-servers/oauth-native-apps/use-system-browser/">Use a System Browser</a> <a href="#fnref:usesystembrowser" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:omniauth-apple-issues">
      <p><a href="https://github.com/nhosoya/omniauth-apple/issues/117">omniauth-apple Github Issue: Setting up with JavaScript/IOS application needs to be documented</a> <a href="#fnref:omniauth-apple-issues" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Mike Dalton</name></author><category term="ruby" /><category term="rails" /><category term="hotwire" /><category term="hotwirenative" /><category term="oauth" /><summary type="html"><![CDATA[Learn how to implement OAuth in a Hotwire Native application using bridge components]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mikedalton.co/assets/images/hotwire-native-oauth-bridge-component/apple-oauth-system-browser.png" /><media:content medium="image" url="https://mikedalton.co/assets/images/hotwire-native-oauth-bridge-component/apple-oauth-system-browser.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Native Apple OAuth with Hotwire Native</title><link href="https://mikedalton.co/2025/09/15/native-oauth-with-hotwire-native/" rel="alternate" type="text/html" title="Native Apple OAuth with Hotwire Native" /><published>2025-09-15T00:00:00+00:00</published><updated>2025-09-15T00:00:00+00:00</updated><id>https://mikedalton.co/2025/09/15/native-oauth-with-hotwire-native</id><content type="html" xml:base="https://mikedalton.co/2025/09/15/native-oauth-with-hotwire-native/"><![CDATA[<p><strong>Update</strong>: I’m now using the approach described in my post <a href="/2026/01/26/hotwire-native-oauth-bridge-component/">Hotwire Native OAuth Bridge Component</a></p>

<hr />

<p>I recently struggled a lot with implementing Apple OAuth for my Hotwire Native app <a href="https://calendarvision.app">Calendar Vision</a> so I figured I would share out what I did.
I would love to hear if there are better ways to do some of this.</p>

<p>The first screen the user sees when opening the iOS app is a native screen that allows the user to sign in with Apple.
The callback that Apple redirects to after authentication is the same Devise Ominauth endpoint that the web app uses.
This allowed me to use the same cookie-based authentication regardless of log in method.</p>

<h2 id="ios-code">iOS Code</h2>

<p>The native screen is implemented with a custom view controller.
This view controller is visited by configuring the getting started URL to point at a view controller in the path configuration.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="ss">patterns: </span><span class="p">[</span>
    <span class="n">hotwire_native_get_started_path</span>
  <span class="p">],</span>
  <span class="ss">properties: </span><span class="p">{</span>
    <span class="ss">uri: </span><span class="s2">"hotwire://fragment/welcome"</span><span class="p">,</span>
    <span class="ss">title: </span><span class="s2">"Welcome"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">SceneController</code> handles this path with the <code class="language-plaintext highlighter-rouge">WelcomeController</code></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">SceneController</span><span class="p">:</span> <span class="kt">NavigatorDelegate</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">handle</span><span class="p">(</span><span class="nv">proposal</span><span class="p">:</span> <span class="kt">VisitProposal</span><span class="p">,</span> <span class="n">from</span> <span class="nv">navigator</span><span class="p">:</span> <span class="kt">Navigator</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">ProposalResult</span> <span class="p">{</span>
        <span class="k">switch</span> <span class="n">proposal</span><span class="o">.</span><span class="n">viewController</span> <span class="p">{</span>
        <span class="k">case</span> <span class="kt">WelcomeController</span><span class="o">.</span><span class="nv">pathConfigurationIdentifier</span><span class="p">:</span>
            <span class="n">window</span><span class="p">?</span><span class="o">.</span><span class="n">rootViewController</span> <span class="o">=</span> <span class="n">navigator</span><span class="o">.</span><span class="n">rootViewController</span>
            <span class="k">return</span> <span class="o">.</span><span class="nf">acceptCustom</span><span class="p">(</span><span class="n">welcomeController</span><span class="o">!</span><span class="p">)</span>
        <span class="k">default</span><span class="p">:</span>
            <span class="k">return</span> <span class="o">.</span><span class="n">accept</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">WelcomeController</code> is defined like this.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">SceneController</span><span class="p">:</span> <span class="kt">UIWindowSceneDelegate</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">scene</span><span class="p">(</span>
        <span class="n">_</span> <span class="nv">scene</span><span class="p">:</span> <span class="kt">UIScene</span><span class="p">,</span>
        <span class="n">willConnectTo</span> <span class="nv">session</span><span class="p">:</span> <span class="kt">UISceneSession</span><span class="p">,</span>
        <span class="n">options</span> <span class="nv">connectionOptions</span><span class="p">:</span> <span class="kt">UIScene</span><span class="o">.</span><span class="kt">ConnectionOptions</span>
    <span class="p">)</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">windowScene</span> <span class="o">=</span> <span class="n">scene</span> <span class="k">as?</span> <span class="kt">UIWindowScene</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
        
        <span class="kt">UNUserNotificationCenter</span><span class="o">.</span><span class="nf">current</span><span class="p">()</span><span class="o">.</span><span class="n">delegate</span> <span class="o">=</span> <span class="n">notificationRouter</span>
        
        <span class="n">welcomeController</span> <span class="o">=</span> <span class="kt">WelcomeController</span><span class="p">(</span><span class="nv">onSignInWithAppleSuccess</span><span class="p">:{</span> <span class="p">[</span><span class="k">self</span><span class="p">]</span> <span class="k">in</span>
            <span class="n">window</span><span class="p">?</span><span class="o">.</span><span class="n">rootViewController</span> <span class="o">=</span> <span class="n">tabBarController</span>
            <span class="n">tabBarController</span><span class="o">.</span><span class="nf">load</span><span class="p">(</span><span class="kt">HotwireTab</span><span class="o">.</span><span class="n">all</span><span class="p">)</span>
        <span class="p">},</span> <span class="nv">onError</span><span class="p">:</span> <span class="p">{</span> <span class="p">[</span><span class="k">self</span><span class="p">]</span> <span class="n">message</span> <span class="k">in</span>
            <span class="nf">showErrorAlert</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"Unable to Authenticate"</span><span class="p">,</span> <span class="nv">message</span><span class="p">:</span> <span class="n">message</span><span class="p">)</span>
        <span class="p">})</span>
        
        <span class="kt">Task</span> <span class="p">{</span> <span class="k">await</span> <span class="nf">checkAuth</span><span class="p">(</span><span class="nv">windowScene</span><span class="p">:</span> <span class="n">windowScene</span><span class="p">)</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">WelcomeController</code> is given an <code class="language-plaintext highlighter-rouge">onSignInWithAppleSuccess</code> function that will load the <code class="language-plaintext highlighter-rouge">tabBarController</code> when the user successfully signs in with Apple.</p>

<p>The <code class="language-plaintext highlighter-rouge">WelcomeController</code> delegates to the <code class="language-plaintext highlighter-rouge">WelcomeView</code></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">WelcomeController</span><span class="p">:</span> <span class="kt">UIHostingController</span><span class="o">&lt;</span><span class="kt">WelcomeView</span><span class="o">&gt;</span><span class="p">,</span> <span class="kt">PathConfigurationIdentifiable</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">var</span> <span class="nv">pathConfigurationIdentifier</span><span class="p">:</span> <span class="kt">String</span> <span class="p">{</span> <span class="s">"welcome"</span> <span class="p">}</span>
    
    <span class="kd">convenience</span> <span class="nf">init</span><span class="p">(</span><span class="nv">onSignInWithAppleSuccess</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">,</span> <span class="nv">onPasswordSignUpClicked</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">,</span> <span class="nv">onContinueWithoutSignUpClicked</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">,</span> <span class="nv">onError</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">String</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">view</span> <span class="o">=</span> <span class="kt">WelcomeView</span><span class="p">(</span><span class="nv">onSignInWithAppleSuccess</span><span class="p">:</span> <span class="n">onSignInWithAppleSuccess</span><span class="p">,</span> <span class="nv">onPasswordSignUpClicked</span><span class="p">:</span> <span class="n">onPasswordSignUpClicked</span><span class="p">,</span> <span class="nv">onContinueWithoutSignUpClicked</span><span class="p">:</span> <span class="n">onContinueWithoutSignUpClicked</span><span class="p">,</span> <span class="nv">onError</span><span class="p">:</span> <span class="n">onError</span><span class="p">)</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">rootView</span><span class="p">:</span> <span class="n">view</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">WelcomeView</code> uses Swift UI and the built in <code class="language-plaintext highlighter-rouge">SignInWithAppleButton</code> component.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">WelcomeView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">onSignInWithAppleSuccess</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
    <span class="k">var</span> <span class="nv">onError</span><span class="p">:</span> <span class="p">(</span><span class="kt">String</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">SignInWithAppleButton</span><span class="p">(</span><span class="o">.</span><span class="n">signUp</span><span class="p">)</span> <span class="p">{</span> <span class="n">request</span> <span class="k">in</span>
            <span class="n">request</span><span class="o">.</span><span class="n">requestedScopes</span> <span class="o">=</span> <span class="p">[</span><span class="o">.</span><span class="n">fullName</span><span class="p">,</span> <span class="o">.</span><span class="n">email</span><span class="p">]</span>
        <span class="p">}</span> <span class="nv">onCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">result</span> <span class="k">in</span>
            <span class="k">switch</span> <span class="n">result</span> <span class="p">{</span>
            <span class="k">case</span> <span class="o">.</span><span class="nf">success</span><span class="p">(</span><span class="k">let</span> <span class="nv">authorization</span><span class="p">):</span>
                <span class="nf">handleSuccessfulLogin</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="n">authorization</span><span class="p">)</span>
            <span class="k">case</span> <span class="o">.</span><span class="nf">failure</span><span class="p">(</span><span class="k">let</span> <span class="nv">error</span><span class="p">):</span>
                <span class="nf">handleLoginError</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="n">error</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">height</span><span class="p">:</span> <span class="mi">50</span><span class="p">)</span>
        <span class="o">.</span><span class="nf">cornerRadius</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>When authentication with Apple is successful, we’re giving authorization data and we need to call the Rails app to log in.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">handleSuccessfulLogin</span><span class="p">(</span><span class="n">with</span> <span class="nv">authorization</span><span class="p">:</span> <span class="kt">ASAuthorization</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="k">let</span> <span class="nv">userCredential</span> <span class="o">=</span> <span class="n">authorization</span><span class="o">.</span><span class="n">credential</span> <span class="k">as?</span> <span class="kt">ASAuthorizationAppleIDCredential</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">identityToken</span> <span class="o">=</span> <span class="n">userCredential</span><span class="o">.</span><span class="n">identityToken</span><span class="p">,</span>
              <span class="k">let</span> <span class="nv">identityTokenString</span> <span class="o">=</span> <span class="kt">String</span><span class="p">(</span><span class="nv">data</span><span class="p">:</span> <span class="n">identityToken</span><span class="p">,</span> <span class="nv">encoding</span><span class="p">:</span> <span class="o">.</span><span class="n">utf8</span><span class="p">),</span>
              <span class="k">let</span> <span class="nv">authorizationCode</span> <span class="o">=</span> <span class="n">userCredential</span><span class="o">.</span><span class="n">authorizationCode</span><span class="p">,</span>
              <span class="k">let</span> <span class="nv">authorizationCodeString</span> <span class="o">=</span> <span class="kt">String</span><span class="p">(</span><span class="nv">data</span><span class="p">:</span> <span class="n">authorizationCode</span><span class="p">,</span> <span class="nv">encoding</span><span class="p">:</span> <span class="o">.</span><span class="n">utf8</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nf">print</span><span class="p">(</span><span class="s">"Failed to get identity token or authorization code"</span><span class="p">)</span>
            <span class="k">return</span>
        <span class="p">}</span>
        
        <span class="kt">Task</span> <span class="p">{</span>
            <span class="k">await</span> <span class="nf">sendToOmniauthCallback</span><span class="p">(</span>
                <span class="nv">identityToken</span><span class="p">:</span> <span class="n">identityTokenString</span><span class="p">,</span>
                <span class="nv">authorizationCode</span><span class="p">:</span> <span class="n">authorizationCodeString</span><span class="p">,</span>
                <span class="nv">userIdentifier</span><span class="p">:</span> <span class="n">userCredential</span><span class="o">.</span><span class="n">user</span><span class="p">,</span>
                <span class="nv">email</span><span class="p">:</span> <span class="n">userCredential</span><span class="o">.</span><span class="n">email</span><span class="p">,</span>
                <span class="nv">fullName</span><span class="p">:</span> <span class="n">userCredential</span><span class="o">.</span><span class="n">fullName</span>
            <span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">private</span> <span class="kd">func</span> <span class="nf">sendToOmniauthCallback</span><span class="p">(</span>
    <span class="nv">identityToken</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span>
    <span class="nv">authorizationCode</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span>
    <span class="nv">userIdentifier</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span>
    <span class="nv">email</span><span class="p">:</span> <span class="kt">String</span><span class="p">?,</span>
    <span class="nv">fullName</span><span class="p">:</span> <span class="kt">PersonNameComponents</span><span class="p">?</span>
<span class="p">)</span> <span class="k">async</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">components</span> <span class="o">=</span> <span class="kt">URLComponents</span><span class="p">()</span>
    <span class="n">components</span><span class="o">.</span><span class="n">scheme</span> <span class="o">=</span> <span class="n">baseUrl</span><span class="o">.</span><span class="n">scheme</span>
    <span class="n">components</span><span class="o">.</span><span class="n">host</span> <span class="o">=</span> <span class="n">baseUrl</span><span class="o">.</span><span class="n">host</span>
    <span class="n">components</span><span class="o">.</span><span class="n">port</span> <span class="o">=</span> <span class="n">baseUrl</span><span class="o">.</span><span class="n">port</span>
    <span class="n">components</span><span class="o">.</span><span class="n">path</span> <span class="o">=</span> <span class="s">"/users/auth/apple/callback"</span>
    
    <span class="k">let</span> <span class="nv">queryItems</span> <span class="o">=</span> <span class="p">[</span>
        <span class="kt">URLQueryItem</span><span class="p">(</span><span class="nv">name</span><span class="p">:</span> <span class="s">"id_token"</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="n">identityToken</span><span class="p">),</span>
        <span class="kt">URLQueryItem</span><span class="p">(</span><span class="nv">name</span><span class="p">:</span> <span class="s">"code"</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="n">authorizationCode</span><span class="p">),</span>
    <span class="p">]</span>
    
    <span class="n">components</span><span class="o">.</span><span class="n">queryItems</span> <span class="o">=</span> <span class="n">queryItems</span>
    
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">callbackURL</span> <span class="o">=</span> <span class="n">components</span><span class="o">.</span><span class="n">url</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"Failed to build callback URL"</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="p">}</span>
    
    <span class="nf">print</span><span class="p">(</span><span class="s">"Sending callback to: </span><span class="se">\(</span><span class="n">callbackURL</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    
    <span class="k">var</span> <span class="nv">request</span> <span class="o">=</span> <span class="kt">URLRequest</span><span class="p">(</span><span class="nv">url</span><span class="p">:</span> <span class="n">callbackURL</span><span class="p">)</span>
    <span class="n">request</span><span class="o">.</span><span class="n">httpMethod</span> <span class="o">=</span> <span class="s">"POST"</span>
    
    <span class="k">do</span> <span class="p">{</span>
        <span class="k">let</span> <span class="p">(</span><span class="nv">_</span><span class="p">,</span> <span class="nv">response</span><span class="p">)</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="kt">URLSession</span><span class="o">.</span><span class="n">shared</span><span class="o">.</span><span class="nf">data</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">request</span><span class="p">)</span>
        
        <span class="k">if</span> <span class="k">let</span> <span class="nv">httpResponse</span> <span class="o">=</span> <span class="n">response</span> <span class="k">as?</span> <span class="kt">HTTPURLResponse</span> <span class="p">{</span>
            <span class="nf">print</span><span class="p">(</span><span class="s">"Rails callback response status: </span><span class="se">\(</span><span class="n">httpResponse</span><span class="o">.</span><span class="n">statusCode</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
            
            <span class="k">if</span> <span class="n">httpResponse</span><span class="o">.</span><span class="n">statusCode</span> <span class="o">==</span> <span class="mi">200</span> <span class="o">||</span> <span class="n">httpResponse</span><span class="o">.</span><span class="n">statusCode</span> <span class="o">==</span> <span class="mi">302</span> <span class="p">{</span>
                <span class="c1">// Cookies from the callback must be copied to the cookie store so the rest of the app can be authenticated</span>
                <span class="k">await</span> <span class="nf">copyCookiesToCookieStore</span><span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">httpResponse</span><span class="p">)</span>
                
                <span class="c1">// Refresh tabs to reflect authenticated state</span>
                <span class="k">await</span> <span class="kt">MainActor</span><span class="o">.</span><span class="n">run</span> <span class="p">{</span>
                    <span class="nf">onSignInWithAppleSuccess</span><span class="p">()</span>
                <span class="p">}</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="nf">print</span><span class="p">(</span><span class="s">"Rails callback failed with status: </span><span class="se">\(</span><span class="n">httpResponse</span><span class="o">.</span><span class="n">statusCode</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"Failed to send callback to Rails: </span><span class="se">\(</span><span class="n">error</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// I'd like to refactor this to a utility class but I got a EXC_BREAKPOINT when I tried that</span>
<span class="kd">private</span> <span class="kd">func</span> <span class="nf">copyCookiesToCookieStore</span><span class="p">(</span><span class="n">from</span> <span class="nv">response</span><span class="p">:</span> <span class="kt">HTTPURLResponse</span><span class="p">)</span> <span class="k">async</span> <span class="p">{</span>
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">url</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">url</span><span class="p">,</span>
          <span class="k">let</span> <span class="nv">headerFields</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">allHeaderFields</span> <span class="k">as?</span> <span class="p">[</span><span class="kt">String</span><span class="p">:</span> <span class="kt">String</span><span class="p">]</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span>
    <span class="p">}</span>
    
    <span class="k">let</span> <span class="nv">cookies</span> <span class="o">=</span> <span class="kt">HTTPCookie</span><span class="o">.</span><span class="nf">cookies</span><span class="p">(</span><span class="nv">withResponseHeaderFields</span><span class="p">:</span> <span class="n">headerFields</span><span class="p">,</span> <span class="nv">for</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">cookieStore</span> <span class="o">=</span> <span class="kt">WKWebsiteDataStore</span><span class="o">.</span><span class="nf">default</span><span class="p">()</span><span class="o">.</span><span class="n">httpCookieStore</span>
    
    <span class="nf">print</span><span class="p">(</span><span class="s">"Copying </span><span class="se">\(</span><span class="n">cookies</span><span class="o">.</span><span class="n">count</span><span class="se">)</span><span class="s"> cookies to web view store"</span><span class="p">)</span>
    
    <span class="k">for</span> <span class="n">cookie</span> <span class="k">in</span> <span class="n">cookies</span> <span class="p">{</span>
        <span class="k">await</span> <span class="n">cookieStore</span><span class="o">.</span><span class="nf">setCookie</span><span class="p">(</span><span class="n">cookie</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>There’s a lot of code here but it’s really just two things happening.</p>

<ol>
  <li>Call the Devise Ominauth callback endpoint with the <code class="language-plaintext highlighter-rouge">authorization</code> data</li>
  <li>Copy the cookies from that request callback request into the cookie store so the rest of the Hotwire Native app can make use of the authenticated session.</li>
</ol>

<h2 id="rails-code">Rails Code</h2>

<p>The Rails code for this is pretty similar to the code needed to get Sign in with Apple working on the web.
I’m using Devise, Omniauth and the Omniauth Apple gems.</p>

<p>These gems need to be configured with all the different Apple identifiers. This was probably the trickiest part of the Rails code.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">config</span><span class="p">.</span><span class="nf">omniauth</span><span class="p">(</span>
    <span class="ss">:apple</span><span class="p">,</span>
    <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:ios</span><span class="p">,</span> <span class="ss">:service_identifier</span><span class="p">),</span>
    <span class="s2">""</span><span class="p">,</span>
    <span class="p">{</span>
      <span class="ss">scope: </span><span class="s2">"email name"</span><span class="p">,</span>
      <span class="ss">team_id: </span><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:ios</span><span class="p">,</span> <span class="ss">:team_id</span><span class="p">),</span>
      <span class="ss">key_id: </span><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:ios</span><span class="p">,</span> <span class="ss">:key_id</span><span class="p">),</span>
      <span class="ss">pem: </span><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:ios</span><span class="p">,</span> <span class="ss">:apns_key</span><span class="p">),</span>
      <span class="ss">provider_ignores_state: </span><span class="kp">true</span><span class="p">,</span>
      <span class="ss">authorized_client_ids: </span><span class="p">[</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:ios</span><span class="p">,</span> <span class="ss">:service_identifier</span><span class="p">),</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:ios</span><span class="p">,</span> <span class="ss">:bundle_identifier</span><span class="p">)</span> <span class="p">],</span>
      <span class="ss">nonce: :local</span>
    <span class="p">},</span>
  <span class="p">)</span>
</code></pre></div></div>

<p>The Omniauth callback looks like most Omniauth callback endpoints</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Users::OmniauthCallbacksController</span> <span class="o">&lt;</span> <span class="no">Devise</span><span class="o">::</span><span class="no">OmniauthCallbacksController</span>
  <span class="n">skip_before_action</span> <span class="ss">:verify_authenticity_token</span>

  <span class="k">def</span> <span class="nf">apple</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">from_omniauth</span><span class="p">(</span><span class="n">auth</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">present?</span>
      <span class="n">flash</span><span class="p">[</span><span class="ss">:notice</span><span class="p">]</span> <span class="o">=</span> <span class="n">t</span> <span class="s2">"devise.omniauth_callbacks.success"</span><span class="p">,</span> <span class="ss">kind: </span><span class="s2">"Apple"</span>
      <span class="n">sign_in_and_redirect</span> <span class="n">user</span><span class="p">,</span> <span class="ss">event: :authentication</span>
    <span class="k">else</span>
      <span class="n">flash</span><span class="p">[</span><span class="ss">:alert</span><span class="p">]</span> <span class="o">=</span>
        <span class="n">t</span> <span class="s2">"devise.omniauth_callbacks.failure"</span><span class="p">,</span> <span class="ss">kind: </span><span class="s2">"Apple"</span><span class="p">,</span> <span class="ss">reason: </span><span class="s2">"</span><span class="si">#{</span><span class="n">auth</span><span class="p">.</span><span class="nf">info</span><span class="p">.</span><span class="nf">email</span><span class="si">}</span><span class="s2"> is not authorized."</span>
      <span class="n">redirect_to</span> <span class="n">root_path</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I did need to monkey patch the Omniauth Apple gem.
<a href="https://github.com/nhosoya/omniauth-apple/issues/117">This issue discussion</a> details why this is necessary and was really useful for understanding what needed to be done.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">OmniAuth</span>
  <span class="k">module</span> <span class="nn">Strategies</span>
    <span class="k">class</span> <span class="nc">Apple</span>
      <span class="kp">private</span> <span class="k">def</span> <span class="nf">verify_nonce!</span><span class="p">(</span><span class="n">id_token</span><span class="p">)</span>
        <span class="n">invalid_claim!</span> <span class="ss">:nonce</span> <span class="k">unless</span> <span class="n">id_token</span><span class="p">[</span><span class="ss">:nonce</span><span class="p">]</span> <span class="o">==</span> <span class="n">stored_nonce</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="whats-next">What’s Next</h2>

<ul>
  <li>I’d love to hear about a better approach to this so feel free to @ me or message me on my <a href="/socials">socials</a>.</li>
  <li>I implemented Native Google OAuth on the Android app and I’ll be following up with a post about that.</li>
  <li>I recently released <a href="/2025/08/27/calendar-vision-1-0/">Calendar Vision</a> so check that out if you’ve ever struggled with adding events to your calendar.</li>
</ul>]]></content><author><name>Mike Dalton</name></author><category term="ruby" /><category term="rails" /><category term="hotwire" /><category term="hotwirenative" /><category term="calendarvision" /><summary type="html"><![CDATA[Update: I’m now using the approach described in my post Hotwire Native OAuth Bridge Component]]></summary></entry><entry><title type="html">Calendar Vision 1.0</title><link href="https://mikedalton.co/2025/08/27/calendar-vision-1-0/" rel="alternate" type="text/html" title="Calendar Vision 1.0" /><published>2025-08-27T00:00:00+00:00</published><updated>2025-08-27T00:00:00+00:00</updated><id>https://mikedalton.co/2025/08/27/calendar-vision-1-0</id><content type="html" xml:base="https://mikedalton.co/2025/08/27/calendar-vision-1-0/"><![CDATA[<p>Earlier this week I released my new Hotwire Native app, <a href="https://calendarvision.app/">Calendar Vision</a>, to the <a href="https://apps.apple.com/us/app/calendar-vision/id6746702391">Apple App Store</a> and <a href="https://play.google.com/store/apps/details?id=com.rowhomelabs.calendarvision">Google Play Store</a>.
Calendar Vision extracts and creates events from photo, screenshots, emails, and more.</p>

<h2 id="upload-your-images">Upload your images</h2>

<p>You can upload a photo you took or take a screenshot and add the event in it to your calendar.</p>

<video autoplay="" controls="" playsinline="" style="display: block; margin: 0 auto;" loading="lazy"><source src="/assets/calendar-vision/ios-demo-upload-photo-and-add-to-calendar.mp4" type="video/mp4" />Your browser does not support the video tag.
</video>

<h2 id="send-in-your-emails">Send in your emails</h2>

<p>You can send an email to the Calendar Vision email address and the events in the email will be extracted from the email.</p>

<video autoplay="" controls="" playsinline="" style="display: block; margin: 0 auto;" loading="lazy"><source src="/assets/calendar-vision/ios-demo-send-email-and-add-to-calendar.mp4" type="video/mp4" />Your browser does not support the video tag.
</video>

<h2 id="scrape-web-pages-coming-soon">Scrape web pages (Coming Soon)</h2>

<p>Soon you’ll be able to share a web page with Calendar Vision and we’ll scrape the site and extract events from the site.</p>

<h2 id="whats-next">What’s next</h2>

<p>It’s been a lot of fun building this app and I’ve learned a lot about Hotwire Native along the way.
I’m planning on writing a couple more posts to share what I’ve learned.
Stay tuned!</p>]]></content><author><name>Mike Dalton</name></author><category term="ruby" /><category term="rails" /><category term="hotwire" /><category term="hotwirenative" /><category term="calendarvision" /><summary type="html"><![CDATA[Earlier this week I released my new Hotwire Native app, Calendar Vision, to the Apple App Store and Google Play Store. Calendar Vision extracts and creates events from photo, screenshots, emails, and more.]]></summary></entry><entry><title type="html">Trix Dark Mode Icons in Jumpstart Pro</title><link href="https://mikedalton.co/2024/10/31/trix-dark-mode-icons/" rel="alternate" type="text/html" title="Trix Dark Mode Icons in Jumpstart Pro" /><published>2024-10-31T00:00:00+00:00</published><updated>2024-10-31T00:00:00+00:00</updated><id>https://mikedalton.co/2024/10/31/trix-dark-mode-icons</id><content type="html" xml:base="https://mikedalton.co/2024/10/31/trix-dark-mode-icons/"><![CDATA[<p>This snippet changes the <a href="https://trix-editor.org/">Trix editor</a> icons to white when dark mode is enabled in Jumpstart Pro.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.dark</span> <span class="nc">.trix-button--icon</span> <span class="p">{</span>
  <span class="nl">-webkit-filter</span><span class="p">:</span> <span class="nf">invert</span><span class="p">(</span><span class="m">100%</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="/assets/trix-dark-mode-css.png" /></p>]]></content><author><name>Mike Dalton</name></author><category term="ruby" /><category term="rails" /><category term="trix" /><category term="jumpstartpro" /><summary type="html"><![CDATA[This snippet changes the Trix editor icons to white when dark mode is enabled in Jumpstart Pro.]]></summary></entry><entry><title type="html">Pagy Frontend Helper for DaisyUI</title><link href="https://mikedalton.co/2023/05/31/daisy-ui-pagy-helper/" rel="alternate" type="text/html" title="Pagy Frontend Helper for DaisyUI" /><published>2023-05-31T00:00:00+00:00</published><updated>2023-05-31T00:00:00+00:00</updated><id>https://mikedalton.co/2023/05/31/daisy-ui-pagy-helper</id><content type="html" xml:base="https://mikedalton.co/2023/05/31/daisy-ui-pagy-helper/"><![CDATA[<p>I’ve been using <a href="https://daisyui.com/">DaisyUI</a> for a new Rails app.
I’m using <a href="https://github.com/ddnexus/pagy">Pagy</a> for pagination but wanted to use DaisyUI components to match the rest of my app. 
Here’s the helper method I’m using. 
It’s used the same way as the built in Pagy helpers. It was easy to adapt <code class="language-plaintext highlighter-rouge">pagy_nav</code> for DaisyUI.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">DaisyUiPagyHelper</span>
  <span class="k">def</span> <span class="nf">pagy_daisy_ui_nav</span><span class="p">(</span><span class="n">pagy</span><span class="p">,</span> <span class="ss">pagy_id: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">link_extra: </span><span class="s1">''</span><span class="p">,</span> <span class="o">**</span><span class="n">vars</span><span class="p">)</span>
    <span class="n">p_id</span>   <span class="o">=</span> <span class="sx">%( id="</span><span class="si">#{</span><span class="n">pagy_id</span><span class="si">}</span><span class="sx">")</span> <span class="k">if</span> <span class="n">pagy_id</span>
    <span class="n">p_prev</span> <span class="o">=</span> <span class="n">pagy</span><span class="p">.</span><span class="nf">prev</span>
    <span class="n">p_next</span> <span class="o">=</span> <span class="n">pagy</span><span class="p">.</span><span class="nf">next</span>

    <span class="n">html</span> <span class="o">=</span> <span class="o">+</span><span class="sx">%(&lt;nav</span><span class="si">#{</span><span class="n">p_id</span><span class="si">}</span><span class="sx"> class="btn-group"&gt;)</span>
    <span class="n">html</span> <span class="o">&lt;&lt;</span> <span class="k">if</span> <span class="n">p_prev</span>
              <span class="sx">%(&lt;a class="btn" href="</span><span class="si">#{</span><span class="n">pagy_url_for</span><span class="p">(</span><span class="n">pagy</span><span class="p">,</span> <span class="n">p_prev</span><span class="p">,</span> <span class="ss">html_escaped: </span><span class="kp">true</span><span class="p">)</span><span class="si">}</span><span class="sx">" aria-label="previous" </span><span class="si">#{</span><span class="n">link_extra</span><span class="si">}</span><span class="sx">&gt;</span><span class="si">#{</span><span class="n">pagy_t</span><span class="p">(</span><span class="s1">'pagy.nav.prev'</span><span class="p">)</span><span class="si">}</span><span class="sx">&lt;/a&gt; )</span>
            <span class="k">else</span>
              <span class="sx">%(&lt;a class="btn btn-disabled" </span><span class="si">#{</span><span class="n">link_extra</span><span class="si">}</span><span class="sx">&gt;</span><span class="si">#{</span><span class="n">pagy_t</span><span class="p">(</span><span class="s1">'pagy.nav.prev'</span><span class="p">)</span><span class="si">}</span><span class="sx">&lt;/a&gt; )</span>
            <span class="k">end</span>
    <span class="n">pagy</span><span class="p">.</span><span class="nf">series</span><span class="p">(</span><span class="o">**</span><span class="n">vars</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span> <span class="c1"># series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]</span>
      <span class="n">html</span> <span class="o">&lt;&lt;</span> <span class="k">case</span> <span class="n">item</span>
              <span class="k">when</span> <span class="no">Integer</span> <span class="k">then</span> <span class="sx">%(&lt;a class="btn" href="</span><span class="si">#{</span><span class="n">pagy_url_for</span><span class="p">(</span><span class="n">pagy</span><span class="p">,</span> <span class="n">item</span><span class="p">,</span> <span class="ss">html_escaped: </span><span class="kp">true</span><span class="p">)</span><span class="si">}</span><span class="sx">" </span><span class="si">#{</span><span class="n">link_extra</span><span class="si">}</span><span class="sx">&gt;</span><span class="si">#{</span><span class="n">item</span><span class="si">}</span><span class="sx">&lt;/a&gt; )</span>
              <span class="k">when</span> <span class="no">String</span>  <span class="k">then</span> <span class="sx">%(&lt;a class="btn btn-active" </span><span class="si">#{</span><span class="n">link_extra</span><span class="si">}</span><span class="sx">&gt;</span><span class="si">#{</span><span class="n">pagy</span><span class="p">.</span><span class="nf">label_for</span><span class="p">(</span><span class="n">item</span><span class="p">)</span><span class="si">}</span><span class="sx">&lt;/a&gt; )</span>
              <span class="k">when</span> <span class="ss">:gap</span>    <span class="k">then</span> <span class="sx">%(&lt;a class="btn btn-disabled" </span><span class="si">#{</span><span class="n">link_extra</span><span class="si">}</span><span class="sx">&gt;</span><span class="si">#{</span><span class="n">pagy_t</span><span class="p">(</span><span class="s1">'pagy.nav.gap'</span><span class="p">)</span><span class="si">}</span><span class="sx">&lt;/a&gt; )</span>
              <span class="k">else</span> <span class="k">raise</span> <span class="no">InternalError</span><span class="p">,</span> <span class="s2">"expected item types in series to be Integer, String or :gap; got </span><span class="si">#{</span><span class="n">item</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
              <span class="k">end</span>
    <span class="k">end</span>
    <span class="n">html</span> <span class="o">&lt;&lt;</span> <span class="k">if</span> <span class="n">p_next</span>
              <span class="sx">%(&lt;a class="btn" href="</span><span class="si">#{</span><span class="n">pagy_url_for</span><span class="p">(</span><span class="n">pagy</span><span class="p">,</span> <span class="n">p_next</span><span class="p">,</span> <span class="ss">html_escaped: </span><span class="kp">true</span><span class="p">)</span><span class="si">}</span><span class="sx">" aria-label="next" </span><span class="si">#{</span><span class="n">link_extra</span><span class="si">}</span><span class="sx">&gt;</span><span class="si">#{</span><span class="n">pagy_t</span><span class="p">(</span><span class="s1">'pagy.nav.next'</span><span class="p">)</span><span class="si">}</span><span class="sx">&lt;/a&gt;)</span>
            <span class="k">else</span>
              <span class="sx">%(&lt;a class="btn btn-disabled" </span><span class="si">#{</span><span class="n">link_extra</span><span class="si">}</span><span class="sx">&gt;</span><span class="si">#{</span><span class="n">pagy_t</span><span class="p">(</span><span class="s1">'pagy.nav.next'</span><span class="p">)</span><span class="si">}</span><span class="sx">&lt;/a&gt;)</span>
            <span class="k">end</span>
    <span class="n">html</span> <span class="o">&lt;&lt;</span> <span class="sx">%(&lt;/nav&gt;)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Here’s what it looks like.</p>

<p><img src="/assets/daisy-ui-pagy.png" /></p>]]></content><author><name>Mike Dalton</name></author><category term="ruby" /><category term="rails" /><category term="pagy" /><category term="daisyui" /><summary type="html"><![CDATA[I’ve been using DaisyUI for a new Rails app. I’m using Pagy for pagination but wanted to use DaisyUI components to match the rest of my app. Here’s the helper method I’m using. It’s used the same way as the built in Pagy helpers. It was easy to adapt pagy_nav for DaisyUI.]]></summary></entry><entry><title type="html">Cheese Steaks</title><link href="https://mikedalton.co/2022/10/26/cheese-steaks/" rel="alternate" type="text/html" title="Cheese Steaks" /><published>2022-10-26T00:00:00+00:00</published><updated>2022-10-26T00:00:00+00:00</updated><id>https://mikedalton.co/2022/10/26/cheese-steaks</id><content type="html" xml:base="https://mikedalton.co/2022/10/26/cheese-steaks/"><![CDATA[<p>When I meet people at conferences and tell them I’m from Philadelphia, I’m often asked for my cheese steak recommendation.
I’ll keep this post updated with my official rankings of cheese steaks in Philadelphia.</p>

<h1 id="mikes-bbq"><a href="https://www.mikesbbqphilly.com/">Mike’s BBQ</a></h1>

<p>Ranking: 1</p>

<p>My Order: Brisket Cheesesteak, Cooper Sharp Wiz, Raw Onions with a side of homemade potato chips</p>

<p><img src="/assets/mikes-bbq.jpg" width="200" title="Mike's BBQ Brisket Cheesesteak" /></p>

<p>This is my favorite cheesesteak in the city right now.
It’s not exactly traditional since it uses brisket instead of rib eye but their brisket is great.
They also have traditional BBQ food there so you can get some of that as well.
The biggest downside is their hours. They are only open from noon to 5pm and they will close early if they sell out.
They don’t have much seating and the surrounding area is pretty residential so I would recommend ordering pick up and being flexible with where you eat it.</p>]]></content><author><name>Mike Dalton</name></author><category term="food" /><category term="philly" /><summary type="html"><![CDATA[When I meet people at conferences and tell them I’m from Philadelphia, I’m often asked for my cheese steak recommendation. I’ll keep this post updated with my official rankings of cheese steaks in Philadelphia.]]></summary></entry><entry><title type="html">Rails Conf 2017 Recap</title><link href="https://mikedalton.co/2017/07/25/rails-conf-2017-recap/" rel="alternate" type="text/html" title="Rails Conf 2017 Recap" /><published>2017-07-25T00:00:00+00:00</published><updated>2017-07-25T00:00:00+00:00</updated><id>https://mikedalton.co/2017/07/25/rails-conf-2017-recap</id><content type="html" xml:base="https://mikedalton.co/2017/07/25/rails-conf-2017-recap/"><![CDATA[<p><img src="https://pbs.twimg.com/media/C-dKTS7XkAM57gA.jpg" style="width: 500px;" /></p>

<p>I attended RailsConf this past April in Phoenix. It was held the week of April 25th. This is a post about my favorite talks of the conference.</p>

<h1 id="opening-keynote---dhh">Opening Keynote - <a href="https://twitter.com/dhh">@dhh</a></h1>

<p>DHH did the opening keynote and did not disappoint. He talked about how developers choose technologies for their projects. He felt that developers are more likely to base their decisions on the communities they already belong to rather than because they’ve evaluated it in comparison with others and it is the best fit for their project. He then compared this idea to the belief systems in religions.</p>

<p>If there’s one talk you watch from the conference, I think this should be it. Regardless of whether you agree with him, it was both entertaining and informative.</p>

<p><a href="https://www.youtube.com/watch?v=Cx6aGMC6MjU">Talk Video</a></p>

<h1 id="goldilocks-and-the-three-code-reviews---vaidehijoshi">Goldilocks and the Three Code Reviews - <a href="https://twitter.com/vaidehijoshi">@vaidehijoshi</a></h1>

<p>Vaidehi’s talk about code reviews really resonated with me.
I’ve been doing code reviews as a formal process for the last 4 years and I’ve yet to find a good way to summarize what the purpose of code reviews are… until I saw Vaidehi’s talk.</p>

<p>The part that stuck with me was her statement that code reviews should be about “defect detection, not correction.”
Code reviews can become contentious and focusing on defects removes a lot of subjectivity from code reviews.</p>

<p><img src="/assets/rails-conf-2017-code-review-talk.jpg" style="width: 200px;" /></p>

<p>If you’ve struggled with code reviews yourself on on your team, I recommend watching her talk. You can also read more about the research she did for this talk at <a href="http://www.bettercode.reviews">bettercode.reviews</a>.</p>

<!-- Code Complete quote around 9:15 -->
<!-- Read relevant sections of Code Complete -->
<!-- "defect detection, not correction" around 12:30. if you're correcting code as you go along, you aren't checking for defects -->
<!-- http://bettercode.reviews -->

<!-- [Talk Video](https://www.youtube.com/watch?v=-6EzycFNwzY) -->

<!-- # A clear-eyed look at Distributed Teams - [@glv](https://twitter.com/glv) and [@mariagutierrez](https://twitter.com/mariagutierrez) -->

<!-- [Talk Video](https://www.youtube.com/watch?v=h8MLXbdOyNs) -->

<!-- # Distributed Tracing: From Theory to Practice - [@practice_cactus](https://twitter.com/practice_cactus) -->

<!-- [Talk Video](https://www.youtube.com/watch?v=-7i02Faw_KU) -->]]></content><author><name>Mike Dalton</name></author><category term="ruby" /><category term="rails" /><category term="railsconf" /><category term="conference" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Adding Functionality to your Ruby Code</title><link href="https://mikedalton.co/2016/09/16/adding-functionality-to-your-ruby-code/" rel="alternate" type="text/html" title="Adding Functionality to your Ruby Code" /><published>2016-09-16T00:00:00+00:00</published><updated>2016-09-16T00:00:00+00:00</updated><id>https://mikedalton.co/2016/09/16/adding-functionality-to-your-ruby-code</id><content type="html" xml:base="https://mikedalton.co/2016/09/16/adding-functionality-to-your-ruby-code/"><![CDATA[<p>This is a blog post I wrote while working at <a href="http://www.weblinc.com">WebLinc</a>.
It was posted on their blog until they shutdown their blog.</p>

<h2 id="adding-functionality-to-your-ruby-code">Adding Functionality to your Ruby Code</h2>

<p>Many programmers start out with languages that favor procedural programming. Because of this, it is tempting to use procedural constructs (while and .each loops) when writing an algorithm in Ruby, even though there may be clearer ways to express the algorithm. This blog post will describe several problems, and compare a procedural implementation and a functional implementation.</p>

<h3 id="overview">Overview</h3>

<p>Let’s start with some definitions and a simple example. The example we will use below is the common, but simple, problem of summing a list of numbers. We begin with a list of numbers and the result is the number that is equal to adding all elements of the list together.</p>

<h3 id="procedural-programming">Procedural programming</h3>

<blockquote>
  <p>Procedural programming uses a list of instructions to tell the computer what to do step-by-step
— http://study.com/academy/lesson/object-oriented-programming-vs-procedural-programming.html</p>
</blockquote>

<p>A procedural implementation to sum a list of numbers would look something like this.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sum</span> <span class="o">=</span> <span class="mi">0</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">number</span><span class="o">|</span>
  <span class="n">sum</span> <span class="o">+=</span> <span class="n">number</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We start out by initializing a local variable to keep track of the sum. We then iterate through the list of numbers and add each number to the sum.</p>

<h3 id="functional-programming">Functional programming</h3>

<blockquote>
  <p>Functional programming is a programming paradigm … that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data
— https://en.wikipedia.org/wiki/Functional_programming</p>
</blockquote>

<p>A functional implementation for the list summation problem would be to “reduce” the list by adding each number to an accumulator.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sum</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">].</span><span class="nf">reduce</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">sum</span><span class="p">,</span> <span class="n">number</span><span class="o">|</span>
  <span class="n">sum</span> <span class="o">+</span> <span class="n">number</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The difference between the two implementations is that functional programming does not require the sum variable to be altered as the algorithm progresses.</p>

<p>This is a very simple example, so it might not be clear yet what the advantages are of a functional approach. Let’s look at a more complicated example to see how functional and procedural programming compare in a practical application.</p>

<h3 id="free-item-discount-example">Free Item Discount Example</h3>

<h4 id="background">Background</h4>

<p>Discounts are essential on ecommerce websites, and WebLinc’s platform supports many different kinds. One of the more popular discounts is the free item discount. The specific free item discount we will discuss is a “Buy 2 of X and/or Y, Get 1 Z”. The discount can be stated more generically as “Buy N from a list of products and Get 1 of the free product”.</p>

<h4 id="implementation-overview">Implementation Overview</h4>

<p>A discount is applied to a cart which is a collection of items. We will represent an item in the cart with this item class.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Item</span> <span class="o">=</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:product_id</span><span class="p">,</span> <span class="ss">:quantity</span><span class="p">)</span>
</code></pre></div></div>

<p>A real cart item has many more fields (price, product name), but for this example we will only focus on the product ID and the quantity. The product ID will be used to represent the X, Y and Z in “Buy 2 of X and/or Y Get 1 Z”. The quantity is necessary because multiples of the same product can contribute to the discount.</p>

<p>Each implementation of the free item discount will conform to the following abstract class.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">FreeItemDiscount</span>
 
  <span class="c1"># @param free_item_product_id</span>
  <span class="c1">#   Product ID for the Free Item</span>
  <span class="c1">#</span>
  <span class="c1"># @param product_ids</span>
  <span class="c1">#   The Product IDs that qualify for the discount</span>
  <span class="c1">#</span>
  <span class="c1"># @param quantity</span>
  <span class="c1">#   The quantity of product_ids that must be in the list of items</span>
  <span class="c1">#     in order to qualify for the discount</span>
  <span class="c1">#</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">free_item_product_id</span><span class="p">,</span> <span class="n">product_ids</span><span class="p">,</span> <span class="n">quantity</span><span class="p">)</span>
    <span class="vi">@free_item_product_id</span> <span class="o">=</span> <span class="n">free_item_product_id</span>
    <span class="vi">@product_ids</span> <span class="o">=</span> <span class="n">product_ids</span>
    <span class="vi">@quantity</span> <span class="o">=</span> <span class="n">quantity</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">free_item</span><span class="p">(</span><span class="ss">items: </span><span class="p">[])</span>
    <span class="k">raise</span> <span class="s2">"</span><span class="si">#{</span><span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">to_s</span><span class="si">}</span><span class="s2"> must implement #free_item"</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">qualifies?</span><span class="p">(</span><span class="ss">items: </span><span class="p">[])</span>
    <span class="k">raise</span> <span class="s2">"</span><span class="si">#{</span><span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">to_s</span><span class="si">}</span><span class="s2"> must implement #qualifies?"</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">item_qualifies?</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
    <span class="vi">@product_ids</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="nf">product_id</span><span class="p">)</span>
  <span class="k">end</span>

<span class="k">end</span>
</code></pre></div></div>

<p>In addition to the two abstract methods free_item and qualifies?, we define a helper method, item_qualifies?, which is used in each implementation. This method returns true if the item’s product ID is in the list of the discount’s eligible product IDs. We will focus our discussion on the implementation of qualifies?.</p>

<h4 id="procedural-implementation">Procedural Implementation</h4>

<p>The procedural implementation of qualifies? creates two local variables to keep track of whether or not the discount qualifies (does_qualify), and the quantity of items that qualify for the discount (quantity_of_qualifying_items)</p>

<p>While iterating through the items we check to see if the item qualifies for the discount. If the item does qualify for the discount, we increment the quantity_of_qualifying_items counter by the quantity of that item. Once we have the updated quantity_of_qualifying_items, we check to see if that quantity is greater than the quantity of items needed to qualify for the discount. If this quantity is greater, the cart qualifies for the discount and we can stop iterating.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ProceduralFreeItemDiscount</span> <span class="o">&lt;</span> <span class="no">FreeItemDiscount</span>

  <span class="k">def</span> <span class="nf">qualifies?</span><span class="p">(</span><span class="ss">items: </span><span class="p">[])</span>
    <span class="n">does_qualify</span> <span class="o">=</span> <span class="kp">false</span>
    <span class="n">quantity_of_qualifying_items</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">items</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
      <span class="k">if</span> <span class="n">item_qualifies?</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
        <span class="n">quantity_of_qualifying_items</span> <span class="o">+=</span> <span class="n">item</span><span class="p">.</span><span class="nf">quantity</span>
      <span class="k">end</span>

      <span class="k">if</span> <span class="n">quantity_of_qualifying_items</span> <span class="o">&gt;=</span> <span class="n">quantity_needed_per_discount</span>
        <span class="n">does_qualify</span> <span class="o">=</span> <span class="kp">true</span>
        <span class="k">break</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="n">does_qualify</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">quantity_needed_per_discount</span>
    <span class="vi">@quantity</span>
  <span class="k">end</span> 

<span class="k">end</span>
</code></pre></div></div>

<h4 id="functional-implementation">Functional Implementation</h4>

<p>The functional implementation of qualifies? checks to see if the discountable_quantity is greater than 0. The discountable_quantity is equal to the number_of_qualifying_items in the cart divided by the discount’s eligible quantity. We calculate the number_of_qualifying_items by adding together the quantity of all qualifying items.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">FunctionalFreeItemDiscount</span> <span class="o">&lt;</span> <span class="no">FreeItemDiscount</span>

  <span class="k">def</span> <span class="nf">qualifies?</span><span class="p">(</span><span class="ss">items: </span><span class="p">[])</span>
    <span class="n">discountable_quantity</span><span class="p">(</span><span class="n">items</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span>
  <span class="k">end</span>
  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">discountable_quantity</span><span class="p">(</span><span class="n">items</span><span class="p">)</span>
    <span class="n">number_of_qualifying_items</span><span class="p">(</span><span class="n">items</span><span class="p">)</span> <span class="o">/</span> <span class="vi">@quantity</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">number_of_qualifying_items</span><span class="p">(</span><span class="n">items</span><span class="p">)</span>
    <span class="n">qualifying_items</span><span class="p">(</span><span class="n">items</span><span class="p">).</span><span class="nf">sum</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:quantity</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">qualifying_items</span><span class="p">(</span><span class="n">items</span><span class="p">)</span>
    <span class="n">items</span><span class="p">.</span><span class="nf">select</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
      <span class="n">item_qualifies?</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

<span class="k">end</span>
</code></pre></div></div>

<h4 id="comparison-of-implementations">Comparison of Implementations</h4>

<p>The procedural and functional approaches implement the same method and return the same results, but the code is quite different. The procedural implementation modifies several state variables while iterating through the list of items, and it will break when it determines that it qualifies for the discount. The functional implementation does not use any state variables and, at each step of the algorithm, returns an object (either an integer or an array).</p>

<p>The advantage of the procedural implementation is that it takes a step-by-step approach that is intuitive for many developers. However, it is difficult to understand each step independent of each other since each step of the algorithm requires something being done before it.</p>

<p>The functional implementation steps are easier to distinguish from one another since they are separate methods. The code is self-documenting with method names. The code is easier to debug since you can use a single method call to evaluate the result at each step.</p>

<h3 id="recommendations">Recommendations</h3>

<p>It is important to remember that each approach is useful in different situations. One approach that I’ve been successful with is starting out by implementing an algorithm procedurally. This is often the most intuitive way to write the algorithm, especially if you are following test-driven development. After finishing the implementation, I try out a functional approach. I’ve found that when I am returning to look at my own code, or reading someone else’s, it is easier to read and re-use functional code.</p>

<p>See <a href="https://github.com/weblinc/functional-programming-discounts-blog-post">GitHub</a> for source code of the discounts used in this post.</p>]]></content><author><name>Mike Dalton</name></author><category term="ruby" /><category term="functional" /><category term="procedural" /><category term="ecommerce" /><category term="weblinc" /><summary type="html"><![CDATA[This is a blog post I wrote while working at WebLinc. It was posted on their blog until they shutdown their blog.]]></summary></entry><entry><title type="html">Steel City Ruby ‘14 Recap - Part 2</title><link href="https://mikedalton.co/2014/08/28/steel-city-ruby-2014-recap-part-2/" rel="alternate" type="text/html" title="Steel City Ruby ‘14 Recap - Part 2" /><published>2014-08-28T00:00:00+00:00</published><updated>2014-08-28T00:00:00+00:00</updated><id>https://mikedalton.co/2014/08/28/steel-city-ruby-2014-recap-part-2</id><content type="html" xml:base="https://mikedalton.co/2014/08/28/steel-city-ruby-2014-recap-part-2/"><![CDATA[<p>(see <a href="/2014/08/23/steel-city-ruby-2014-recap-part-1/">Steel City Ruby ‘14 - Part 1</a> for my thoughts on the first day of the conference)</p>

<h1 id="entrepreneurship-for-engineers-by-bryan-helmkamp">“Entrepreneurship for Engineers” by Bryan Helmkamp</h1>
<p>The creator of Code Climate gave a talk on starting a business as an engineer. He discussed the various kinds of businesses an engineer could start (freelance, B2B (business to business), B2C (business to consumer), social networks, etc.) and the level of difficulty with finding customers (freelancing being on the easier end and social networks being on the higher end of the spectrum). His recommendation? Build a B2B service and find 150 customers who will pay $67/month.</p>

<h1 id="the-metaphysics-of-strings-by-greg-gates">“The Metaphysics of Strings” by Greg Gates</h1>
<p>In Rails, any Strings have there HTML escaped unless they are marked as HTML safe. The #html_safe method is responsible for marking a String as HTML safe. Greg talked about how this method can produce strange results when concatenating safe and non-safe strings.</p>

<p>His first example showed that a safe string and a non-safe string are considered equal.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">001</span><span class="p">:</span><span class="mi">0</span><span class="o">&gt;</span> <span class="nb">require</span> <span class="s1">'action_view'</span>
<span class="o">=&gt;</span> <span class="kp">true</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">002</span><span class="p">:</span><span class="mi">0</span><span class="o">&gt;</span> <span class="n">safe_string</span> <span class="o">=</span> <span class="s2">"I am a string"</span><span class="p">.</span><span class="nf">html_safe</span>
<span class="o">=&gt;</span> <span class="s2">"I am a string"</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">003</span><span class="p">:</span><span class="mi">0</span><span class="o">&gt;</span> <span class="n">not_safe_string</span> <span class="o">=</span> <span class="s2">"I am a string"</span>
<span class="o">=&gt;</span> <span class="s2">"I am a string"</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">004</span><span class="p">:</span><span class="mi">0</span><span class="o">&gt;</span> <span class="n">safe_string</span> <span class="o">==</span> <span class="n">not_safe_string</span>
<span class="o">=&gt;</span> <span class="kp">true</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">005</span><span class="p">:</span><span class="mi">0</span><span class="o">&gt;</span> <span class="n">not_safe_string</span> <span class="o">==</span> <span class="n">safe_string</span>
<span class="o">=&gt;</span> <span class="kp">true</span></code></pre></figure>

<p>His second example showed that, even though strings of equal contents were equal regardless of safe-ness, the order in which they were concatenated determined if the resulting string was considered safe.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">001</span><span class="p">:</span><span class="mi">0</span><span class="o">&gt;</span> <span class="nb">require</span> <span class="s1">'action_view'</span>
<span class="o">=&gt;</span> <span class="kp">true</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">002</span><span class="p">:</span><span class="mi">0</span><span class="o">&gt;</span> <span class="n">safe_string</span> <span class="o">=</span> <span class="s2">"Safe String"</span><span class="p">.</span><span class="nf">html_safe</span>
<span class="o">=&gt;</span> <span class="s2">"Safe String"</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">003</span><span class="p">:</span><span class="mi">0</span><span class="o">&gt;</span> <span class="n">not_safe_string</span> <span class="o">=</span> <span class="s2">"Not Safe String"</span>
<span class="o">=&gt;</span> <span class="s2">"Not Safe String"</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">004</span><span class="p">:</span><span class="mi">0</span><span class="o">&gt;</span> <span class="p">(</span><span class="n">safe_string</span> <span class="o">+</span> <span class="n">not_safe_string</span><span class="p">).</span><span class="nf">html_safe?</span>
<span class="o">=&gt;</span> <span class="kp">true</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">005</span><span class="p">:</span><span class="mi">0</span><span class="o">&gt;</span> <span class="p">(</span><span class="n">not_safe_string</span> <span class="o">+</span> <span class="n">safe_string</span><span class="p">).</span><span class="nf">html_safe?</span>
<span class="o">=&gt;</span> <span class="kp">false</span></code></pre></figure>

<h1 id="generative-testing-for-better-code-by-jessica-kerr">“Generative Testing for Better Code” by Jessica Kerr</h1>
<p>This was the final talk of the conference and the organizers couldn’t have picked a better talk to end with. Generative testing, instead of example-based testing, uses randomized inputs and then after application is run with these randomized input verifies that a set of properties hold true. In a standard example-based test, the input would be fixed (the same every time the test is run) and the expected behavior would be well-defined. Generative testing is a little more difficult since the input is not determined until the test is run and it may be difficult to know the expected behavior when writing the test.</p>

<p>From her abstract:</p>

<blockquote>
  <p>The goal of Generative Testing is to carefully define every possible input, then let the framework run hundreds of scenarios through the same test. It means thinking about more than a few examples, and deciding what the code should do in every possible situation.</p>
</blockquote>

<p>Even though Generative Testing is more mature in other languages, she gave some examples in Ruby using the library <a href="https://github.com/hayeah/rantly">rantly</a>. This repository has some examples she went through during the talk <a href="https://github.com/jessitron/venn">https://github.com/jessitron/venn</a>.</p>

<h1 id="after-the-conference">After the conference</h1>

<p>I had a lot of fun at my first Ruby conference and I am looking forward to attending more in the future. I definitely would recommend this conference to anyone in the future. They have a great Ruby community in Pittsburgh and they put a lot of time and effort into making it a great weekend for those who attended.</p>]]></content><author><name>Mike Dalton</name></author><category term="ruby" /><category term="scrc" /><category term="conference" /><category term="entrepreneurship" /><category term="action_view" /><category term="testing" /><summary type="html"><![CDATA[(see Steel City Ruby ‘14 - Part 1 for my thoughts on the first day of the conference)]]></summary></entry><entry><title type="html">Steel City Ruby ‘14 Recap - Part 1</title><link href="https://mikedalton.co/2014/08/23/steel-city-ruby-2014-recap-part-1/" rel="alternate" type="text/html" title="Steel City Ruby ‘14 Recap - Part 1" /><published>2014-08-23T00:00:00+00:00</published><updated>2014-08-23T00:00:00+00:00</updated><id>https://mikedalton.co/2014/08/23/steel-city-ruby-2014-recap-part-1</id><content type="html" xml:base="https://mikedalton.co/2014/08/23/steel-city-ruby-2014-recap-part-1/"><![CDATA[<p>Last weekend I attended my first Ruby conference, Steely City Ruby in Pittsburgh, PA. I learned a lot of the two-day conference and had a lot of fun along the way. Four co-workers and I left Thursday night from Philadelphia. We gave one of the speakers at the conference, <a href="https://twitter.com/AustinSeraphin">Austin Seraphin</a>, a ride out. He is an iOS accessibility consultant and developer of some pretty cool command line tools for interacting with iOS and RubyMotion. We got into Pittsburgh late that night and went right to sleep in preparation of next morning’s talks.</p>

<p>Here are my highlights from the first day:</p>

<h1 id="jruby-the-best-parts-by-charles-nutter">“JRuby: The Best Parts” by Charles Nutter</h1>
<p>I knew a little about JRuby before the conference, but this talk by the creator of the implementation, gave me a lot of insight into the reasons someone would choose it over MRI. The first “best part” that stuck out to me was that the virtual machine it uses (the Java Virtual Machine) is more mature than any other virtual machine since it has been optimized over the years. The second “best part” was that JRuby utilizes Java threads and can run multiple threads concurrently unlike MRI. He also highlighted two common complaints with JRuby: slow startup and lack of C-extensions. He addressed slow startup by suggestion a tool called drip that preloads new JVM instances. There was an experiment at adding support for C-extensions but there were too many issues with it.</p>

<h1 id="utils-is-a-junk-drawer-by-franklin-weber">“Utils is a Junk Drawer” by Franklin Weber</h1>
<p>Franklin talked a lot about util classes that could be utilized across projects. The example that stuck out the most to me was from Rake.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># from https://github.com/jimweirich/rake/blob/master/lib/rake/ext/string.rb</span>
<span class="k">class</span> <span class="nc">String</span>
  <span class="n">rake_extension</span><span class="p">(</span><span class="s2">"ext"</span><span class="p">)</span> <span class="k">do</span>
    <span class="k">def</span> <span class="nf">ext</span><span class="p">(</span><span class="n">newext</span><span class="o">=</span><span class="s1">''</span><span class="p">)</span>
      <span class="o">...</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="c1"># from https://github.com/jimweirich/rake/blob/master/lib/rake/ext/core.rb</span>
<span class="k">class</span> <span class="nc">Module</span>
  <span class="k">def</span> <span class="nf">rake_extension</span><span class="p">(</span><span class="nb">method</span><span class="p">)</span>
    <span class="k">if</span> <span class="nb">method_defined?</span><span class="p">(</span><span class="nb">method</span><span class="p">)</span>
      <span class="vg">$stderr</span><span class="p">.</span><span class="nf">puts</span> <span class="s2">"WARNING: Possible conflict with Rake extension: "</span> <span class="o">+</span>
        <span class="s2">"</span><span class="si">#{</span><span class="nb">self</span><span class="si">}</span><span class="s2">#</span><span class="si">#{</span><span class="nb">method</span><span class="si">}</span><span class="s2"> already exists"</span>
    <span class="k">else</span>
      <span class="k">yield</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre></figure>

<p>The idea behind this code is to a <code class="language-plaintext highlighter-rouge">rake_extension</code> call around a core method that you are redefining to notify the developer if the method has already been created by another Gem.</p>

<h1 id="better-coding-with-ruby-lambdas-by-keith-bennett">“Better Coding with Ruby Lambdas” by Keith Bennett</h1>
<p>Keith talked about the various uses of lambda’s and how they are under-utilized when compared to normal methods. I particularly liked his use of lambdas to create “inner methods”. This technique reminds me a lot of Scala inner methods. When a method needs to call several other function, instead of creating private methods, local lambdas can be created.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># from https://github.com/keithrbennett/ruby-lambdas-presentation-notes/blob/master/inner_functions__lambdas.rb</span>
<span class="k">class</span> <span class="nc">SampleUsingLambdas</span>

  <span class="k">def</span> <span class="nf">task_1</span>

    <span class="n">validate_inputs</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">()</span> <span class="k">do</span>
      <span class="o">...</span>
    <span class="k">end</span>

    <span class="n">perform</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">()</span> <span class="k">do</span>
      <span class="o">...</span>
    <span class="k">end</span>

    <span class="n">cleanup</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">()</span> <span class="k">do</span>
      <span class="o">...</span>
    <span class="k">end</span>

    <span class="n">validate_inputs</span><span class="o">.</span><span class="p">()</span>
    <span class="n">perform</span><span class="o">.</span><span class="p">()</span>
    <span class="n">cleanup</span><span class="o">.</span><span class="p">()</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="c1"># instead of</span>

<span class="k">class</span> <span class="nc">SampleUsingLambdas</span>

  <span class="k">def</span> <span class="nf">task_1</span>
    <span class="n">validate_inputs</span>
    <span class="n">perform</span>
    <span class="n">cleanup</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">validate_inputs</span>
    <span class="o">...</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">perform</span>
    <span class="o">...</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">cleanup</span>
    <span class="o">...</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre></figure>

<h1 id="after-the-talks">After the talks</h1>

<p>After the talks were over for the day, we headed to the conference reception at PNC Park (where the Pittsburgh Pirates play). They had great food and a great view waiting for us.</p>

<p><img src="/assets/steel-city-ruby-pnc-park.jpg" alt="Steel City Ruby Reception at PNC Park" /></p>

<p>(see <a href="/2014/08/28/steel-city-ruby-2014-recap-part-2/">Steel City Ruby ‘14 - Part 2</a> for my thoughts on the second day of the conference)</p>]]></content><author><name>Mike Dalton</name></author><category term="ruby" /><category term="scrc" /><category term="conference" /><category term="jruby" /><category term="rake" /><category term="functional" /><summary type="html"><![CDATA[Last weekend I attended my first Ruby conference, Steely City Ruby in Pittsburgh, PA. I learned a lot of the two-day conference and had a lot of fun along the way. Four co-workers and I left Thursday night from Philadelphia. We gave one of the speakers at the conference, Austin Seraphin, a ride out. He is an iOS accessibility consultant and developer of some pretty cool command line tools for interacting with iOS and RubyMotion. We got into Pittsburgh late that night and went right to sleep in preparation of next morning’s talks.]]></summary></entry></feed>