<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://localjoost.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://localjoost.github.io/" rel="alternate" type="text/html" /><updated>2026-04-24T08:51:27+02:00</updated><id>https://localjoost.github.io/feed.xml</id><title type="html">DotNetByExample - The Next Generation</title><subtitle>Code samples and how-to&apos;s, mostly in the Microsoft stack, currently concentrating on Mixed Reality</subtitle><author><name>Joost van Schaik (&apos;LocalJoost&apos;)</name></author><entry><title type="html">Protecting your Entra token on Snap Spectacles with a PIN code</title><link href="https://localjoost.github.io/Protecting-your-Entra-token-on-Snap-Spectacles-with-a-PIN-code/" rel="alternate" type="text/html" title="Protecting your Entra token on Snap Spectacles with a PIN code" /><published>2026-04-24T00:00:00+02:00</published><updated>2026-04-24T00:00:00+02:00</updated><id>https://localjoost.github.io/Protecting-your-Entra-token-on-Snap-Spectacles-with-a-PIN-code</id><content type="html" xml:base="https://localjoost.github.io/Protecting-your-Entra-token-on-Snap-Spectacles-with-a-PIN-code/"><![CDATA[<p>A few weeks ago I wrote <a href="https://localjoost.github.io/Microsoft-Entra-authentication-using-Device-Code-Flow-for-Snap-Spectacles/">an article about authenticating Snap Spectacles with Microsoft Entra using Device Code Flow</a>. At the end of that article I noted that the <code class="language-plaintext highlighter-rouge">PersistentStorageTokenStore</code> I had built was fine for a demo, but not quite right for an enterprise deployment: as long as the refresh token sits there in plain JSON, <em>anyone who puts on your Spectacles is you</em> as far as your backend is concerned. I promised I might write a better <code class="language-plaintext highlighter-rouge">ITokenStore</code> fix in a follow-up post. This is that follow-up post.</p>

<h2 id="what-is-the-idea">What is the idea?</h2>

<p>The Spectacles themselves have no user authentication that we, as lens developers, can piggyback on. So if we want any kind of “prove it’s really you” before using the stored token, we have to do it ourselves inside the lens. The simplest thing that could possibly work is, of course, a PIN code: the user picks one the first time they log in, we use it as an encryption key for the token, and from that point on the lens can only recover the token if the user types the PIN again.</p>

<p>The nice side effect is that the token on disk is no longer readable at all without the PIN, which should calm your Compliance Officer down a bit (although as I wrote last time, I still don’t think anything but the lens itself can read that storage anyway, but let’s not argue with Compliance Officers).</p>

<p>So this is what I built. After you have authenticated, you get asked to enter your pincode twice, then you login:</p>

<p><img src="/assets/2026-04-24-Protecting-your-Entra-token-on-Snap-Spectacles-with-a-PIN-code/login.gif" alt="login" /></p>

<p>And when you start the lens for the second ir later time time you can login with just your pin.</p>

<p><img src="/assets/2026-04-24-Protecting-your-Entra-token-on-Snap-Spectacles-with-a-PIN-code/login2.gif" alt="login2" /></p>

<p>That PIN is actually used in the encryption. No PIN, no decryption, no token, no service calls. And you can even use the Spectacles app on your phone to type in the pin, which usually is easier than using a floating keyboard.</p>

<h2 id="the-only-new-interface-you-need-to-know-about">The only new interface you need to know about</h2>

<p>The whole story really boils down to one new service interface:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">IPincodeRequestService</span> <span class="p">{</span>
    <span class="nx">askForPinCode</span><span class="p">(</span><span class="nx">requireConfirm</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nx">IPincodeRequestService</span><span class="p">()</span> <span class="p">{}</span>
</code></pre></div></div>

<p>This is the contract between the token store (which needs a PIN to do its job) and whatever part of your app happens to know how to talk to the user. The <code class="language-plaintext highlighter-rouge">requireConfirm</code> flag is the difference between “pick a new PIN” (ask twice, make sure the user entered the same thing both times) and “enter your existing PIN” (ask once).</p>

<p>The empty function at the bottom is the service token <a href="https://localjoost.github.io/Service-driven-development-for-Snap-Spectacles-in-Lens-Studio/">I wrote about in my service-driven development article</a>.</p>

<h2 id="the-new-token-store">The new token store</h2>

<p>With that interface in hand, the new <code class="language-plaintext highlighter-rouge">EncryptedPersistentStorageTokenStore</code> is pretty readable. Here is the main part:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">AESEncryptionHandler</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">LocalJoost/Security/AESEncryptionHandler</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AccessToken</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./AccessToken</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ITokenStore</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./ITokenStore</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">IPincodeRequestService</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./IPincodeRequestService</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ServiceManager</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../ServiceManager</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">EncryptedPersistentStorageTokenStore</span> <span class="k">implements</span> <span class="nx">ITokenStore</span> <span class="p">{</span>
    <span class="k">private</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">GeneralDataStore</span> <span class="o">=</span> <span class="nb">global</span><span class="p">.</span><span class="nx">persistentStorageSystem</span><span class="p">.</span><span class="nx">store</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">tokenKey</span><span class="p">:</span> <span class="kr">string</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">encrypted_access_token</span><span class="dl">"</span><span class="p">;</span>
    <span class="k">private</span> <span class="nx">pinCode</span> <span class="o">=</span> <span class="dl">""</span>
    <span class="k">private</span> <span class="nx">pinCodeRequestServiceCache</span><span class="p">:</span> <span class="nx">IPincodeRequestService</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">maxRetries</span> <span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">async</span> <span class="nx">getToken</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">AccessToken</span> <span class="o">|</span> <span class="kc">null</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">getEncryptedTokenFromStore</span><span class="p">();</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nx">AccessToken</span><span class="p">.</span><span class="nx">fromJSON</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">token</span><span class="p">));</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="nx">setToken</span><span class="p">(</span><span class="nx">token</span><span class="p">:</span> <span class="nx">AccessToken</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">pinCode</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">pinCode</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">pinCodeRequestService</span><span class="p">.</span><span class="nx">askForPinCode</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="kd">var</span> <span class="nx">encryptedToken</span> <span class="o">=</span> <span class="nx">AESEncryptionHandler</span><span class="p">.</span><span class="nx">encryptWithPin</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">token</span><span class="p">),</span> <span class="k">this</span><span class="p">.</span><span class="nx">pinCode</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">putString</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">tokenKey</span><span class="p">,</span> <span class="nx">encryptedToken</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="nx">clearToken</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">tokenKey</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">pinCode</span> <span class="o">=</span> <span class="dl">""</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A few things to notice:</p>

<ul>
  <li>The PIN is kept in memory after the first successful entry. So you only get prompted once per lens session, not on every token refresh. This prevents the decryption having to happen every time the PIN is asked. On <code class="language-plaintext highlighter-rouge">clearToken</code> (that is, on logout) we wipe it again.</li>
  <li><code class="language-plaintext highlighter-rouge">setToken</code> with <code class="language-plaintext highlighter-rouge">requireConfirm: true</code> - when a token is being stored, we’re either doing it for the first time ever, or the user has just logged in again after a logout. Either way they need to <em>set</em> a PIN, not recall one, so we make them confirm it.</li>
  <li><code class="language-plaintext highlighter-rouge">getToken</code> with <code class="language-plaintext highlighter-rouge">requireConfirm: false</code> - the user is trying to unlock an existing token, so asking once is enough.</li>
  <li>The <code class="language-plaintext highlighter-rouge">pinCodeRequestServiceCache</code> is just a lazy lookup into the <code class="language-plaintext highlighter-rouge">ServiceManager</code>. This is because the <code class="language-plaintext highlighter-rouge">IPincodeRequestService</code> is typically registered by some UI component, and the token store may already exist before that UI is ready. Looking it up on first use avoids initialization-order headaches.</li>
</ul>

<p>The actual encryption happens in <code class="language-plaintext highlighter-rouge">AESEncryptionHandler.encryptWithPin</code>. More about that in a minute. First, the other half of <code class="language-plaintext highlighter-rouge">getEncryptedTokenFromStore</code>:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">async</span> <span class="nx">getEncryptedTokenFromStore</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">encryptedToken</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">getString</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">tokenKey</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">encryptedToken</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="kd">var</span> <span class="na">retries</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">maxRetries</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>

    <span class="kd">var</span> <span class="na">unencryptedCode</span> <span class="p">:</span> <span class="kr">string</span>
    <span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="nx">unencryptedCode</span> <span class="o">&amp;&amp;</span> <span class="nx">retries</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">pinCode</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">pinCode</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">pinCodeRequestService</span><span class="p">.</span><span class="nx">askForPinCode</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="nx">unencryptedCode</span> <span class="o">=</span> <span class="nx">AESEncryptionHandler</span><span class="p">.</span><span class="nx">decryptWithPin</span><span class="p">(</span><span class="nx">encryptedToken</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">pinCode</span><span class="p">);</span>
        <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">unencryptedCode</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">pinCode</span> <span class="o">=</span> <span class="dl">""</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="nx">retries</span><span class="o">--</span><span class="p">;</span>
        <span class="k">if</span><span class="p">(</span> <span class="nx">retries</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">clearToken</span><span class="p">();</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Maximum retries reached. Please re-authenticate the device.</span><span class="dl">"</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nx">unencryptedCode</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The logic is:</p>

<ul>
  <li>If there’s no token in storage, there’s nothing to decrypt. Return null, and the service will fall through to a fresh device code flow.</li>
  <li>If there is a token, ask for the PIN and try to decrypt.</li>
  <li>Wrong PIN? <code class="language-plaintext highlighter-rouge">decryptWithPin</code> returns null (more on that in a minute), we throw away the cached PIN, and ask again.</li>
  <li>After three wrong PINs, give up, wipe the encrypted token from storage, and throw. The user is forced to do a fresh device code flow, which is exactly what you want: a thief with a stolen pair of Spectacles doesn’t get unlimited guesses.</li>
</ul>

<h2 id="the-encryption-itself">The encryption itself</h2>

<p>Spectacles doesn’t ship with a crypto API, so I dropped in a slighty modified version of <a href="https://www.npmjs.com/package/crypto-js">crypto-js</a> as a vendored library under <code class="language-plaintext highlighter-rouge">LocalJoost/Security/crypto-js</code>. Around it sits <code class="language-plaintext highlighter-rouge">AESEncryptionHandler</code>, which does two things: plain AES-CBC (given a key and IV), and a PIN-based variant that derives the key from the PIN.</p>

<p>The PIN-based variant is what the token store actually uses:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="nx">encryptWithPin</span><span class="p">(</span><span class="nx">plainText</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">pin</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">salt</span> <span class="o">=</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">lib</span><span class="p">.</span><span class="nx">WordArray</span><span class="p">.</span><span class="nx">random</span><span class="p">(</span><span class="mi">16</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">iv</span> <span class="o">=</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">lib</span><span class="p">.</span><span class="nx">WordArray</span><span class="p">.</span><span class="nx">random</span><span class="p">(</span><span class="mi">16</span><span class="p">);</span>

        <span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">PBKDF2</span><span class="p">(</span><span class="nx">pin</span><span class="p">,</span> <span class="nx">salt</span><span class="p">,</span> <span class="p">{</span>
            <span class="na">keySize</span><span class="p">:</span> <span class="mi">256</span> <span class="o">/</span> <span class="mi">32</span><span class="p">,</span>
            <span class="na">iterations</span><span class="p">:</span> <span class="mi">1000</span>
        <span class="p">});</span>

        <span class="kd">const</span> <span class="nx">ivHex</span> <span class="o">=</span> <span class="nx">iv</span><span class="p">.</span><span class="nx">toString</span><span class="p">();</span>
        <span class="kd">const</span> <span class="nx">encryptedCiphertext</span> <span class="o">=</span> <span class="nx">AESEncryptionHandler</span><span class="p">.</span><span class="nx">encrypt</span><span class="p">(</span>
            <span class="nx">plainText</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">hex:</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">key</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span>
            <span class="dl">"</span><span class="s2">hex:</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">ivHex</span>
        <span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">encryptedCiphertext</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Format: salt (32 hex chars) + iv (32 hex chars) + ciphertext (hex)</span>
        <span class="k">return</span> <span class="nx">salt</span><span class="p">.</span><span class="nx">toString</span><span class="p">()</span> <span class="o">+</span> <span class="nx">ivHex</span> <span class="o">+</span> <span class="nx">encryptedCiphertext</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">print</span><span class="p">(</span><span class="dl">"</span><span class="s2">Encryption error: </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">error</span><span class="p">);</span>
        <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>For the non-cryptographers: a PIN code like “123456” is way too short and predictable to use as an encryption key directly. PBKDF2 takes the PIN and a random salt, chews on them for 1000 iterations, and spits out a proper 256-bit key. The salt is different every time you encrypt, which is why you can’t just store <code class="language-plaintext highlighter-rouge">key = f(pin)</code> somewhere and be done with it - you need the salt alongside the ciphertext to reconstruct the key later.</p>

<p>The IV (initialization vector) is also random per encryption and also needs to be stored alongside. So the output format is simply <code class="language-plaintext highlighter-rouge">salt_hex + iv_hex + ciphertext_hex</code>, all glued into one string. Decryption reverses it:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="nx">decryptWithPin</span><span class="p">(</span><span class="nx">encryptedText</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">pin</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span>
        <span class="c1">// Extract salt (first 32 hex chars = 16 bytes), IV (next 32), and ciphertext (rest)</span>
        <span class="kd">const</span> <span class="nx">salt</span> <span class="o">=</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">enc</span><span class="p">.</span><span class="nx">Hex</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">encryptedText</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">32</span><span class="p">));</span>
        <span class="kd">const</span> <span class="nx">ivHex</span> <span class="o">=</span> <span class="nx">encryptedText</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">32</span><span class="p">,</span> <span class="mi">64</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">ciphertextHex</span> <span class="o">=</span> <span class="nx">encryptedText</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">64</span><span class="p">);</span>

        <span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">PBKDF2</span><span class="p">(</span><span class="nx">pin</span><span class="p">,</span> <span class="nx">salt</span><span class="p">,</span> <span class="p">{</span>
            <span class="na">keySize</span><span class="p">:</span> <span class="mi">256</span> <span class="o">/</span> <span class="mi">32</span><span class="p">,</span>
            <span class="na">iterations</span><span class="p">:</span> <span class="mi">1000</span>
        <span class="p">});</span>

        <span class="k">return</span> <span class="nx">AESEncryptionHandler</span><span class="p">.</span><span class="nx">decrypt</span><span class="p">(</span>
            <span class="nx">ciphertextHex</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">hex:</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">key</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span>
            <span class="dl">"</span><span class="s2">hex:</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">ivHex</span>
        <span class="p">);</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">print</span><span class="p">(</span><span class="dl">"</span><span class="s2">Decryption error: </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">error</span><span class="p">);</span>
        <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Chop the stored string into three pieces, re-derive the key from the PIN and the salt, and ask AES to decrypt. If the PIN was wrong, CBC with PKCS7 padding will typically fail the padding check and crypto-js will throw - which is why the whole thing is wrapped in a try/catch that returns null on failure. That’s what the retry loop in the token store keys off of.</p>

<p>There’s also a <code class="language-plaintext highlighter-rouge">test()</code> method in there that does a round-trip with <code class="language-plaintext highlighter-rouge">"123456"</code> and verifies a wrong PIN returns null. Handy for when you’re poking at it in the editor and wondering why nothing works - usually it’s because you forgot to actually register the service.</p>

<h2 id="a-little-ripple-effect-on-the-interface">A little ripple effect on the interface</h2>

<p>Making the token store wait for user input means <code class="language-plaintext highlighter-rouge">ITokenStore</code> itself had to become async. It used to be:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">interface</span> <span class="nx">ITokenStore</span> <span class="p">{</span>
    <span class="nx">getToken</span><span class="p">():</span> <span class="nx">AccessToken</span> <span class="o">|</span> <span class="kc">null</span><span class="p">;</span>
    <span class="nx">setToken</span><span class="p">(</span><span class="nx">token</span><span class="p">:</span> <span class="nx">AccessToken</span><span class="p">):</span> <span class="k">void</span><span class="p">;</span>
    <span class="nx">clearToken</span><span class="p">():</span> <span class="k">void</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now it’s:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">interface</span> <span class="nx">ITokenStore</span> <span class="p">{</span>
    <span class="nx">getToken</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">AccessToken</span> <span class="o">|</span> <span class="kc">null</span><span class="o">&gt;</span>
    <span class="nx">setToken</span><span class="p">(</span><span class="nx">token</span><span class="p">:</span> <span class="nx">AccessToken</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span><span class="p">;</span>
    <span class="nx">clearToken</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Which means <code class="language-plaintext highlighter-rouge">EntraDeviceCodeFlowAuthenticationService.authenticate</code> needs a couple of <code class="language-plaintext highlighter-rouge">await</code>s sprinkled in, and, more importantly, a try/catch around the initial <code class="language-plaintext highlighter-rouge">getToken</code>, because that’s the call that can now throw the “maximum retries reached” error I just described:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="nx">authenticate</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">AccessToken</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="na">currentToken</span><span class="p">:</span> <span class="nx">AccessToken</span> <span class="o">|</span> <span class="kc">null</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
    <span class="k">try</span><span class="p">{</span>
        <span class="nx">currentToken</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">tokenStore</span><span class="p">.</span><span class="nx">getToken</span><span class="p">();</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">errorMessage</span> <span class="o">=</span> <span class="nx">error</span> <span class="k">instanceof</span> <span class="nb">Error</span> <span class="p">?</span> <span class="nx">error</span><span class="p">.</span><span class="nx">message</span> <span class="p">:</span> <span class="nb">String</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="nx">errorMessage</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">UserActionRequiredEvent</span><span class="p">.</span><span class="nx">invoke</span><span class="p">(</span><span class="nx">errorMessage</span><span class="p">);</span>
        <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">awaitableSleep</span><span class="p">.</span><span class="nx">sleep</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If the user fails the PIN three times, we show them the error via <code class="language-plaintext highlighter-rouge">UserActionRequiredEvent</code>, wait three seconds so they can actually read it, and then fall through to the normal “no valid token, start device code flow” path. Which is the correct behaviour: you got locked out, so go log in again.</p>

<p>The old <code class="language-plaintext highlighter-rouge">PersistentStorageTokenStore</code> is still there, by the way. It just got its method signatures updated to <code class="language-plaintext highlighter-rouge">async</code>/<code class="language-plaintext highlighter-rouge">Promise</code>. If you don’t want PIN protection you can still use it - just revert the line in <code class="language-plaintext highlighter-rouge">EntraBootstrapper</code> that we’ll look at next.</p>

<h2 id="wiring-it-all-up">Wiring it all up</h2>

<p>The bootstrapper is almost the same as before. The one-line difference:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">entraService</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EntraDeviceCodeFlowAuthenticationService</span><span class="p">(</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">tenantId</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">clientId</span><span class="p">,</span> <span class="k">new</span> <span class="nx">EncryptedPersistentStorageTokenStore</span><span class="p">(),</span> <span class="kc">true</span><span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">EncryptedPersistentStorageTokenStore</code> instead of <code class="language-plaintext highlighter-rouge">PersistentStorageTokenStore</code>. That’s it. Everything else, the tenant ID, the client ID, the service registration, all unchanged.</p>

<p>The other bit of wiring is on the UI side. Something has to actually implement <code class="language-plaintext highlighter-rouge">IPincodeRequestService</code> and pop up a keyboard when the token store calls <code class="language-plaintext highlighter-rouge">askForPinCode</code>. For the demo I let <code class="language-plaintext highlighter-rouge">EntraSampleUIWindow</code> do double duty: it’s both the thing that shows the welcome messages <em>and</em> the thing that asks for the PIN. In a real app you would probably make a dedicated component, but for a sample this keeps the moving parts to a minimum.</p>

<p>In the scene, I added a <code class="language-plaintext highlighter-rouge">TextInputField</code> (one of the new UIKit components) to the prefab and hooked it up to a new <code class="language-plaintext highlighter-rouge">@input</code>:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">input</span> <span class="k">private</span> <span class="nx">pincodeInput</span><span class="p">:</span> <span class="nx">TextInputField</span><span class="p">;</span>
</code></pre></div></div>

<p>In <code class="language-plaintext highlighter-rouge">onAwake</code> we register ourselves as the implementation:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">serviceManager</span> <span class="o">=</span> <span class="nx">ServiceManager</span><span class="p">.</span><span class="nx">getInstance</span><span class="p">();</span>
<span class="nx">serviceManager</span><span class="p">.</span><span class="nx">register</span><span class="p">(</span><span class="nx">IPincodeRequestService</span><span class="p">,</span> <span class="k">this</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">entraService</span> <span class="o">=</span> <span class="nx">serviceManager</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">IEntraDeviceCodeFlowAuthenticationService</span><span class="p">);</span>
</code></pre></div></div>

<p>And then the actual implementation of the interface method:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="nx">askForPinCode</span><span class="p">(</span><span class="nx">requireConfirm</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">validPinCodeObtained</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">pincodeInput</span><span class="p">.</span><span class="nx">sceneObject</span><span class="p">.</span><span class="nx">enabled</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="kd">var</span> <span class="na">pinCode</span><span class="p">:</span> <span class="kr">string</span> <span class="o">=</span> <span class="dl">""</span><span class="p">;</span>
    <span class="k">do</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="nx">requireConfirm</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">Please enter a PIN code to encrypt your token:</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">Please enter your PIN code to decrypt your token:</span><span class="dl">"</span><span class="p">);</span>
        <span class="nx">pinCode</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">waitForInput</span><span class="p">();</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">requireConfirm</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">validPinCodeObtained</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="p">{</span>
            <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">awaitableSleep</span><span class="p">.</span><span class="nx">sleep</span><span class="p">(</span><span class="mf">0.25</span><span class="p">);</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="dl">"</span><span class="s2">Please confirm your PIN code by entering it again:</span><span class="dl">"</span><span class="p">);</span>
            <span class="kd">var</span> <span class="nx">confirmPinCode</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">waitForInput</span><span class="p">();</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">pinCode</span> <span class="o">!==</span> <span class="nx">confirmPinCode</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="dl">"</span><span class="s2">PIN codes do not match. Please try again.</span><span class="dl">"</span><span class="p">);</span>
                <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">awaitableSleep</span><span class="p">.</span><span class="nx">sleep</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="nx">validPinCodeObtained</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="nx">validPinCodeObtained</span><span class="p">)</span>
    <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">awaitableSleep</span><span class="p">.</span><span class="nx">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">pincodeInput</span><span class="p">.</span><span class="nx">sceneObject</span><span class="p">.</span><span class="nx">enabled</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">pinCode</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Enable the input field, show the right prompt, wait for the user, and if we’re in “pick a new PIN” mode, ask again and loop until the two entries match. When we’re done, hide the input field so it doesn’t clutter the rest of the flow. Note again the reappearance of the trusty <code class="language-plaintext highlighter-rouge">AwaitableSleep</code>.</p>

<p>The only mildly interesting bit is <code class="language-plaintext highlighter-rouge">waitForInput</code>:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">async</span> <span class="nx">waitForInput</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">pincodeInput</span><span class="p">.</span><span class="nx">text</span> <span class="o">=</span> <span class="dl">""</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">enterTestPinCode</span><span class="p">();</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">handler</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">input</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">pincodeInput</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">trim</span><span class="p">();</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;=</span> <span class="mi">6</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">pincodeInput</span><span class="p">.</span><span class="nx">onTextChanged</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="nx">handler</span><span class="p">);</span>
                <span class="nb">global</span><span class="p">.</span><span class="nx">textInputSystem</span><span class="p">.</span><span class="nx">dismissKeyboard</span><span class="p">();</span>
                <span class="nx">resolve</span><span class="p">(</span><span class="nx">input</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">};</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">pincodeInput</span><span class="p">.</span><span class="nx">onTextChanged</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">handler</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The UIKit <code class="language-plaintext highlighter-rouge">TextInputField</code> fires <code class="language-plaintext highlighter-rouge">onTextChanged</code> on every keystroke. We wait until there are at least 6 characters, then call it done, remove the handler, dismiss the keyboard, and resolve the promise. No explicit “submit” button needed.</p>

<p>And finally a confession - typing a PIN on the virtual keyboard in the Lens Studio editor is annoying, so I cheated:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="nx">enterTestPinCode</span><span class="p">()</span> <span class="p">:</span> <span class="k">void</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">global</span><span class="p">.</span><span class="nx">deviceInfoSystem</span><span class="p">.</span><span class="nx">isEditor</span><span class="p">())</span> <span class="k">return</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">delayedEvent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">createEvent</span><span class="p">(</span><span class="dl">"</span><span class="s2">DelayedCallbackEvent</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">delayedEvent</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">pincodeInput</span><span class="p">.</span><span class="nx">text</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">123456</span><span class="dl">"</span><span class="p">;</span>
    <span class="p">});</span>
    <span class="nx">delayedEvent</span><span class="p">.</span><span class="nx">reset</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Two seconds after the prompt appears, if we’re running in the editor, the field is auto-filled with <code class="language-plaintext highlighter-rouge">123456</code>. On the actual device this does nothing. Small quality-of-life hack that will save you a lot of typing while developing.</p>

<h2 id="trying-it-out">Trying it out</h2>

<p>Run the lens once, go through the device code flow on your computer as before. After you log in, you’ll get prompted to pick a PIN. Enter it twice. The token is now encrypted in storage.</p>

<p>Stop the lens. Start it again. You’ll get asked for your PIN. Enter it, and you’ll get the welcome message without having to go through device code flow again. Enter the wrong PIN three times and it will kick you out and restart the device code flow.</p>

<p>In the editor you’ll actually see things happening in the debug console if you leave <code class="language-plaintext highlighter-rouge">verbose</code> on, which is kind of fun. On device it all just works.</p>

<h2 id="one-last-caveat">One last caveat</h2>

<p>I want to be very clear: this is <em>a</em> solution, not <em>the</em> solution. A 6-digit PIN encrypted with PBKDF2-1000 is not going to hold up against a determined attacker with the encrypted blob in hand. 1000 iterations is on the low side by 2026 standards and a six-digit numerical PIN has only a million possible values. Also, I think there needs to be protection against stupidly simple PIN codes like 000000, 111111 and the 123456 I showed in the demo because you <em>know</em> people are going to pick those stupid codes if you let them. But on Spectacles, as I wrote last time, there is (as far as I know) no way to get the encrypted blob out of the lens’ persistent storage in the first place, so brute-forcing it is not a realistic attack. What this <em>does</em> protect against is the much more mundane scenario: someone picks up your glasses, puts them on, and fires up the lens. Without the PIN, they get nothing, and your enterprise backend is not going to hand out data to a stranger. That’s a lot better than where we were last month.</p>

<p>If your threat model is scarier than that, you probably want a proper secure enclave, hardware attestation, and a team of people with ‘CISSP’ on their LinkedIn. I wish you all the best.</p>

<p>Code, as always, <a href="https://github.com/LocalJoost/SpecEntraAuthService/tree/pincode-protected">can be found at GitHub</a> - in the pincode-protected branch.</p>]]></content><author><name>Joost van Schaik (&apos;LocalJoost&apos;)</name></author><category term="Spectacles" /><category term="Lens Studio" /><category term="TypeScript" /><category term="Entra" /><category term="Azure" /><category term="Microsoft" /><summary type="html"><![CDATA[A few weeks ago I wrote an article about authenticating Snap Spectacles with Microsoft Entra using Device Code Flow. At the end of that article I noted that the PersistentStorageTokenStore I had built was fine for a demo, but not quite right for an enterprise deployment: as long as the refresh token sits there in plain JSON, anyone who puts on your Spectacles is you as far as your backend is concerned. I promised I might write a better ITokenStore fix in a follow-up post. This is that follow-up post.]]></summary></entry><entry><title type="html">Microsoft Entra authentication using Device Code Flow for Snap Spectacles</title><link href="https://localjoost.github.io/Microsoft-Entra-authentication-using-Device-Code-Flow-for-Snap-Spectacles/" rel="alternate" type="text/html" title="Microsoft Entra authentication using Device Code Flow for Snap Spectacles" /><published>2026-03-21T00:00:00+01:00</published><updated>2026-03-21T00:00:00+01:00</updated><id>https://localjoost.github.io/Microsoft-Entra-authentication-using-Device-Code-Flow-for-Snap-Spectacles</id><content type="html" xml:base="https://localjoost.github.io/Microsoft-Entra-authentication-using-Device-Code-Flow-for-Snap-Spectacles/"><![CDATA[<p>While Snap Spectacles are clearly targeted towards <em>consumers</em>, and <a href="https://ar.snap.com/snap-supabase">Snap have partnered with Supabase</a> as a preferred cloud backend, there are of course always nitwits who are going against the grain and try to use a consumer device to talk to enterprise-grade backends. This tendency is especially present in a particularly rare subclass of those nitwits who actually come from an enterprise programming background and kind of accidentally stumbled into Mixed Reality. This here nitwit set out to make Snap Spectacles authenticate with <em><a href="https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id">Microsoft Entra</a></em> as a kind of what he thought to be a fun experiment, succeeded at doing so, and then of course publishes it in case there are <em>other</em> nitwits around who would like to do the same thing.</p>

<h2 id="entra-authentication-the-general-idea">Entra authentication, the general idea</h2>

<p>For normal authentication, the flow is usually:</p>
<ol>
  <li>You or your app pops up a window or a browser that requires you to enter credentials. This can be either username and password (followed by some 2FA prompt on your phone - you have set that up right?), a passkey, or a Windows Hello recognition.</li>
  <li>If satisfactory credentials are supplied, you get a package of data that includes your name, ID (usually email address), and most importantly: an access token and a refresh token.</li>
  <li>The access token can be used as a bearer token to access secure resources, for instance some API.</li>
  <li>The refresh token can be used to get a new access token when the old one expires.</li>
</ol>

<h2 id="device-code-flow">Device Code Flow</h2>

<p>This all works fine on a computer or a phone, but on Spectacles we don’t have popup browser windows, passkeys, or Windows Hello, and although the virtual keyboard is pretty decent and you can type in usernames and passwords using your phone as well, anyone who has done so repeatedly (like me) can tell you it’s a pretty tedious PITA to do it that way. The ETF OAuth working group, who created standards around OAuth, fortunately saw this as well and defined something: <em>OAuth 2.0 Device Authorization Grant</em>. It’s essentially a way to do authentication for what they call so beautifully an ‘input-restricted device’. The Microsoft implementation is commonly referred to as <a href="https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code">Device Code Flow</a>. This works as follows:</p>

<ol>
  <li>The device calls a particular API provided by the identity authority (in this case by Microsoft).</li>
  <li>The API returns a data structure containing, amongst other things, a code and a verification URL.</li>
  <li>The device instructs the user to go to a particular website (in Microsoft’s case this is usually https://login.microsoft.com/device) on any device they see fit. I usually just take a browser on a computer, as typing on a keyboard while wearing a see-through device is perfectly possible.</li>
  <li>You log in normally there, using whatever authentication you or your admin has set up.</li>
  <li>The device, in the meantime, has been polling the verification URL every few seconds. While the user has not logged in on the computer, it gets a 400 error and should retry after a couple of seconds. As soon as the user has logged in, it gets a 200 status plus a data structure containing user data as well as an access token, an expiration date, and a refresh token. The access token you can add as a normal bearer token to your HTTP request to a secured resource; the refresh token can be used to get a new token when the old one expires, as I wrote before.</li>
</ol>

<p>The login procedure looks like this:</p>

<p><img src="/assets/2026-03-21-Microsoft-Entra-authentication-using-Device-Code-Flow-for-Snap-Spectacles/DeviceCodeFlow.gif" alt="DeviceCodeFlow" /></p>

<p>This kind of login is used for Xbox for instance, but also for things like streaming services, as no one ever liked entering passwords with a TV remote. Because it keeps your token and refresh token, you (fortunately) don’t have to log in every time you use those devices. I have implemented this for Spectacles and Entra using - of course - a service.</p>

<h2 id="basic-usage">Basic usage</h2>

<p>The service needs to be initialized with a tenant ID, a client ID, an <code class="language-plaintext highlighter-rouge">ITokenStore</code> implementation for token persistence (I will explain that later), and an option to get verbose logging. And believe me, verbose it is when you ask it to be so. Getting an Azure tenant ID, setting up a client in Azure to get an ID, and deploying a secured resource to test access with is outside of the scope of this article, but rest assured: if you ask one of the AI chatbots of today, it will guide you through the process pretty easily.</p>

<p>But enfin, the initialization, in the <code class="language-plaintext highlighter-rouge">EntraBootstrapper</code> component:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">serviceManager</span> <span class="o">=</span> <span class="nx">ServiceManager</span><span class="p">.</span><span class="nx">getInstance</span><span class="p">();</span>
<span class="kd">var</span> <span class="nx">entraService</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EntraDeviceCodeFlowAuthenticationService</span><span class="p">(</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">tenantId</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">clientId</span><span class="p">,</span> <span class="k">new</span> <span class="nx">PersistentStorageTokenStore</span><span class="p">(),</span> <span class="kc">true</span><span class="p">);</span>
<span class="nx">serviceManager</span><span class="p">.</span><span class="nx">register</span><span class="p">(</span><span class="nx">IEntraDeviceCodeFlowAuthenticationService</span><span class="p">,</span> <span class="nx">entraService</span><span class="p">);</span>
</code></pre></div></div>

<p><a href="https://localjoost.github.io/Service-driven-development-for-Snap-Spectacles-in-Lens-Studio/#registering-and-using-a-service">Service bootstrapper I explained before in my original article about services in Lens Studio</a>. If you look it up in the scene, you see two text boxes where you can configure the Azure tenant ID and your Azure client app ID.</p>

<p><img src="/assets/2026-03-21-Microsoft-Entra-authentication-using-Device-Code-Flow-for-Snap-Spectacles/configurebootstrapper.png" alt="configurebootstrapper" /></p>

<p>The service has a very simple interface:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">interface</span> <span class="nx">IEntraDeviceCodeFlowAuthenticationService</span> <span class="p">{</span>
    <span class="nx">authenticate</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">AccessToken</span><span class="o">&gt;</span><span class="p">;</span>
    <span class="nl">UserActionRequiredEvent</span><span class="p">:</span> <span class="nx">Event</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">;</span>
    <span class="nx">logout</span><span class="p">():</span> <span class="k">void</span>
<span class="p">}</span>
</code></pre></div></div>

<p>You get yourself a reference to the service:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="p">.</span><span class="nx">entraService</span> <span class="o">=</span> <span class="nx">ServiceManager</span><span class="p">.</span><span class="nx">getInstance</span><span class="p">().</span><span class="kd">get</span><span class="p">(</span><span class="nx">IEntraDeviceCodeFlowAuthenticationService</span><span class="p">);</span>
</code></pre></div></div>

<p>You subscribe to the service’s <code class="language-plaintext highlighter-rouge">UserActionRequiredEvent</code>, and when it’s triggered, call a method that shows a message to the user indicating what to do or what happened:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="p">.</span><span class="nx">entraService</span><span class="p">.</span><span class="nx">UserActionRequiredEvent</span><span class="p">.</span><span class="nx">add</span><span class="p">((</span><span class="nx">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Call <code class="language-plaintext highlighter-rouge">authenticate</code> and wait for a token to appear - or an error to occur:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="na">token</span><span class="p">:</span> <span class="nx">AccessToken</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">entraService</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">();</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">token</span><span class="p">.</span><span class="nx">userName</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="s2">`Authentication failed: </span><span class="p">${</span><span class="nx">e</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And then you call a test URL:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// this needs to be in the declaration part of your calling class</span>
<span class="k">private</span> <span class="k">readonly</span> <span class="nx">http</span><span class="p">:</span> <span class="nx">InternetModule</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">LensStudio:InternetModule</span><span class="dl">"</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">httpRequest</span><span class="p">:</span> <span class="nx">RemoteServiceHttpRequest</span> <span class="o">=</span> <span class="nx">RemoteServiceHttpRequest</span><span class="p">.</span><span class="nx">create</span><span class="p">();</span>
<span class="nx">httpRequest</span><span class="p">.</span><span class="nx">method</span> <span class="o">=</span> <span class="nx">RemoteServiceHttpRequest</span><span class="p">.</span><span class="nx">HttpRequestMethod</span><span class="p">.</span><span class="nx">Get</span><span class="p">;</span>
<span class="nx">httpRequest</span><span class="p">.</span><span class="nx">url</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">testApiUrl</span><span class="p">;</span>
<span class="nx">httpRequest</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">Authorization</span><span class="dl">"</span><span class="p">,</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">token</span><span class="p">.</span><span class="nx">accessToken</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">http</span><span class="p">.</span><span class="nx">performHttpRequest</span><span class="p">(</span><span class="nx">httpRequest</span><span class="p">,</span> <span class="p">(</span><span class="nx">resp</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="s2">`Test URL status code: </span><span class="p">${</span><span class="nx">resp</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Finally, if you want the app to forget about you, simply call the service’s <code class="language-plaintext highlighter-rouge">logout</code> method.<br />
And that’s about it. How it actually <em>works</em> is a little bit more complicated. Although the basic service is simple enough, there are some nasty hairy bits. It also seems simpler than it is because by now I am building on top of quite a number of nifty utility classes that make things a lot simpler. I won’t explain every little detail because that would take an even longer article (and this one is already absurdly long and will most likely only be read by LLM training scrapers in its entirety), but at least I will take you through the flow of the code. But be sure to follow up the code into its veins because there’s some handy stuff in here, other than the Entra authentication alone.</p>

<h2 id="start-of-the-service">Start of the service</h2>

<p>The top of the service class contains all the stuff we need, including some things Microsoft requires us to use and know. Don’t worry too much about the why - this is what you need and how it’s done. The constructor just takes the arguments we have seen, fills in some derivatives, and then waits for the user to start an authentication flow.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">class</span> <span class="nx">EntraDeviceCodeFlowAuthenticationService</span> <span class="k">implements</span> <span class="nx">IEntraDeviceCodeFlowAuthenticationService</span> <span class="p">{</span>
    <span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="nx">OAUTH_PATH</span><span class="p">:</span> <span class="kr">string</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">oauth2/v2.0</span><span class="dl">"</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="nx">ENTRA_HOST</span><span class="p">:</span> <span class="kr">string</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">https://login.microsoftonline.com</span><span class="dl">"</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="nx">CONTENT_TYPE_FORM</span><span class="p">:</span> <span class="kr">string</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">application/x-www-form-urlencoded</span><span class="dl">"</span><span class="p">;</span>

    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">http</span><span class="p">:</span> <span class="nx">InternetModule</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">LensStudio:InternetModule</span><span class="dl">"</span><span class="p">);</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">verbose</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">clientId</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">authority</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">scope</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="k">private</span> <span class="nx">active</span><span class="p">:</span> <span class="nx">boolean</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
    <span class="k">private</span> <span class="nx">backoff</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
    <span class="k">private</span> <span class="nx">awaitableSleep</span><span class="p">:</span> <span class="nx">AwaitableSleep</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AwaitableSleep</span><span class="p">();</span> 
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">tokenStore</span><span class="p">:</span> <span class="nx">ITokenStore</span><span class="p">;</span>

    <span class="kd">constructor</span><span class="p">(</span><span class="nx">tenant</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">clientId</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">tokenStore</span><span class="p">:</span> <span class="nx">ITokenStore</span><span class="p">,</span> <span class="nx">verbose</span><span class="p">:</span> <span class="nx">boolean</span> <span class="o">=</span> <span class="kc">false</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">clientId</span> <span class="o">=</span> <span class="nx">clientId</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">verbose</span> <span class="o">=</span> <span class="nx">verbose</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">authority</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">EntraDeviceCodeFlowAuthenticationService</span><span class="p">.</span><span class="nx">ENTRA_HOST</span><span class="p">}</span><span class="s2">/</span><span class="p">${</span><span class="nx">tenant</span><span class="p">}</span><span class="s2">/</span><span class="p">${</span><span class="nx">EntraDeviceCodeFlowAuthenticationService</span><span class="p">.</span><span class="nx">OAUTH_PATH</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">scope</span> <span class="o">=</span> <span class="s2">`api://</span><span class="p">${</span><span class="nx">clientId</span><span class="p">}</span><span class="s2">/user_impersonation offline_access`</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">tokenStore</span> <span class="o">=</span> <span class="nx">tokenStore</span><span class="p">;</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>Note, though, <code class="language-plaintext highlighter-rouge">this.authority</code>. This is the base URL where we are going to request device codes and tokens, using the service’s <code class="language-plaintext highlighter-rouge">postAsync</code> method.</p>

<h2 id="a-little-aside">A little aside</h2>

<p>I would like to draw your attention to <code class="language-plaintext highlighter-rouge">AwaitableSleep</code>. Coming from Unity and C#, I really like to use something like <code class="language-plaintext highlighter-rouge">Task.Delay</code> to wait without having to deal with the TypeScript promises baloney, so I <em>made</em> something that works like this.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">class</span> <span class="nx">AwaitableSleep</span> <span class="p">{</span>
    <span class="nl">waitToken</span><span class="p">:</span> <span class="nx">CancelToken</span> <span class="o">|</span> <span class="kc">null</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
    <span class="k">public</span> <span class="nx">sleep</span><span class="p">(</span><span class="nx">durationSeconds</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span><span class="p">((</span><span class="nx">done</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">waitToken</span> <span class="o">=</span> <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">done</span><span class="p">(),</span> <span class="nx">durationSeconds</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">);</span>
        <span class="p">});</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="nx">cancel</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">waitToken</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">clearTimeout</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">waitToken</span><span class="p">);</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">waitToken</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If I want to wait for 5 seconds, I can simply do</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">awaitableSleep</span><span class="p">.</span><span class="nx">sleep</span><span class="p">(</span><span class="mi">5</span><span class="p">);</span>
</code></pre></div></div>

<p>and cancel it as well, from another thread, if that needs be.</p>

<h2 id="the-heart-of-the-matter-authentication">The heart of the matter: authentication</h2>

<p>It looks simple enough:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="nx">authenticate</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">AccessToken</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="kd">let</span> <span class="nx">currentToken</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">tokenStore</span><span class="p">.</span><span class="nx">getToken</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">currentToken</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="dl">"</span><span class="s2">Existing token found, checking expiration</span><span class="dl">"</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">currentToken</span><span class="p">.</span><span class="nx">isExpired</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="dl">"</span><span class="s2">Using cached access token</span><span class="dl">"</span><span class="p">);</span>
            <span class="k">return</span> <span class="nx">currentToken</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="p">{</span>
            <span class="nx">currentToken</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">refreshToken</span><span class="p">(</span><span class="nx">currentToken</span><span class="p">.</span><span class="nx">refreshToken</span><span class="p">);</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">currentToken</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">tokenStore</span><span class="p">.</span><span class="nx">setToken</span><span class="p">(</span><span class="nx">currentToken</span><span class="p">);</span>
                <span class="k">return</span> <span class="nx">currentToken</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="dl">"</span><span class="s2">No valid token available, starting new device code flow authentication</span><span class="dl">"</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">deviceCodeResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">requestDeviceCode</span><span class="p">();</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="s2">`User code: </span><span class="p">${</span><span class="nx">deviceCodeResponse</span><span class="p">.</span><span class="nx">user_code</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">UserActionRequiredEvent</span><span class="p">.</span><span class="nx">invoke</span><span class="p">(</span><span class="s2">`Please go to </span><span class="p">${</span><span class="nx">deviceCodeResponse</span><span class="p">.</span><span class="nx">verification_uri</span><span class="p">}</span><span class="s2"> and enter code: </span><span class="p">${</span><span class="nx">deviceCodeResponse</span><span class="p">.</span><span class="nx">user_code</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="nx">currentToken</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">pollForToken</span><span class="p">(</span><span class="nx">deviceCodeResponse</span><span class="p">.</span><span class="nx">device_code</span><span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">tokenStore</span><span class="p">.</span><span class="nx">setToken</span><span class="p">(</span><span class="nx">currentToken</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">currentToken</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<ul>
  <li>Does the token store have a token and is it not expired? We are done! Return the token.</li>
  <li>If there is a token in the store but it is expired, refresh it and return it (and store it for next time).</li>
  <li>If there was no token, get a device code.</li>
  <li>Start polling for a token.</li>
  <li>If we get a token, we store it, then return it.</li>
</ul>

<h2 id="requesting-a-device-code">Requesting a device code</h2>

<p>The first time, there won’t be a token at all, of course. So we end up at the bottom of <code class="language-plaintext highlighter-rouge">authenticate</code> and have to get a device token first:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">async</span> <span class="nx">requestDeviceCode</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">IDeviceCodeResponse</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="na">result</span><span class="p">:</span> <span class="nx">IHttpResult</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">postAsync</span><span class="p">(</span><span class="dl">"</span><span class="s2">devicecode</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">client_id</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">clientId</span><span class="p">,</span>
        <span class="na">scope</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">scope</span><span class="p">,</span>
    <span class="p">});</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">statusCode</span> <span class="o">!==</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nx">TokenRequestError</span><span class="p">(</span>
            <span class="s2">`Device code request returned HTTP </span><span class="p">${</span><span class="nx">result</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
            <span class="nx">result</span><span class="p">.</span><span class="nx">body</span>
        <span class="p">);</span>
    <span class="p">}</span>

    <span class="kd">const</span> <span class="nx">parsed</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span> <span class="k">as</span> <span class="nx">IDeviceCodeResponse</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="s2">`Received user code: </span><span class="p">${</span><span class="nx">parsed</span><span class="p">.</span><span class="nx">user_code</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">parsed</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Curiously we do this by HTTP POST (while we are basically <em>getting</em> something, but this is apparently how it’s done). It returns a structure containing a <code class="language-plaintext highlighter-rouge">device_code</code> and a <code class="language-plaintext highlighter-rouge">verification_code</code>, which we need in the next step.</p>

<h2 id="polling-for-token">Polling for token</h2>

<p>So hurray, we have a device code. The user has been informed to go somewhere and log in with this code and their Entra credentials. Now we have to poll to see if Microsoft is giving their blessing and will grace our code with an actual access token. This also happens via an HTTP POST:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">async</span> <span class="nx">pollForToken</span><span class="p">(</span><span class="nx">deviceCode</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">AccessToken</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">active</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">backoff</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>

    <span class="kd">const</span> <span class="na">tokenParams</span><span class="p">:</span> <span class="nx">FormParams</span> <span class="o">=</span> <span class="p">{</span>
        <span class="na">client_id</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">clientId</span><span class="p">,</span>
        <span class="na">grant_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">urn:ietf:params:oauth:grant-type:device_code</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">device_code</span><span class="p">:</span> <span class="nx">deviceCode</span><span class="p">,</span>
    <span class="p">};</span>

    <span class="k">while</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">active</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="dl">"</span><span class="s2">Polling for token...</span><span class="dl">"</span><span class="p">);</span>
        <span class="kd">const</span> <span class="na">result</span><span class="p">:</span> <span class="nx">IHttpResult</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">postAsync</span><span class="p">(</span><span class="dl">"</span><span class="s2">token</span><span class="dl">"</span><span class="p">,</span> <span class="nx">tokenParams</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">statusCode</span> <span class="o">===</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">halt</span><span class="p">();</span>
            <span class="kd">var</span> <span class="nx">tokenResponse</span> <span class="o">=</span> <span class="nx">payload</span> <span class="k">as</span> <span class="nx">ITokenResponse</span><span class="p">;</span>
            <span class="k">return</span> <span class="nx">AccessToken</span><span class="p">.</span><span class="nx">create</span><span class="p">(</span><span class="nx">tokenResponse</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="kd">const</span> <span class="nx">tokenError</span> <span class="o">=</span> <span class="nx">payload</span> <span class="k">as</span> <span class="nx">ITokenError</span><span class="p">;</span>

        <span class="k">switch</span> <span class="p">(</span><span class="nx">tokenError</span><span class="p">.</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">case</span> <span class="dl">"</span><span class="s2">authorization_pending</span><span class="dl">"</span><span class="p">:</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="dl">"</span><span class="s2">Waiting for user to authorize...</span><span class="dl">"</span><span class="p">);</span>
                <span class="k">break</span><span class="p">;</span>
            <span class="k">case</span> <span class="dl">"</span><span class="s2">slow_down</span><span class="dl">"</span><span class="p">:</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">backoff</span> <span class="o">+=</span> <span class="mi">5</span><span class="p">;</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="s2">`Increased polling interval to </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">backoff</span><span class="p">}</span><span class="s2">s`</span><span class="p">);</span>
                <span class="k">break</span><span class="p">;</span>
            <span class="nl">default</span><span class="p">:</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">handlePollingError</span><span class="p">(</span><span class="nx">tokenError</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">awaitableSleep</span><span class="p">.</span><span class="nx">sleep</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">backoff</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nx">TokenRequestError</span><span class="p">(</span><span class="dl">"</span><span class="s2">Polling was cancelled</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>I basically poll every five seconds, or slower when requested. If we actually get a result, it’s an <code class="language-plaintext highlighter-rouge">ITokenResponse</code>, that contains these fields:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">ITokenResponse</span> <span class="p">{</span>
  <span class="nl">token_type</span><span class="p">:</span> <span class="kr">string</span>
  <span class="nx">scope</span><span class="p">:</span> <span class="kr">string</span>
  <span class="nx">expires_in</span><span class="p">:</span> <span class="kr">number</span>
  <span class="nx">access_token</span><span class="p">:</span> <span class="kr">string</span>
  <span class="nx">refresh_token</span><span class="p">?:</span> <span class="kr">string</span>
  <span class="nx">id_token</span><span class="p">?:</span> <span class="kr">string</span>
<span class="p">}</span>
</code></pre></div></div>

<p>You might notice, by the way, there’s never an <em>implementation</em> of <code class="language-plaintext highlighter-rouge">ITokenResponse</code> (or any other of the responses). Apparently this is possible in TypeScript and, as far as I understand, the ‘canonical way to do it’.</p>

<h2 id="creating-a-token-from-the-token">Creating a token from the token</h2>

<p>Anyway, <code class="language-plaintext highlighter-rouge">ITokenResponse</code>’s <code class="language-plaintext highlighter-rouge">access_token</code> field contains some encoded data we want to know as well, like the user’s ID (usually the email address) and the actual name, so we can welcome them. Also, we want to know when the token expires.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">StringUtils</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">LocalJoost/Utils/StringUtils</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ITokenResponse</span><span class="p">,</span> <span class="nx">ITokenData</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./TokenResponses</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">AccessToken</span> <span class="p">{</span>
    <span class="k">public</span> <span class="nx">accessToken</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="k">public</span> <span class="nx">refreshToken</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="k">public</span> <span class="nx">expiration</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
    <span class="k">public</span> <span class="nx">userName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="k">public</span> <span class="nx">oid</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">static</span> <span class="nx">create</span><span class="p">(</span><span class="nx">tokenResponse</span><span class="p">:</span> <span class="nx">ITokenResponse</span><span class="p">):</span> <span class="nx">AccessToken</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AccessToken</span><span class="p">();</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">accessToken</span> <span class="o">=</span> <span class="nx">tokenResponse</span><span class="p">.</span><span class="nx">access_token</span><span class="p">;</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">refreshToken</span> <span class="o">=</span> <span class="nx">tokenResponse</span><span class="p">.</span><span class="nx">refresh_token</span><span class="p">;</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">expiration</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">/</span> <span class="mi">1000</span> <span class="o">+</span> <span class="nx">tokenResponse</span><span class="p">.</span><span class="nx">expires_in</span><span class="p">;</span>
        <span class="kd">var</span> <span class="nx">tokenData</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">getTokenDataFromAccessToken</span><span class="p">(</span><span class="nx">tokenResponse</span><span class="p">.</span><span class="nx">access_token</span><span class="p">);</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">userName</span> <span class="o">=</span> <span class="nx">tokenData</span><span class="p">.</span><span class="nx">name</span><span class="p">;</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">oid</span> <span class="o">=</span> <span class="nx">tokenData</span><span class="p">.</span><span class="nx">oid</span><span class="p">;</span>
        <span class="k">return</span> <span class="nx">token</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">static</span> <span class="nx">fromJSON</span><span class="p">(</span><span class="nx">json</span><span class="p">:</span> <span class="nx">object</span><span class="p">):</span> <span class="nx">AccessToken</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AccessToken</span><span class="p">();</span>
        <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">token</span><span class="p">,</span> <span class="nx">json</span><span class="p">);</span>
        <span class="k">return</span> <span class="nx">token</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="kd">get</span> <span class="nx">isExpired</span><span class="p">():</span> <span class="nx">boolean</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">/</span> <span class="mi">1000</span> <span class="o">&gt;=</span> <span class="k">this</span><span class="p">.</span><span class="nx">expiration</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">static</span> <span class="nx">getTokenDataFromAccessToken</span><span class="p">(</span><span class="nx">accessToken</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">ITokenData</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">splitToken</span><span class="p">:</span> <span class="kr">string</span><span class="p">[]</span> <span class="o">=</span> <span class="nx">accessToken</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">.</span><span class="dl">'</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">correctedBase64</span><span class="p">:</span> <span class="kr">string</span> <span class="o">=</span> <span class="nx">StringUtils</span><span class="p">.</span><span class="nx">base64UrlToBase64String</span><span class="p">(</span><span class="nx">splitToken</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
        <span class="kd">const</span> <span class="nx">decoded</span><span class="p">:</span> <span class="kr">string</span> <span class="o">=</span> <span class="nx">StringUtils</span><span class="p">.</span><span class="nx">base64Decode</span><span class="p">(</span><span class="nx">correctedBase64</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">tokenData</span><span class="p">:</span> <span class="nx">ITokenData</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">decoded</span><span class="p">);</span>
        <span class="k">return</span> <span class="nx">tokenData</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The whole prying apart happens in <code class="language-plaintext highlighter-rouge">getTokenDataFromAccessToken</code> and that goes into all kinds of Base64 decoding and taking into account the fact that JWT access tokens are not encoded in standard Base64 but slightly differently - this method transforms it into <em>standard</em> Base64 so it can be decoded. <code class="language-plaintext highlighter-rouge">StringUtils</code> contains some nice methods for encoding and decoding strings, which are also useful in other scenarios. The <code class="language-plaintext highlighter-rouge">fromJSON</code> method is used to be able to retrieve it from storage, more about that later.</p>

<p>Anyway, at the end of <code class="language-plaintext highlighter-rouge">pollForToken</code> we have a token we can use, but that might expire. This is where the <code class="language-plaintext highlighter-rouge">refreshToken</code> method comes in, which is fairly simple:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">async</span> <span class="nx">refreshToken</span><span class="p">(</span><span class="nx">refreshToken</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">AccessToken</span> <span class="o">|</span> <span class="kc">null</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="dl">"</span><span class="s2">Refreshing access token</span><span class="dl">"</span><span class="p">);</span>
    <span class="kd">const</span> <span class="na">result</span><span class="p">:</span> <span class="nx">IHttpResult</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">postAsync</span><span class="p">(</span><span class="dl">"</span><span class="s2">token</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">client_id</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">clientId</span><span class="p">,</span>
        <span class="na">grant_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">refresh_token</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">refresh_token</span><span class="p">:</span> <span class="nx">refreshToken</span><span class="p">,</span>
        <span class="na">scope</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">scope</span><span class="p">,</span>
    <span class="p">});</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">statusCode</span> <span class="o">!==</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="s2">`Token refresh failed with HTTP </span><span class="p">${</span><span class="nx">result</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">trace</span><span class="p">(</span><span class="dl">"</span><span class="s2">Access token refreshed successfully</span><span class="dl">"</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">tokenResponse</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span> <span class="k">as</span> <span class="nx">ITokenResponse</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">AccessToken</span><span class="p">.</span><span class="nx">create</span><span class="p">(</span><span class="nx">tokenResponse</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It posts a refresh token request, gets a new token back - with a new refresh token and everything. It’s like you re-login but without you needing to do something. The fun thing is, you can just keep calling <code class="language-plaintext highlighter-rouge">authenticate</code> every time you need to access the secured API, because it will automatically retrieve a stored token, prompt the user for credentials to create a token, or refresh it -all the song and dance around the actual authentication happens behind the curtain.</p>

<h2 id="some-helper-methods">Some helper methods</h2>

<p>To make communication with the Entra authentication API a bit easier, this <code class="language-plaintext highlighter-rouge">postAsync</code> method was created.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="nx">postAsync</span><span class="p">(</span><span class="nx">endpoint</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">params</span><span class="p">:</span> <span class="nx">FormParams</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">IHttpResult</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="na">httpRequest</span><span class="p">:</span> <span class="nx">RemoteServiceHttpRequest</span> <span class="o">=</span> <span class="nx">RemoteServiceHttpRequest</span><span class="p">.</span><span class="nx">create</span><span class="p">();</span>
    <span class="nx">httpRequest</span><span class="p">.</span><span class="nx">method</span> <span class="o">=</span> <span class="nx">RemoteServiceHttpRequest</span><span class="p">.</span><span class="nx">HttpRequestMethod</span><span class="p">.</span><span class="nx">Post</span><span class="p">;</span>
    <span class="nx">httpRequest</span><span class="p">.</span><span class="nx">url</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">authority</span><span class="p">}</span><span class="s2">/</span><span class="p">${</span><span class="nx">endpoint</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
    <span class="nx">httpRequest</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="nx">FormParamUtils</span><span class="p">.</span><span class="nx">serializeForm</span><span class="p">(</span><span class="nx">params</span><span class="p">);</span>
    <span class="nx">httpRequest</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">,</span> <span class="nx">EntraDeviceCodeFlowAuthenticationService</span><span class="p">.</span><span class="nx">CONTENT_TYPE_FORM</span><span class="p">);</span>

    <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">IHttpResult</span><span class="o">&gt;</span><span class="p">((</span><span class="nx">resolve</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">http</span><span class="p">.</span><span class="nx">performHttpRequest</span><span class="p">(</span><span class="nx">httpRequest</span><span class="p">,</span> <span class="p">(</span><span class="nx">resp</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nx">resolve</span><span class="p">({</span> <span class="na">statusCode</span><span class="p">:</span> <span class="nx">resp</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">,</span> <span class="na">body</span><span class="p">:</span> <span class="nx">resp</span><span class="p">.</span><span class="nx">body</span> <span class="p">});</span>
        <span class="p">});</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This saves the calling code a lot of plumbing for creating three HTTP POST requests (device code, poll, and refresh) and the interpretation of the result. It also uses the <code class="language-plaintext highlighter-rouge">FormParams</code> type and a helper method to make it easier to build an HTTP body payload.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">type</span> <span class="nx">FormParams</span> <span class="o">=</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="kr">string</span><span class="o">&gt;</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">FormParamUtils</span> <span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="nx">serializeForm</span><span class="p">(</span><span class="nx">params</span><span class="p">:</span> <span class="nx">FormParams</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">parts</span><span class="p">:</span> <span class="kr">string</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">key</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">params</span><span class="p">))</span> <span class="p">{</span>
            <span class="nx">parts</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">key</span><span class="p">}</span><span class="s2">=</span><span class="p">${</span><span class="nb">encodeURIComponent</span><span class="p">(</span><span class="nx">params</span><span class="p">[</span><span class="nx">key</span><span class="p">])}</span><span class="s2">`</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nx">parts</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">&amp;</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="persistence-and-storage">Persistence and storage</h2>

<p>Of course, if you have to log in this way every time you start the lens, it kind of defies the purpose. So once you have logged in, there needs to be a form of persistence. This is where an <code class="language-plaintext highlighter-rouge">ITokenStore</code> comes into play. The <code class="language-plaintext highlighter-rouge">EntraDeviceCodeFlowAuthenticationService</code> simply defers storing and retrieving to an object that implements a very simple interface:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">interface</span> <span class="nx">ITokenStore</span> <span class="p">{</span>
    <span class="nx">getToken</span><span class="p">():</span> <span class="nx">AccessToken</span> <span class="o">|</span> <span class="kc">null</span><span class="p">;</span>
    <span class="nx">setToken</span><span class="p">(</span><span class="nx">token</span><span class="p">:</span> <span class="nx">AccessToken</span><span class="p">):</span> <span class="k">void</span><span class="p">;</span>
    <span class="nx">clearToken</span><span class="p">():</span> <span class="k">void</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To demonstrate the principle, I have created a very simple implementation that stores the <code class="language-plaintext highlighter-rouge">AccessToken</code> as JSON in Spectacles’ persistent storage system:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">AccessToken</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./AccessToken</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ITokenStore</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./ITokenStore</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">PersistentStorageTokenStore</span> <span class="k">implements</span> <span class="nx">ITokenStore</span> <span class="p">{</span>
    <span class="k">private</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">GeneralDataStore</span> <span class="o">=</span> <span class="nb">global</span><span class="p">.</span><span class="nx">persistentStorageSystem</span><span class="p">.</span><span class="nx">store</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">tokenKey</span><span class="p">:</span> <span class="kr">string</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">access_token</span><span class="dl">"</span><span class="p">;</span>

    <span class="nx">getToken</span><span class="p">():</span> <span class="nx">AccessToken</span> <span class="o">|</span> <span class="kc">null</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">getString</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">tokenKey</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nx">AccessToken</span><span class="p">.</span><span class="nx">fromJSON</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">token</span><span class="p">));</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">setToken</span><span class="p">(</span><span class="nx">token</span><span class="p">:</span> <span class="nx">AccessToken</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">putString</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">tokenKey</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">token</span><span class="p">));</span>
    <span class="p">}</span>

    <span class="nx">clearToken</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">tokenKey</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This works fine, but you have to take one important issue into consideration. As long as the refresh token is valid, <em>the lens will log in with your credentials, regardless of who wears the device</em>, until you log out. And Spectacles <em>itself</em> does not have login or authentication we can use to protect it. So if your Spectacles get nicked, the thief can use the lens on your behalf and access your data. This is no different than someone stealing your Xbox where you logged in to. For an actual enterprise deployment, it behooves to think a bit about securing the login in a simple way, like with a PIN code or something. And store the data encrypted, although I wonder how useful that is, because as far as I know, there is no way to access the data in a lens’ persistent storage by anything but the lens itself. However, your Compliance Officer might get very nervous about it. I might write a better <code class="language-plaintext highlighter-rouge">ITokenStore</code> fix in a follow-up post - this one is long enough as it goes.</p>

<h2 id="demo-code">Demo code</h2>

<p>I have added an EntraSampleUIWindow prefab that is a very simple UI and some code to actually start the authentication, to prove that it works. It can be configured with a simple test API URL that proves that it can actually call the secured URL.</p>

<p><img src="/assets/2026-03-21-Microsoft-Entra-authentication-using-Device-Code-Flow-for-Snap-Spectacles/testscreen.png" alt="testscreen" /></p>

<p>To show it all works, I have written the following demo code in the equally named <code class="language-plaintext highlighter-rouge">EntraSampleUIWindow</code> component that shows the following things happening in your lens:</p>

<p><img src="/assets/2026-03-21-Microsoft-Entra-authentication-using-Device-Code-Flow-for-Snap-Spectacles/demoloop.gif" alt="demoloop" /></p>

<p>The code looks like this:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">async</span> <span class="nx">demoAuthentication</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">entraService</span><span class="p">.</span><span class="nx">UserActionRequiredEvent</span><span class="p">.</span><span class="nx">add</span><span class="p">((</span><span class="na">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
        <span class="p">});</span>
        <span class="kd">var</span> <span class="na">token</span><span class="p">:</span> <span class="nx">AccessToken</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">entraService</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">();</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">token</span><span class="p">.</span><span class="nx">userName</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">awaitableSleep</span><span class="p">.</span><span class="nx">sleep</span><span class="p">(</span><span class="mi">4</span><span class="p">);</span>

        <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="s2">`Token valid until </span><span class="p">${</span><span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">token</span><span class="p">.</span><span class="nx">expiration</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)}</span><span class="s2">`</span><span class="p">);</span>
        <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">awaitableSleep</span><span class="p">.</span><span class="nx">sleep</span><span class="p">(</span><span class="mi">4</span><span class="p">);</span>

        <span class="kd">const</span> <span class="na">httpRequest</span><span class="p">:</span> <span class="nx">RemoteServiceHttpRequest</span> <span class="o">=</span> <span class="nx">RemoteServiceHttpRequest</span><span class="p">.</span><span class="nx">create</span><span class="p">();</span>
        <span class="nx">httpRequest</span><span class="p">.</span><span class="nx">method</span> <span class="o">=</span> <span class="nx">RemoteServiceHttpRequest</span><span class="p">.</span><span class="nx">HttpRequestMethod</span><span class="p">.</span><span class="nx">Get</span><span class="p">;</span>
        <span class="nx">httpRequest</span><span class="p">.</span><span class="nx">url</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">testApiUrl</span><span class="p">;</span>
        <span class="nx">httpRequest</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">Authorization</span><span class="dl">"</span><span class="p">,</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">token</span><span class="p">.</span><span class="nx">accessToken</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">http</span><span class="p">.</span><span class="nx">performHttpRequest</span><span class="p">(</span><span class="nx">httpRequest</span><span class="p">,</span> <span class="p">(</span><span class="nx">resp</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="s2">`Test URL status code: </span><span class="p">${</span><span class="nx">resp</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="p">});</span>
        <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">awaitableSleep</span><span class="p">.</span><span class="nx">sleep</span><span class="p">(</span><span class="mi">4</span><span class="p">);</span>

        <span class="c1">// Intentionally expire the token to demonstrate refresh flow</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">expiration</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">;</span>
        <span class="kd">var</span> <span class="nx">newToken</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">entraService</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">();</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="s2">`Token refreshed. New expiration time: </span><span class="p">${</span><span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">newToken</span><span class="p">.</span><span class="nx">expiration</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)}</span><span class="s2">`</span><span class="p">);</span>
        <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">awaitableSleep</span><span class="p">.</span><span class="nx">sleep</span><span class="p">(</span><span class="mi">4</span><span class="p">);</span>

        <span class="nx">httpRequest</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">Authorization</span><span class="dl">"</span><span class="p">,</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">newToken</span><span class="p">.</span><span class="nx">accessToken</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">http</span><span class="p">.</span><span class="nx">performHttpRequest</span><span class="p">(</span><span class="nx">httpRequest</span><span class="p">,</span> <span class="p">(</span><span class="nx">resp</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="s2">`Test URL status code after refresh: </span><span class="p">${</span><span class="nx">resp</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="p">});</span>

        <span class="k">this</span><span class="p">.</span><span class="nx">button</span><span class="p">.</span><span class="nx">sceneObject</span><span class="p">.</span><span class="nx">enabled</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">showMessage</span><span class="p">(</span><span class="s2">`Authentication failed: </span><span class="p">${</span><span class="nx">e</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<ul>
  <li>First, it subscribes to the <code class="language-plaintext highlighter-rouge">UserActionRequiredEvent</code>.</li>
  <li>Then it waits for authentication, using the whole rigmarole of code we have just seen.</li>
  <li>It shows a welcome text with the user’s name that we have plucked out of the access token.</li>
  <li>4 seconds later it tries to call the test API and shows the result.</li>
  <li>Then it intentionally messes up the token’s expiration date to force a refresh. If you have the debug on in the editor, you can actually see this working.</li>
  <li>It calls the test API again to check that still works.</li>
  <li>In the final step, it enables the logout button.</li>
</ul>

<p>If you now stop the lens, and start it again (whether on Spectacles or on Lens Studio) you will not see the device flow prompt, but it will immediately welcome you, because it pulled the token from the persistent store. However, if you press the logout button, it will clear the token persistent store. The very simple code for that sits in the <code class="language-plaintext highlighter-rouge">onAwake</code> method of the <code class="language-plaintext highlighter-rouge">EntraSampleUIWindow</code> component:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="p">.</span><span class="nx">button</span><span class="p">.</span><span class="nx">sceneObject</span><span class="p">.</span><span class="nx">enabled</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">button</span><span class="p">.</span><span class="nx">onTriggerDown</span><span class="p">.</span><span class="nx">add</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">entraService</span><span class="p">.</span><span class="nx">logout</span><span class="p">();</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">demoAuthentication</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>

<p>It starts the authentication again after logging out, thus showing you the device code prompt again, proving you have indeed logged out.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Although it’s not quite the intended use case for Spectacles, I am pretty sure lots of enterprises are keeping a keen eye on whatever Snap is doing. As Snap founder <a href="https://www.linkedin.com/in/evan-spiegel/">Evan Spiegel</a> indicated on stage at Lens Fest 2025 during Q&amp;A: Snap regularly gets (and denies) requests for Spectacles bulk purchasing from companies, but he was not surprised by getting those requests, because “there are not very many compelling devices on the market, and most of them are probably in this room”. This was a very valid point, and it became even more valid recently: we already knew HoloLens 2 is being deprecated without a successor, but now also Magic Leap has thrown in the towel for Magic Leap 2. So this makes me wonder what will happen when the 2026 commercial version of Spectacles will come to market. I know it’s possible to integrate Spectacles with enterprise services - and so do you now.</p>

<p>“Spiegel” means “mirror” in Dutch. I wonder what future for XR Mr. Spiegel has seen in his (magic) mirror. We will learn soon enough later this year.</p>

<p>Code, as always,<a href="https://github.com/LocalJoost/SpecEntraAuthService"> can be found at GitHub</a></p>]]></content><author><name>Joost van Schaik (&apos;LocalJoost&apos;)</name></author><category term="Spectacles" /><category term="Lens Studio" /><category term="TypeScript" /><category term="Entra" /><category term="Azure" /><category term="Microsoft" /><summary type="html"><![CDATA[While Snap Spectacles are clearly targeted towards consumers, and Snap have partnered with Supabase as a preferred cloud backend, there are of course always nitwits who are going against the grain and try to use a consumer device to talk to enterprise-grade backends. This tendency is especially present in a particularly rare subclass of those nitwits who actually come from an enterprise programming background and kind of accidentally stumbled into Mixed Reality. This here nitwit set out to make Snap Spectacles authenticate with Microsoft Entra as a kind of what he thought to be a fun experiment, succeeded at doing so, and then of course publishes it in case there are other nitwits around who would like to do the same thing.]]></summary></entry><entry><title type="html">MRTK3 apps on Apple Vision Pro - fixing the stuck to your head issue (and more)</title><link href="https://localjoost.github.io/MRTK3-apps-on-Apple-Vision-Pro-fixing-the-stuck-to-your-head-issue-(and-more)/" rel="alternate" type="text/html" title="MRTK3 apps on Apple Vision Pro - fixing the stuck to your head issue (and more)" /><published>2026-01-04T00:00:00+01:00</published><updated>2026-01-04T00:00:00+01:00</updated><id>https://localjoost.github.io/MRTK3-apps-on-Apple-Vision-Pro--fixing-the-stuck-to-your-head-issue-(and-more)</id><content type="html" xml:base="https://localjoost.github.io/MRTK3-apps-on-Apple-Vision-Pro-fixing-the-stuck-to-your-head-issue-(and-more)/"><![CDATA[<p>Ah, Unity. The company that was instrumental in me being able to venture into Mixed Reality, the very embodiment of the Silicon Valley motto “move fast and break things”… with unfortunately, <em>emphasis on the second part</em>.</p>

<p>Early HoloLens days, I learned an important lesson quickly: one of the scariest things you could do is upgrade your apps to a new Unity version - even just a point release could hose your app. As fellow Mixed Reality developer <a href="https://github.com/olcsa">Olivér Vasvári</a> noted in <a href="https://github.com/localjoost/blogcomments/issues/489">comments</a> on my <a href="https://localjoost.github.io/Adapting-MRTK3-apps-for-Apple-Vision-Pro/">August blog about running MRTK3 apps on Apple Vision Pro</a>, my solution (based on Unity 6000.0.49) was still working on 6000.0.53, but fell apart on 6000.0.62. Specifically, the XR camera view was ‘stuck’ to your head - that is, every virtual object was moving along with your head on the Vision Pro.</p>

<p><img src="/assets/2026-01-04-MRTK3-apps-on-Apple-Vision-Pro--fixing-the-stuck-to-your-head-issue-(and-more)/Headlock.gif" alt="" /></p>

<p>This kind of thing is unfortunately part of the life of a Unity Mixed Reality developer. So I set out to find if I could revive my solution. Spoiler - <em>I could</em>.</p>

<p>Starting point was <a href="https://github.com/LocalJoost/QuestBouncer/tree/AppleVisionPro">my last Vision Pro MRTK port</a>. I went for broke and did not just upgrade to the version where Oliver noticed the problem, but jumped to the newest Unity 6.3 version (which was 6000.3.2f1 at the point I wrote that).</p>

<h2 id="upgrade-project-to-63">Upgrade project to 6.3</h2>

<p>Straightforward enough: open the solution with 6000.3.2f1. This apparently needs an URP upgrade:</p>

<p><img src="/assets/2026-01-04-MRTK3-apps-on-Apple-Vision-Pro--fixing-the-stuck-to-your-head-issue-(and-more)/upgradeurp.png" alt="" /></p>

<p>Click OK, and then you will be greeted by another issue:</p>

<p><img src="/assets/2026-01-04-MRTK3-apps-on-Apple-Vision-Pro--fixing-the-stuck-to-your-head-issue-(and-more)/package_error.png" alt="" /></p>

<p>Click open Package Manager and you will notice my hacked 2.2.4 Vision OS XR Plugin is no longer accepted.</p>

<p><img src="/assets/2026-01-04-MRTK3-apps-on-Apple-Vision-Pro--fixing-the-stuck-to-your-head-issue-(and-more)/hackedpackage.png" alt="" /></p>

<p>Uninstall it, and Unity will proceed to install the newest Vision Pro packages:</p>

<p><img src="/assets/2026-01-04-MRTK3-apps-on-Apple-Vision-Pro--fixing-the-stuck-to-your-head-issue-(and-more)/newvisonpropackages.png" alt="" /></p>

<h2 id="upgrade-mrtk3-optional">Upgrade MRTK3 (optional)</h2>

<p>In <a href="https://localjoost.github.io/Adapting-MRTK3-apps-for-Apple-Vision-Pro/">my previous Vision Pro post</a> I used MRTK3 4.0.0-pre.1, but in the meantime 4.0.0-pre.2 has been released. It would be a shame not to use all the good work done by <a href="https://www.linkedin.com/in/kurtie/">Kurtis Eveleigh</a>, the proud MRTK custodian. The MRTK Feature Tool has been abandoned, and never worked on the Mac anyway, so the only way to upgrade I can think of is to <a href="https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/releases/tag/core-v4.0.0-pre.2">download the packages manually</a>, and put them in the Packages/MixedReality folder. The quickest way to effectuate the upgrade is to open the manifest.json in a text editor, do a search &amp; replace on pre.1, and replace it with pre.2. You can then, of course, remove the pre.1 tgz files.</p>

<p>Unity will then popup this warning:</p>

<p><img src="/assets/2026-01-04-MRTK3-apps-on-Apple-Vision-Pro--fixing-the-stuck-to-your-head-issue-(and-more)/warningsig.png" alt="" /></p>

<p>This you can safely ignore.</p>

<h2 id="fix-nothing-visible">Fix nothing visible</h2>

<p>The app can now be deployed on the Vision Pro, but when you run it, you will most likely see nothing. That is because the cubes are created 1.6m above your head. To fix this, you will need to change the Y value of the Camera Offset in the MRTK XR Rig to 0.</p>

<p><img src="/assets/2026-01-04-MRTK3-apps-on-Apple-Vision-Pro--fixing-the-stuck-to-your-head-issue-(and-more)/yoffset.png" alt="" /></p>

<p>This is normally not a problem, but apparently it is now. If it’s Unity that does not like this, the Vision Pro packages, or something else, I don’t know.</p>

<h2 id="fix-view-being-stuck-to-camera">Fix view being stuck to camera</h2>

<p>If you now run the app, the cubes will appear all right, but if you move your head, all the cubes move with your head instead of hanging in space after the initial spawn, as displayed on the gif a the start of this post. This is one of the most peculiar changes we need to make:</p>

<p><img src="/assets/2026-01-04-MRTK3-apps-on-Apple-Vision-Pro--fixing-the-stuck-to-your-head-issue-(and-more)/trackedposedriver.png" alt="" /></p>

<p>As you can see, I have disabled the original Tracked Pose Driver and added a second one. This one has some hard-coded Action definitions in stead of an Action <em>References</em>. I got this wisdom by studying the <a href="https://drive.google.com/drive/folders/1Oe-6bBCCmk7okbK832HWiYFbM8mV0XrZ">Vision OS template v 3.0.2</a>. Why I didn’t use Action References, and made a new Input Actions asset, like the default MRTK Default Input Actions, tailored for Vision Pro? Believe me, <em>I tried</em>, but either I don’t understand those Input Action assets correctly, or it simply doesn’t work in the Unity 6.3/Vision Pro combo. However, <em>this</em> works. But this leads to a new problem.</p>

<h2 id="fix-cubes-appearing-on-floor">Fix cubes appearing on floor</h2>

<p>The cubes are now no longer stuck to your head, but instead of appearing in the view, they appear on the <em>floor</em> and even partially <em>in</em> the floor, with only one or two rows visible. The rest is invisible due to occlusion being applied to the Spatial Map. This issue, although seemingly very simple, took me quite some time to figure out. My best guess is: whatever layer Unity is putting on top of the Apple stuff - it apparently needs some time to initialize and figure out where the headset actually is before it properly sets the main camera’s transform position.</p>

<p>The simplest way to fix that was to change my own startup code in <code class="language-plaintext highlighter-rouge">CubeManager</code> from:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">void</span> <span class="nf">Start</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">audioSource</span> <span class="p">=</span> <span class="n">GetComponent</span><span class="p">&lt;</span><span class="n">AudioSource</span><span class="p">&gt;();</span>
    <span class="nf">CreateGrid</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>to</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">Start</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">audioSource</span> <span class="p">=</span> <span class="n">GetComponent</span><span class="p">&lt;</span><span class="n">AudioSource</span><span class="p">&gt;();</span>
    <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Delay</span><span class="p">(</span><span class="m">1000</span><span class="p">);</span>
    <span class="nf">CreateGrid</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Simply wait a second till the Unity Player get it’s act together. I don’t know if this is optimal, but for my demo app, it does the trick.</p>

<h2 id="fix-hand-menu">Fix hand menu</h2>

<p>Now the only thing missing is the hand menu - that does not appear when you hold up your hand. The root cause is still the same: Vision Pro apparently does not track your <em>palm</em>, and that’s what the hand menu (and other things in the MRTK) apparently rely on. So I went back to the trick <a href="https://www.linkedin.com/in/guillaume-vauclin-51592021/">Guillaume Vauclin from EADS, France</a> created - adapt the code of the VisionOSHandProvider.cs file so that it no longer reports the palm cannot be tracked, but returns the wrist <code class="language-plaintext highlighter-rouge">Pose</code> instead. There is only one thing - I could not get Unity to accept a hacked version of the Apple Vision OS XR Plugin with a changed file inside it. So I went back to an old trick <a href="https://localjoost.github.io/Fixing-hand-models-spawning-when-hand-tracking-is-lost-in-MRTK-28x/">I used in November 2022 for patching the MRTK2</a> - replace the file in the Library folder after Unity has unpacked all the library files.</p>

<p>You will find the fixed VisionOSHandProvider.cs in the Patches folder, together with a small shell script I created (actually I asked Claude to write it) that simply takes all C# files in the current folder, finds the location of each correspondingey named file in the Library folder, and replaces it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># Get the current directory</span>
<span class="nv">CURRENT_DIR</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span><span class="s2">"</span>
<span class="nv">LIBRARY_DIR</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span><span class="s2">/../Library"</span>

<span class="c"># Check if Library directory exists</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-d</span> <span class="s2">"</span><span class="nv">$LIBRARY_DIR</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"Error: </span><span class="nv">$LIBRARY_DIR</span><span class="s2"> directory not found"</span>
    <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Find all C# files in current directory</span>
<span class="k">for </span>cs_file <span class="k">in</span> <span class="k">*</span>.cs<span class="p">;</span> <span class="k">do</span>
    <span class="c"># Skip if no .cs files found</span>
    <span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$cs_file</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"No C# files found in current directory"</span>
        <span class="nb">exit </span>0
    <span class="k">fi
    
    </span><span class="nb">echo</span> <span class="s2">"Processing: </span><span class="nv">$cs_file</span><span class="s2">"</span>
    
    <span class="c"># Find all matching files in Library and subdirectories</span>
    <span class="k">while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> target_file<span class="p">;</span> <span class="k">do
        </span><span class="nb">echo</span> <span class="s2">"  Replacing: </span><span class="nv">$target_file</span><span class="s2">"</span>
        <span class="nb">cp</span> <span class="s2">"</span><span class="nv">$cs_file</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$target_file</span><span class="s2">"</span>
    <span class="k">done</span> &lt; &lt;<span class="o">(</span>find <span class="s2">"</span><span class="nv">$LIBRARY_DIR</span><span class="s2">"</span> <span class="nt">-type</span> f <span class="nt">-name</span> <span class="s2">"</span><span class="nv">$cs_file</span><span class="s2">"</span><span class="o">)</span>
<span class="k">done

</span><span class="nb">echo</span> <span class="s2">"Done!"</span>
</code></pre></div></div>

<p>Make sure the project is opened by Unity at least once, wait until it is fully done loading and installing all packages. Then open the Patches folder in Terminal, and run the following commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod</span> +x replace_files.sh
./replace_files.sh
</code></pre></div></div>

<p>The first command you will need to run only the first time. Also, remember to run this command in your CI build scripts. In my previous jobs, I wrote CI scripts that did the following steps when a patch like this was needed:</p>
<ul>
  <li>Pull code</li>
  <li>Open in Unity
    <ul>
      <li>basically do nothing</li>
      <li>quit</li>
    </ul>
  </li>
  <li>Run patch script</li>
  <li>Open in Unity again
    <ul>
      <li>run tests</li>
      <li>perform actual build</li>
      <li>etc</li>
    </ul>
  </li>
</ul>

<h2 id="concluding-words">Concluding words</h2>

<p>It’s remarkable how flexible and malleable MRTK3 is - I have got it to run on all Mixed Reality headsets I managed to get my hands on, with minor tweaks. This makes for a very consistent experience and retains as much of your investments in Mixed Reality as possible. Which is an important thing, given the HoloLens 2 deprecation.</p>

<p>Updated QuestBouncer project for Apple Vision Pro can <a href="https://github.com/LocalJoost/QuestBouncer/tree/AppleVisionPro_Unity63">be found here</a>.</p>]]></content><author><name>Joost van Schaik (&apos;LocalJoost&apos;)</name></author><category term="Apple" /><category term="MRTK3" /><category term="Unity" /><category term="Vision Pro" /><summary type="html"><![CDATA[Ah, Unity. The company that was instrumental in me being able to venture into Mixed Reality, the very embodiment of the Silicon Valley motto “move fast and break things”… with unfortunately, emphasis on the second part.]]></summary></entry><entry><title type="html">Controlling a Lens Studio UIKit button’s Interactable behavior</title><link href="https://localjoost.github.io/Controlling-a-Lens-Studio-UIKit-button's-Interactable-behavior/" rel="alternate" type="text/html" title="Controlling a Lens Studio UIKit button’s Interactable behavior" /><published>2025-12-22T00:00:00+01:00</published><updated>2025-12-22T00:00:00+01:00</updated><id>https://localjoost.github.io/Controlling-a-Lens-Studio-UIKit-button&apos;s-Interactable-behavior</id><content type="html" xml:base="https://localjoost.github.io/Controlling-a-Lens-Studio-UIKit-button&apos;s-Interactable-behavior/"><![CDATA[<p>In ye olden days - in Lens Studio time units, this is about 8 weeks ago - we used a PinchButton from the Spectacles Interaction Kit to create a button on some kind of menu. This was - and still is - a pretty full-featured component, but all those options made it sometimes a bit hard to use. Enter UIKit, which makes it a lot simpler. You just, for instance, add a UIKit <code class="language-plaintext highlighter-rouge">RectangleButton</code>, <code class="language-plaintext highlighter-rouge">RoundButton</code>, or <code class="language-plaintext highlighter-rouge">CapsuleButton</code> script to a SceneObject, add a text or an icon to it, and you’re done. One button, ready to run.</p>

<p><img src="/assets/2025-12-22-Controlling-a-Lens-Studio-UIKit-button's-Interactable-behavior/rectanglebutton.png" alt="rectanglebutton" /></p>

<p>Unfortunately, we also lost a bit. For instance, we lost sound effects. In addition, the PinchButton contained an <code class="language-plaintext highlighter-rouge">Interactable</code>, which allowed us to control the button’s behavior in quite some detail:</p>

<p><img src="/assets/2025-12-22-Controlling-a-Lens-Studio-UIKit-button's-Interactable-behavior/interactable.png" alt="interactable" /></p>

<p>However, it turns out that the UIKit button scripts are quite busybodies. If you select the “Inspect Preview” button on your preview panel, you’ll see what’s actually going on behind the scenes:</p>

<p><img src="/assets/2025-12-22-Controlling-a-Lens-Studio-UIKit-button's-Interactable-behavior/behindthescenes.png" alt="behindthescenes" /></p>

<p>Aha! In fact, most of what the three UIKit buttons do is create all the components you need to make a functioning button <em>on the fly</em>, including an <code class="language-plaintext highlighter-rouge">Interactable</code>, to save you a lot of work. This, now, we can use to our advantage!</p>

<p>As you can see on the <code class="language-plaintext highlighter-rouge">MyButton</code> prefab I made in the <a href="https://github.com/LocalJoost/DynamicScrollMenu">demo project for the scrollable menu</a>, there is an <code class="language-plaintext highlighter-rouge">UIKitButtonController</code> script added below the <code class="language-plaintext highlighter-rouge">RectangleButton</code>. Not coincidentally, this has exactly the same properties as an <code class="language-plaintext highlighter-rouge">Interactable</code>.</p>

<p><img src="/assets/2025-12-22-Controlling-a-Lens-Studio-UIKit-button's-Interactable-behavior/UIKitButtonController.png" alt="UIKitButtonController" /></p>

<p>This is a little script I created. Actually, most of it I just nicked from <code class="language-plaintext highlighter-rouge">Interactable</code> itself: all the properties and UI widget declarations. The only part I added was this:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">onAwake</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">tryGetInteractable</span><span class="p">();</span>
<span class="p">}</span>

<span class="k">private</span> <span class="nx">tryGetInteractable</span><span class="p">(</span><span class="nx">attempts</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="mi">0</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">interactable</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">getSceneObject</span><span class="p">().</span><span class="nx">getComponent</span><span class="p">(</span><span class="nx">Interactable</span><span class="p">.</span><span class="nx">getTypeName</span><span class="p">())</span> <span class="k">as</span> <span class="nx">Interactable</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">interactable</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">updateInteractableProperties</span><span class="p">(</span><span class="nx">interactable</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">attempts</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Max 10 attempts</span>
        <span class="kd">const</span> <span class="nx">delayedEvent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">createEvent</span><span class="p">(</span><span class="dl">"</span><span class="s2">DelayedCallbackEvent</span><span class="dl">"</span><span class="p">);</span>
        <span class="nx">delayedEvent</span><span class="p">.</span><span class="nx">bind</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">tryGetInteractable</span><span class="p">(</span><span class="nx">attempts</span> <span class="o">+</span> <span class="mi">1</span><span class="p">));</span>
        <span class="nx">delayedEvent</span><span class="p">.</span><span class="nx">reset</span><span class="p">(</span><span class="mf">0.1</span><span class="p">);</span> <span class="c1">// Wait 100ms between attempts</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nx">print</span><span class="p">(</span><span class="dl">"</span><span class="s2">UIKitButtonController: Failed to find Interactable component after 10 attempts</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">private</span> <span class="nx">updateInteractableProperties</span><span class="p">(</span><span class="nx">interactable</span><span class="p">:</span> <span class="nx">Interactable</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">targetingMode</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">targetingMode</span><span class="p">;</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">targetingVisual</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">targetingVisual</span><span class="p">;</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">ignoreInteractionPlane</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">ignoreInteractionPlane</span><span class="p">;</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">keepHoverOnTrigger</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">keepHoverOnTrigger</span><span class="p">;</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">enableInstantDrag</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">enableInstantDrag</span><span class="p">;</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">isScrollable</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">isScrollable</span><span class="p">;</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">allowMultipleInteractors</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">allowMultipleInteractors</span><span class="p">;</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">enablePokeDirectionality</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">enablePokeDirectionality</span><span class="p">;</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">acceptableXDirections</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">acceptableXDirections</span><span class="p">;</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">acceptableYDirections</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">acceptableYDirections</span><span class="p">;</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">acceptableZDirections</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">acceptableZDirections</span><span class="p">;</span>
    <span class="nx">interactable</span><span class="p">.</span><span class="nx">useFilteredPinch</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">useFilteredPinch</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>After <code class="language-plaintext highlighter-rouge">onAwake</code>, it tries ten times within a second to see if the button script has already finished creating the <code class="language-plaintext highlighter-rouge">Interactable</code>, and if so, it transfers all the properties you set in the Inspector to the actual <code class="language-plaintext highlighter-rouge">Interactable</code>. Not quite rocket science, but very useful.</p>

<p>And by the way, if you want to have the sound effects back that you got using the SIK buttons, just add an <code class="language-plaintext highlighter-rouge">Audio</code> component and a properly configured <code class="language-plaintext highlighter-rouge">InteractableAudioFeedback</code> below the button script, and it will be picked up automatically by the <code class="language-plaintext highlighter-rouge">Interactable</code> that will be created at runtime by the UIKit button scripts, like this:</p>

<p><img src="/assets/2025-12-22-Controlling-a-Lens-Studio-UIKit-button's-Interactable-behavior/addsounds.png" alt="addsounds" /></p>

<p>All code and samples are already in <a href="https://github.com/LocalJoost/DynamicScrollMenu">the demo project used in my previous two posts</a>, so I’m not going to add a specific new project for this. You can simply nick the <code class="language-plaintext highlighter-rouge">UIKitButtonController</code> script from there.</p>]]></content><author><name>Joost van Schaik (&apos;LocalJoost&apos;)</name></author><category term="Lens Studio" /><category term="Spectacles" /><category term="TypeScript" /><category term="UIKit" /><summary type="html"><![CDATA[In ye olden days - in Lens Studio time units, this is about 8 weeks ago - we used a PinchButton from the Spectacles Interaction Kit to create a button on some kind of menu. This was - and still is - a pretty full-featured component, but all those options made it sometimes a bit hard to use. Enter UIKit, which makes it a lot simpler. You just, for instance, add a UIKit RectangleButton, RoundButton, or CapsuleButton script to a SceneObject, add a text or an icon to it, and you’re done. One button, ready to run.]]></summary></entry><entry><title type="html">Dynamic data-driven scrollable button menu construction kit for Snap Spectacles part 2 - how it works</title><link href="https://localjoost.github.io/Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-2-how-it-works/" rel="alternate" type="text/html" title="Dynamic data-driven scrollable button menu construction kit for Snap Spectacles part 2 - how it works" /><published>2025-12-13T00:00:00+01:00</published><updated>2025-12-13T00:00:00+01:00</updated><id>https://localjoost.github.io/Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-2--how-it-works</id><content type="html" xml:base="https://localjoost.github.io/Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-2-how-it-works/"><![CDATA[<p>In <a href="https://localjoost.github.io/Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-1-usage/">part 1</a> I described how this component can be used, and I promised to go deeper into the details about how it worked in a follow-up post. This is that post.</p>

<h2 id="the-scrollmenu-prefab">The ScrollMenu prefab</h2>

<p>I usually build up my prefabs in such a way that the top-level SceneObject has a kind of controller script, while there is always <em>one</em> child SceneObject that holds the actual visible part (in this case, a menu). That way, I can let the controller script handle the actual display state and the way things work by calling a script method, without having to mess with the actual internal structure of the prefab, potentially turning off parts that have vital controlling scripts on it, messing up the workings of the app, and potentially creating issues that way.</p>

<p><img src="/assets/2025-12-13-Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-2--how-it-works/scrollprefab.png" alt="" /></p>

<p>The controller script in this case is called - very originally - <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code>. It features a few input fields. You can change the first three (in fact, you <em>must</em> do so with the Scroll Button Prefab field after you dragged it onto your scene, <a href="https://localjoost.github.io/Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-1-usage/">as I explained in part 1</a>). The last three should best be left undisturbed.</p>

<p>The first field is the vertical size a button uses (including padding), the second the horizontal size. The control makes buttons in two columns and as many rows as necessary. If you want more columns, you will have to adapt the code. Since you will have to press them by finger, I don’t anticipate much narrower buttons, so I guess you don’t have to change Column Size that often, but Y Offset you might. It is now tailored toward my sample button.</p>

<p>The MenuFrame component contains the <code class="language-plaintext highlighter-rouge">Frame</code> script showing the UI canvas, as well as the <code class="language-plaintext highlighter-rouge">HeadLock</code> and the <code class="language-plaintext highlighter-rouge">Billboard</code> script keeping the UI more or less in view.</p>

<p><img src="/assets/2025-12-13-Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-2--how-it-works/menuframe.png" alt="" /></p>

<p>One thing of note - if you are using <code class="language-plaintext highlighter-rouge">Billboard</code>, please remember to disable “Allow translation”, otherwise you can still grab and move the floating window, but you will more or less be fighting the <code class="language-plaintext highlighter-rouge">HeadLock</code> and the <code class="language-plaintext highlighter-rouge">Billboard</code> scripts, which is not desirable. Either the user decides where a window goes, or the system - but not both.</p>

<p><img src="/assets/2025-12-13-Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-2--how-it-works/Frame.png" alt="" /></p>

<p>Some other details:</p>
<ul>
  <li>ScrollWindowAnchor determines where on the floating screen the scroll window will appear. This you can use mostly to decide the vertical starting point, should you require to change that.</li>
  <li>ScrollWindow itself decides the actual size of the scroll area.</li>
  <li>Scrollbar determines the vertical position of the scrollbar.</li>
  <li>Slider determines the size of the scrollbar.</li>
</ul>

<p>If you change either ScrollWindowAnchor or ScrollWindow, be prepared to fiddle with Scrollbar and ScrollbarSlider until it all fits nicely together again, with sizes aligning visually, etc.</p>

<h2 id="scripts">Scripts</h2>

<p>The whole thing works using only three custom scripts:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">BaseScrollButtonData</code> (which I already <a href="">explained in the previous post</a>)</li>
  <li><code class="language-plaintext highlighter-rouge">BaseUIKitScrollButtonController</code></li>
  <li><code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code></li>
</ul>

<p>So let’s start with the easy part:</p>

<h3 id="baseuikitscrollbuttoncontroller">BaseUIKitScrollButtonController</h3>

<p>This is a fairly simple script, but still requires some explanation. Let’s start with the header and the events.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">component</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">BaseUIKitScrollButtonController</span> <span class="kd">extends</span> <span class="nx">BaseScriptComponent</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">input</span> <span class="nx">buttonText</span><span class="p">:</span> <span class="nx">Text</span><span class="p">;</span>
    <span class="p">@</span><span class="nd">input</span> <span class="nx">uiKitButton</span><span class="p">:</span> <span class="nx">BaseButton</span><span class="p">;</span>

    <span class="k">private</span> <span class="nx">onButtonPressedEvent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Event</span><span class="o">&lt;</span><span class="nx">BaseScrollButtonData</span><span class="o">&gt;</span><span class="p">();</span>
    <span class="k">public</span> <span class="k">readonly</span> <span class="nx">onButtonPressed</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">onButtonPressedEvent</span><span class="p">.</span><span class="nx">publicApi</span><span class="p">();</span>

    <span class="k">public</span> <span class="nx">onHoveredEvent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Event</span><span class="o">&lt;</span><span class="nx">boolean</span><span class="o">&gt;</span><span class="p">();</span>
    <span class="k">public</span> <span class="nx">onHovered</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">onHoveredEvent</span><span class="p">.</span><span class="nx">publicApi</span><span class="p">();</span>
</code></pre></div></div>

<p>Remember this can be used as a parent class component for your own button script. Here you can see what it does behind the curtains.</p>
<ul>
  <li>Text should contain the button’s text to be set by the data fed to this button’s <code class="language-plaintext highlighter-rouge">setButtonData</code> method (<a href="">as explained before</a>)</li>
  <li>It exposes an <code class="language-plaintext highlighter-rouge">onButtonPressed</code> event that is triggered when the button is pressed, and returns the <code class="language-plaintext highlighter-rouge">BaseScrollButtonData</code> that was used to create this button in the first place.</li>
  <li>It also exposes an event <code class="language-plaintext highlighter-rouge">onHovered</code> that tells the interested listener whether the button is hovered over by the user.</li>
</ul>

<p>Although both events are public, they are typically only used internally, by the <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code>, as will become clear later.</p>

<p>The <code class="language-plaintext highlighter-rouge">setButtonData</code> is used by <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code> to feed the actual button data to the button that is to be created:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="nx">setButtonData</span><span class="p">(</span><span class="nx">scrollButtonData</span><span class="p">:</span> <span class="nx">BaseScrollButtonData</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">uiKitButton</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">uiKitButton</span><span class="p">.</span><span class="nx">onHoverEnter</span><span class="p">.</span><span class="nx">add</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">onHoveredEvent</span><span class="p">.</span><span class="nx">invoke</span><span class="p">(</span><span class="kc">true</span><span class="p">));</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">uiKitButton</span><span class="p">.</span><span class="nx">onHoverExit</span><span class="p">.</span><span class="nx">add</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">onHoveredEvent</span><span class="p">.</span><span class="nx">invoke</span><span class="p">(</span><span class="kc">false</span><span class="p">));</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">uiKitButton</span><span class="p">.</span><span class="nx">onTriggerDown</span><span class="p">.</span><span class="nx">add</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">onButtonPressedEvent</span><span class="p">.</span><span class="nx">invoke</span><span class="p">(</span><span class="nx">scrollButtonData</span><span class="p">));</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">buttonText</span><span class="p">.</span><span class="nx">text</span> <span class="o">=</span> <span class="nx">scrollButtonData</span><span class="p">.</span><span class="nx">buttonText</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">applyCustomSettings</span><span class="p">(</span><span class="nx">scrollButtonData</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">protected</span> <span class="nx">applyCustomSettings</span><span class="p">(</span><span class="nx">scrollButtonData</span><span class="p">:</span> <span class="nx">BaseScrollButtonData</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It wires up the button’s internal events to the <code class="language-plaintext highlighter-rouge">BaseUIKitScrollButtonController</code>’s <code class="language-plaintext highlighter-rouge">onButtonPressed</code> and <code class="language-plaintext highlighter-rouge">onHovered</code>, both of which will be consumed by the <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code>. It also sets the button’s text, then finally calls the (here empty) <code class="language-plaintext highlighter-rouge">applyCustomSettings</code> method that you can override in a child class should you need to do so, to perform some custom actions for your custom button. I showed an example of that <a href="">here</a>.</p>

<h2 id="uikitscrollmenucontroller">UIKitScrollMenuController</h2>

<p>This is basically the magic wand that all ties it together. The start is simple enough:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">component</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">UIKitScrollMenuController</span> <span class="kd">extends</span> <span class="nx">BaseScriptComponent</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">input</span> <span class="nx">yOffset</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
    <span class="p">@</span><span class="nd">input</span> <span class="nx">columnSize</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
    <span class="p">@</span><span class="nd">input</span> <span class="nx">scrollButtonPrefab</span><span class="p">:</span> <span class="nx">ObjectPrefab</span><span class="p">;</span>
    <span class="p">@</span><span class="nd">input</span> <span class="nx">scrollWindow</span><span class="p">:</span> <span class="nx">ScrollWindow</span><span class="p">;</span>
    <span class="p">@</span><span class="nd">input</span> <span class="nx">menuRoot</span><span class="p">:</span> <span class="nx">SceneObject</span><span class="p">;</span>
    <span class="p">@</span><span class="nd">input</span> <span class="nx">closeButton</span><span class="p">:</span> <span class="nx">BaseButton</span><span class="p">;</span>

    <span class="k">private</span> <span class="nx">onButtonPressedEvent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Event</span><span class="o">&lt;</span><span class="nx">BaseScrollButtonData</span><span class="o">&gt;</span><span class="p">();</span>
    <span class="k">public</span> <span class="k">readonly</span> <span class="nx">onButtonPressed</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">onButtonPressedEvent</span><span class="p">.</span><span class="nx">publicApi</span><span class="p">();</span>
    <span class="k">private</span> <span class="nx">scrollArea</span><span class="p">:</span> <span class="nx">SceneObject</span><span class="p">;</span>
</code></pre></div></div>

<p>On top we see the six inputs already discussed before, then again a <code class="language-plaintext highlighter-rouge">onButtonPressed</code> that can inform interested listeners what button in the list was pressed (as shown <a href="https://localjoost.github.io/Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-1-usage/#feed-the-menu-and-listen-to-the-menu">here</a>). The <code class="language-plaintext highlighter-rouge">scrollArea</code> we will need for a peculiar thing later.</p>

<p>Next is the setting up in <code class="language-plaintext highlighter-rouge">onAwake</code>:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="nx">onAwake</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">scrollArea</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">scrollWindow</span><span class="p">.</span><span class="nx">getSceneObject</span><span class="p">();</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">setMenuVisible</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">delayedEvent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">createEvent</span><span class="p">(</span><span class="dl">"</span><span class="s2">DelayedCallbackEvent</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">delayedEvent</span><span class="p">.</span><span class="nx">bind</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">initializeUI</span><span class="p">();</span>
    <span class="p">});</span>
    <span class="nx">delayedEvent</span><span class="p">.</span><span class="nx">reset</span><span class="p">(</span><span class="mf">0.1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We get a reference to the actual scrollWindow, we hide the menu for now, then start a delayed event for initializing the UI. This is necessary because for some reason the close button is not awake at <code class="language-plaintext highlighter-rouge">onAwake</code> yet, so you get the “Component not yet awake” error otherwise.</p>

<p>The methods for initializing the UI, as well as opening and closing the menu are as follows:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">protected</span> <span class="nx">initializeUI</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">closeButton</span><span class="p">.</span><span class="nx">onTriggerDown</span><span class="p">.</span><span class="nx">add</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">closeMenu</span><span class="p">());</span>
<span class="p">}</span>

<span class="nx">closeMenu</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">delayedEvent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">createEvent</span><span class="p">(</span><span class="dl">"</span><span class="s2">DelayedCallbackEvent</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">delayedEvent</span><span class="p">.</span><span class="nx">bind</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">setMenuVisible</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
    <span class="p">});</span>
    <span class="nx">delayedEvent</span><span class="p">.</span><span class="nx">reset</span><span class="p">(</span><span class="mf">0.25</span><span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">setMenuVisible</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">public</span> <span class="nx">setMenuVisible</span><span class="p">(</span><span class="nx">visible</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">menuRoot</span><span class="p">.</span><span class="nx">enabled</span> <span class="o">=</span> <span class="nx">visible</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In the <code class="language-plaintext highlighter-rouge">closeMenu</code> I keep a standard 0.25 seconds delay so the ‘click’ sound of the button has time to play; otherwise it will not play or be clipped as <code class="language-plaintext highlighter-rouge">menuRoot</code> SceneComponent, that holds all the UI, is hidden. The <code class="language-plaintext highlighter-rouge">menuRoot</code> component should be set to the first child in the prefab, as said before:</p>

<p><img src="/assets/2025-12-13-Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-2--how-it-works/frame2.png" alt="" /></p>

<p>Otherwise the <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code> will essentially disable itself.</p>

<p>The meat of the matter is the <code class="language-plaintext highlighter-rouge">createButtons</code> method, which essentially creates all buttons and the structure to support events to the outside world. Your own code should call it, feeding it an array of <code class="language-plaintext highlighter-rouge">BaseScrollButtonData</code> (or a child class of that). It starts as follows:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="nx">createButtons</span><span class="p">(</span><span class="nx">scrollButtonData</span><span class="p">:</span> <span class="nx">BaseScrollButtonData</span><span class="p">[]):</span> <span class="k">void</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">lines</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">ceil</span><span class="p">(</span><span class="nx">scrollButtonData</span><span class="p">.</span><span class="nx">length</span> <span class="o">/</span> <span class="mi">2</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">initOffset</span> <span class="o">=</span> <span class="nx">lines</span> <span class="o">%</span> <span class="mi">2</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">?</span> <span class="k">this</span><span class="p">.</span><span class="nx">yOffset</span> <span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">yOffset</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
    <span class="kd">var</span> <span class="nx">yStart</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">ceil</span><span class="p">(</span><span class="nx">lines</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="k">this</span><span class="p">.</span><span class="nx">yOffset</span> <span class="o">-</span> <span class="nx">initOffset</span><span class="p">;</span>
    <span class="kd">var</span> <span class="nx">line</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">scrollWindow</span><span class="p">.</span><span class="nx">onInitialized</span><span class="p">.</span><span class="nx">add</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">scrollWindow</span><span class="p">.</span><span class="nx">setScrollDimensions</span><span class="p">(</span><span class="k">new</span> <span class="nx">vec2</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">lines</span> <span class="o">*</span> <span class="k">this</span><span class="p">.</span><span class="nx">yOffset</span><span class="p">));</span>
    <span class="p">});</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">setMenuVisible</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
</code></pre></div></div>

<p>It first calculates how many rows of buttons are required, then the initial offset from the center, and with that at what y coordinate the buttons need to start (this will be used later). Then the scroll window scroll dimensions is set to the vertical size times the number of lines. Since we are only scrolling <em>vertically</em>, we only need to set the y part of it.</p>

<p>In hindsight I should have called <code class="language-plaintext highlighter-rouge">yOffset</code> “rowSize”, but what the heck.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">scrollButtonData</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">button</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">scrollButtonPrefab</span><span class="p">.</span><span class="nx">instantiate</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">scrollArea</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">buttonTransform</span> <span class="o">=</span> <span class="nx">button</span><span class="p">.</span><span class="nx">getTransform</span><span class="p">();</span>
    <span class="kd">var</span> <span class="nx">xPos</span> <span class="o">=</span> <span class="p">(</span><span class="nx">i</span> <span class="o">%</span> <span class="mi">2</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">?</span> <span class="o">-</span><span class="k">this</span><span class="p">.</span><span class="nx">columnSize</span> <span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">columnSize</span><span class="p">;</span>
    <span class="nx">buttonTransform</span><span class="p">.</span><span class="nx">setLocalPosition</span><span class="p">(</span>
      <span class="k">new</span> <span class="nx">vec3</span><span class="p">(</span><span class="nx">xPos</span><span class="p">,</span> <span class="nx">yStart</span> <span class="o">-</span> <span class="k">this</span><span class="p">.</span><span class="nx">yOffset</span> <span class="o">*</span> <span class="nx">line</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">));</span>
    <span class="nx">button</span><span class="p">.</span><span class="nx">enabled</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">%</span> <span class="mi">2</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">line</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kd">const</span> <span class="nx">buttonController</span> <span class="o">=</span>
       <span class="nx">getComponent</span><span class="o">&lt;</span><span class="nx">BaseUIKitScrollButtonController</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">button</span><span class="p">,</span>
                   <span class="nx">BaseUIKitScrollButtonController</span><span class="p">);</span>
    <span class="nx">buttonController</span><span class="p">.</span><span class="nx">setButtonData</span><span class="p">(</span><span class="nx">scrollButtonData</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
    <span class="nx">buttonController</span><span class="p">.</span><span class="nx">onHovered</span><span class="p">.</span><span class="nx">add</span><span class="p">((</span><span class="nx">p</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">scrollWindow</span><span class="p">.</span><span class="nx">vertical</span> <span class="o">=</span> <span class="o">!</span><span class="nx">p</span><span class="p">;</span>
    <span class="p">});</span>
    <span class="nx">buttonController</span><span class="p">.</span><span class="nx">onButtonPressed</span><span class="p">.</span><span class="nx">add</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span>
       <span class="k">this</span><span class="p">.</span><span class="nx">onButtonPressedEvent</span><span class="p">.</span><span class="nx">invoke</span><span class="p">(</span><span class="nx">data</span><span class="p">));</span>
<span class="p">}</span>
<span class="k">this</span><span class="p">.</span><span class="nx">updateScrollPosition</span><span class="p">();</span>
</code></pre></div></div>

<p>So, for every entry in <code class="language-plaintext highlighter-rouge">scrollButtonData</code> this code:</p>
<ul>
  <li>Creates a button</li>
  <li>Places it on the left or right side of the list based on whether it’s an odd or even button</li>
  <li>Enables the button</li>
  <li>Increases the line after every two buttons</li>
  <li>Gets a reference to <code class="language-plaintext highlighter-rouge">BaseUIKitScrollButtonController</code></li>
  <li>Feeds the <code class="language-plaintext highlighter-rouge">BaseScrollButtonData</code> entry to its <code class="language-plaintext highlighter-rouge">setButtonData</code> method - this will hook up the button</li>
  <li>Makes sure the window scrolling is disabled when you hover over a button</li>
  <li>Routes a button’s pressed event to the outside so you can listen to all buttons in one place</li>
  <li>And finally calls <code class="language-plaintext highlighter-rouge">updateScrollPosition</code></li>
</ul>

<p>The third-to-last thing deserves a bit of explanation. I noticed it was pretty hard to press a button on a scrollable list, especially in not very good lighting conditions, as you tend to accidentally drag/move the list if you just miss the buttons, or the Spectacles camera misses you trying to press it. This makes it a lot more usable.</p>

<p><code class="language-plaintext highlighter-rouge">updateScrollPosition</code> now is also a bit of a hacky thing. Because if you fill up a UIKit scroll list, it tends to set its scroll button halfway down the list. Why that is, I don’t know.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="nx">updateScrollPosition</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">delayedEvent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">createEvent</span><span class="p">(</span><span class="dl">"</span><span class="s2">DelayedCallbackEvent</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">delayedEvent</span><span class="p">.</span><span class="nx">bind</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">scrollWindow</span><span class="p">.</span><span class="nx">scrollPositionNormalized</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">vec2</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">menuRoot</span><span class="p">.</span><span class="nx">getTransform</span><span class="p">().</span><span class="nx">setLocalScale</span><span class="p">(</span><span class="k">new</span> <span class="nx">vec3</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">));</span>
    <span class="p">});</span>
    <span class="nx">delayedEvent</span><span class="p">.</span><span class="nx">reset</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It basically sets the <code class="language-plaintext highlighter-rouge">scrollPositionNormalized</code> to 0, 1, which translates to “vertical top scroll position”. 0, 0 is vertical center, 0, -1 is vertical bottom. If you don’t add <code class="language-plaintext highlighter-rouge">updateScrollPosition</code>, instead of the desired left screen, you get the right screen.</p>

<p><img src="/assets/2025-12-13-Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-2--how-it-works/updateScrollPosition.png" alt="" /></p>

<h2 id="conclusion">Conclusion</h2>

<p>So that’s kind of it. I do hope it’s useful for you and also gives you some insights into how you can cajole some UIKit elements into the shape you want. There is actually another little helper class in here, but I will deal with that later in yet another blog post.</p>

<p>The demo project is <a href="https://github.com/LocalJoost/DynamicScrollMenu">(still) here at GitHub</a>.</p>]]></content><author><name>Joost van Schaik (&apos;LocalJoost&apos;)</name></author><category term="Lens Studio" /><category term="Spectacles" /><category term="TypeScript" /><category term="UIKit" /><summary type="html"><![CDATA[In part 1 I described how this component can be used, and I promised to go deeper into the details about how it worked in a follow-up post. This is that post.]]></summary></entry><entry><title type="html">Dynamic data-driven scrollable button menu construction kit for Snap Spectacles part 1 - usage</title><link href="https://localjoost.github.io/Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-1-usage/" rel="alternate" type="text/html" title="Dynamic data-driven scrollable button menu construction kit for Snap Spectacles part 1 - usage" /><published>2025-12-09T00:00:00+01:00</published><updated>2025-12-09T00:00:00+01:00</updated><id>https://localjoost.github.io/Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-1--usage</id><content type="html" xml:base="https://localjoost.github.io/Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-1-usage/"><![CDATA[<p>If you have played with <a href="https://www.snapchat.com/lens/1e9933cbc6df44ceb936b154b0cb7b78?type=SNAPCODE&amp;metadata=01">my Spectacles lens HoloATC</a> you might have noticed the start menu with a scrollable list of buttons, which allows you to choose airports. This is a dynamic menu, that gets its data by downloading it from a service on Microsoft Azure. A list of data is dynamically translated into a scrollable list of buttons. I have turned that into a reusable and extendable component - well, more like a construction kit - that you can plug into your own lens for use.</p>

<p>This will be a two part blog post. This first post is relatively short and explains how to <em>use</em> it, the next one will describe more in detail how it <em>works</em>. If just you want to use it, this post should get you going.</p>

<h2 id="how-does-it-look">How does it look?</h2>

<p>Well, like this.</p>

<p><img src="/assets/2025-12-09-Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-1--usage/menu.png" alt="menu" /></p>

<p>And if you press buttons, you see per button two messages in the logger:</p>

<p><img src="/assets/2025-12-09-Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-1--usage/loggeroutput.png" alt="loggeroutput" /></p>

<p>Both the button and the menu itself print out what button is pressed. That is all for now - it’s yours to decide what you <em>actually</em> want the events to do. The menu is a just frame, also stays neatly in view, but that’s all standard Spectacles Interaction Kit components. The only thing I added is a custom close button.</p>

<h2 id="how-can-it-be-used">How can it be used?</h2>

<p>Very fast and high over:</p>

<ul>
  <li>Make a prefab with a UIKit BaseButton, and make sure is has a <code class="language-plaintext highlighter-rouge">BaseUIKitScrollButtonController</code> component (or a child class of that)</li>
  <li>Drag the ScrollMenu’s  prefab onto the scene</li>
  <li>Drag your button prefab on the input field “Scroll Button Prefab” of the ScrollMenu’s <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code> component.</li>
  <li>Create a bootstrapper script</li>
  <li>Give that a reference (@input) to a <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code></li>
  <li>Drag the ScrollMenu’s <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code> on that input</li>
  <li>The bootstrapper script somehow gets a feed of data, and turns in into a list of <code class="language-plaintext highlighter-rouge">BaseScrollButtonData</code> - or a list of <code class="language-plaintext highlighter-rouge">BaseScrollButtonData</code> child classes.</li>
  <li>It feeds that list into <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController.createButtons</code></li>
  <li>And the buttons will be created.</li>
</ul>

<p>Now if that went a bit too fast, I will go over it into more detail</p>

<h2 id="button-and-button-controller">Button and button controller</h2>

<p>The <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code>, which I will talk about later, instatiates buttons within its scroll list, so it needs something <em>to</em> instatiate. If you look into the <a href="https://github.com/LocalJoost/DynamicScrollMenu.git">demo project</a>, you will see an example of how such an instantiable button prefab might look. My example is called, very orignally, <strong>MyButton</strong>. This has a couple of rather standard components, and it also has a <code class="language-plaintext highlighter-rouge">MyButtonController</code>.</p>

<p><img src="/assets/2025-12-09-Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-1--usage/MyButton.png" alt="MyButton" /></p>

<p>It is defined like this:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">BaseScrollButtonData</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">LocalJoost/Ui/ScrollWindow/Scripts/BaseScrollButtonData</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">BaseUIKitScrollButtonController</span> <span class="p">}</span> 
  <span class="k">from</span> <span class="dl">"</span><span class="s2">LocalJoost/Ui/ScrollWindow/Scripts/BaseUIKitScrollButtonController</span><span class="dl">"</span><span class="p">;</span>

<span class="p">@</span><span class="nd">component</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">MyButtonController</span> <span class="kd">extends</span> <span class="nx">BaseUIKitScrollButtonController</span> <span class="p">{</span>
    <span class="k">protected</span> <span class="nx">applyCustomSettings</span><span class="p">(</span><span class="nx">scrollButtonData</span><span class="p">:</span> <span class="nx">BaseScrollButtonData</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
        <span class="k">super</span><span class="p">.</span><span class="nx">applyCustomSettings</span><span class="p">(</span><span class="nx">scrollButtonData</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">uiKitButton</span><span class="p">.</span><span class="nx">onTriggerDown</span><span class="p">.</span><span class="nx">add</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nx">print</span><span class="p">(</span><span class="dl">"</span><span class="s2">From MyButtonController: button pressed: </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">scrollButtonData</span><span class="p">.</span><span class="nx">buttonText</span><span class="p">);</span>
        <span class="p">});</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Your button controller <em>must inherit</em> <code class="language-plaintext highlighter-rouge">BaseUIKitScrollButtonController</code>. It inherits a <code class="language-plaintext highlighter-rouge">Text</code> and a <code class="language-plaintext highlighter-rouge">UIKitButton</code> @input field from it, and those <em>must be set</em> in the editor. The base class does a lot of useful things, setting the button text amongst others. It also gives you an <code class="language-plaintext highlighter-rouge">applyCustomSettings</code> method to override, in this case it rather trivially prints out to the logger that it is pressed, and shows the button text from the <code class="language-plaintext highlighter-rouge">BaseScrollButtonData</code> that it has been fed.</p>

<h2 id="basescrollbuttondata">BaseScrollButtonData</h2>

<p>This is a very simple data class from which buttons are created and every buttons gets fed such a <code class="language-plaintext highlighter-rouge">BaseScrollButtonData</code> object (or a child class)</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">class</span> <span class="nx">BaseScrollButtonData</span> <span class="p">{</span>
    <span class="k">public</span> <span class="nx">buttonText</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As stated, you can also make a child class that contains lots more data fields, and do something interesting with it to the button in the override of <code class="language-plaintext highlighter-rouge">applyCustomSettings</code> of your custom equivalent of <code class="language-plaintext highlighter-rouge">MyButtonController</code>. For instance, you can add a color attribute and give every button a different color. Or, like I did, you can add attributes that hold location, altitude, name and the IATA airport code of an airport and use that to select a location on a map when a button is pressed.</p>

<h2 id="configure-scrollmenu-prefab">Configure ScrollMenu prefab</h2>

<p>The whole menu is fit within the ScrollMenu prefab. Drag this prefab onto your scene. The prefab has a couple of inputs you can fill, all of which can be ignored - apart from the Scroll Button Prefab input: that <em>must</em> contain a prefab. Drag your Button prefab on that field</p>

<p><img src="/assets/2025-12-09-Dynamic-data-driven-scrollable-button-menu-construction-kit-for-Snap-Spectacles-part-1--usage/scrollmenu.png" alt="scrollmenu" /></p>

<p>and the ScrollMenu is almost ready to go.</p>

<h2 id="feed-the-menu-and-listen-to-the-menu">Feed the menu and listen to the menu</h2>

<p>As wrote before, the menu needs a list of <code class="language-plaintext highlighter-rouge">BaseScrollButtonData</code> data objects containing at least the button text to be able to generate a list of buttons. This is a sample of a bootstrapper script that does just that:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">BaseScrollButtonData</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">LocalJoost/Ui/ScrollWindow/Scripts/BaseScrollButtonData</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">UIKitScrollMenuController</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">LocalJoost/Ui/ScrollWindow/Scripts/UIKitScrollMenuController</span><span class="dl">"</span><span class="p">;</span>

<span class="p">@</span><span class="nd">component</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">ScrollButtonDataLoader</span> <span class="kd">extends</span> <span class="nx">BaseScriptComponent</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">input</span> <span class="nx">scrollMenuController</span><span class="p">:</span> <span class="nx">UIKitScrollMenuController</span>

    <span class="k">private</span> <span class="nx">onAwake</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="na">buttonDataArray</span><span class="p">:</span> <span class="nx">BaseScrollButtonData</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;=</span> <span class="mi">20</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">buttonData</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">BaseScrollButtonData</span><span class="p">();</span>
            <span class="nx">buttonData</span><span class="p">.</span><span class="nx">buttonText</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Button </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">i</span><span class="p">;</span>
            <span class="nx">buttonDataArray</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">buttonData</span><span class="p">);</span> 
        <span class="p">}</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">createButtons</span><span class="p">(</span><span class="nx">buttonDataArray</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">scrollMenuController</span><span class="p">.</span><span class="nx">onButtonPressed</span><span class="p">.</span><span class="nx">add</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nx">print</span><span class="p">(</span><span class="dl">"</span><span class="s2">From UIKitScrollMenuController: button Pressed: </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">data</span><span class="p">.</span><span class="nx">buttonText</span><span class="p">);</span>
        <span class="p">});</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This script needs an reference to the menu’s <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code> so in can call it’s method <code class="language-plaintext highlighter-rouge">createButtons</code> method.</p>

<p>In real use, <code class="language-plaintext highlighter-rouge">buttonDataArray</code> would be filled by reading a data stream from the web or some kind of configuration file. However, in this simple sample I just make them locally. But the <code class="language-plaintext highlighter-rouge">scrollMenuController</code> also exposes and event <code class="language-plaintext highlighter-rouge">onButtonPressed</code> to which you can subscribe and do something with, although here we also just print out the data that was fed to the button, just to show it works.</p>

<h2 id="some-bits-and-pieces">Some bits and pieces</h2>

<p>The <code class="language-plaintext highlighter-rouge">UIKitScrollMenuController</code> also features a <code class="language-plaintext highlighter-rouge">setMenuVisible</code> method which you can use to turn the menu of or off from the outside, without having to reload the data using the <code class="language-plaintext highlighter-rouge">ScrollButtonDataLoader</code> or whatever you want to call your loader script.</p>

<p>Make sure your scripts lives in a scene object <em>under</em> the actual ScrollMenu prefab, otherwise you most likely will run into the dreaded “Compent not yet awake” error.</p>

<p>For the sake of completness: this menu needs both the Spectacles Interaction Kit and UIKit to be installed.</p>

<p>The <a href="https://github.com/LocalJoost/DynamicScrollMenu.git">demo project with all the neccesary components can be found here.</a></p>]]></content><author><name>Joost van Schaik (&apos;LocalJoost&apos;)</name></author><category term="Lens Studio" /><category term="Spectacles" /><category term="TypeScript" /><category term="UIKit" /><summary type="html"><![CDATA[If you have played with my Spectacles lens HoloATC you might have noticed the start menu with a scrollable list of buttons, which allows you to choose airports. This is a dynamic menu, that gets its data by downloading it from a service on Microsoft Azure. A list of data is dynamically translated into a scrollable list of buttons. I have turned that into a reusable and extendable component - well, more like a construction kit - that you can plug into your own lens for use.]]></summary></entry><entry><title type="html">Getting components by their base class name in Lens Studio</title><link href="https://localjoost.github.io/Getting-components-by-their-base-class-name-in-Lens-Studio/" rel="alternate" type="text/html" title="Getting components by their base class name in Lens Studio" /><published>2025-12-07T00:00:00+01:00</published><updated>2025-12-07T00:00:00+01:00</updated><id>https://localjoost.github.io/Getting-components-by-their-base-class-name-in-Lens-Studio</id><content type="html" xml:base="https://localjoost.github.io/Getting-components-by-their-base-class-name-in-Lens-Studio/"><![CDATA[<p>Yet another small thing, this time to solve something that annoyed the heck out of me since day one. I really dislike this construction that you have to use when you have to get a reference to a component from a <code class="language-plaintext highlighter-rouge">SceneObject</code></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">someSceneObject</span><span class="p">.</span><span class="nx">getComponent</span><span class="p">(</span><span class="nx">MyComponentClass</span><span class="p">.</span><span class="nx">getTypeName</span><span class="p">())</span> <span class="k">as</span> <span class="nx">MyComponentClass</span><span class="p">;</span>
</code></pre></div></div>

<p>And what is even <em>worse</em>, it does not work <em>at all</em> when when you need to find the reference but you only have the component’s <em>base class</em>. Suppose I have this trivial base controller component:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">component</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">MyBaseClass</span> <span class="kd">extends</span> <span class="nx">BaseScriptComponent</span> <span class="p">{</span>

    <span class="k">public</span> <span class="nx">hello</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="nx">print</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello from MyBaseClass</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>However, I have multiple prefabs types with a slightly different child class suited for their particular purpose, for instance:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">MyBaseClass</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./MyBaseClass</span><span class="dl">"</span><span class="p">;</span>

<span class="p">@</span><span class="nd">component</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">MyChildClass</span> <span class="kd">extends</span> <span class="nx">MyBaseClass</span> <span class="p">{</span>

    <span class="k">public</span> <span class="nx">hello</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="nx">print</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello from MyChildClass</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If I add this child class to my prefab, then try to find a reference to its controlling component using the <code class="language-plaintext highlighter-rouge">MyBaseClass</code> class</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">myPrefab</span><span class="p">.</span><span class="nx">getComponent</span><span class="p">(</span><span class="nx">MyBaseClass</span><span class="p">.</span><span class="nx">getTypeName</span><span class="p">())</span> <span class="k">as</span> <span class="nx">MyBaseClass</span><span class="p">;</span>
</code></pre></div></div>

<p>I wil get a null value result! This is highly confusing for someone coming from Unity, and rather limiting as well.</p>

<p>However, there is a solution. I have extended my SceneUtils with this little method:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">function</span> <span class="nx">getComponent</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span>
    <span class="nx">sceneObject</span><span class="p">:</span> <span class="nx">SceneObject</span><span class="p">,</span>
    <span class="nx">myClassType</span><span class="p">:</span> <span class="k">new</span> <span class="p">(...</span><span class="nx">args</span><span class="p">:</span> <span class="kr">any</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="nx">T</span><span class="p">):</span> <span class="nx">T</span> <span class="o">|</span> <span class="kc">null</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">component</span> <span class="k">of</span> <span class="nx">sceneObject</span><span class="p">.</span><span class="nx">getComponents</span><span class="p">(</span><span class="dl">"</span><span class="s2">Component</span><span class="dl">"</span><span class="p">)</span> <span class="k">as</span> <span class="nx">Component</span><span class="p">[])</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">component</span> <span class="k">instanceof</span> <span class="nx">myClassType</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nx">component</span> <span class="k">as</span> <span class="nx">T</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I made a little test script <a href="https://github.com/LocalJoost/GetComponentLikeUnity.git">in the demo project</a> to show how it works:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">MyBaseClass</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./MyBaseClass</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">getComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">LocalJoost/Utilities/SceneUtils</span><span class="dl">"</span><span class="p">;</span>

<span class="p">@</span><span class="nd">component</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">TestScript</span> <span class="kd">extends</span> <span class="nx">BaseScriptComponent</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">input</span> <span class="nx">prefab</span> <span class="p">:</span> <span class="nx">ObjectPrefab</span><span class="p">;</span>
   
    <span class="nx">onAwake</span><span class="p">()</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">instance</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">prefab</span><span class="p">.</span><span class="nx">instantiate</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">getSceneObject</span><span class="p">());</span>
        <span class="kd">var</span> <span class="nx">getComponentResult</span> <span class="o">=</span> <span class="nx">instance</span><span class="p">.</span><span class="nx">getComponent</span><span class="p">(</span><span class="nx">MyBaseClass</span><span class="p">.</span><span class="nx">getTypeName</span><span class="p">())</span> <span class="k">as</span> <span class="nx">MyBaseClass</span><span class="p">;</span>
        <span class="nx">print</span><span class="p">(</span><span class="dl">"</span><span class="s2">getComponentByClass: </span><span class="dl">"</span> <span class="o">+</span> <span class="p">(</span><span class="nx">getComponentResult</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">));</span>

        <span class="kd">var</span> <span class="nx">findScriptComponentResult</span> <span class="o">=</span> <span class="nx">getComponent</span><span class="o">&lt;</span><span class="nx">MyBaseClass</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">instance</span><span class="p">,</span> <span class="nx">MyBaseClass</span><span class="p">);</span>
        <span class="nx">print</span><span class="p">(</span><span class="dl">"</span><span class="s2">findScriptComponentByClass: </span><span class="dl">"</span> <span class="o">+</span> <span class="p">(</span><span class="nx">findScriptComponentResult</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">));</span>
        <span class="nx">findScriptComponentResult</span><span class="p">.</span><span class="nx">hello</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here’s the logger output that actually <em>proves</em> it works</p>

<p><img src="/assets/2025-12-07-Getting-components-by-their-base-class-name-in-Lens-Studio/logger.png" alt="logger" /></p>

<p>Although I search using the <em>base</em> class, I get a reference to a <em>child</em> class object. Which is exactly what you would expect. Or at least what <em>I</em> would expect. It’s only five lines of code, so I wonder why Snap didn’t implement it this way in their own <code class="language-plaintext highlighter-rouge">getComponent</code>. Maybe they wanted to leave in some challenges for grumpy old skool software engineers to solve 😁. However, this works and it easy to use.</p>]]></content><author><name>Joost van Schaik (&apos;LocalJoost&apos;)</name></author><category term="Lens Studio" /><category term="Spectacles" /><category term="ScriptComponent" /><category term="TypeScript" /><summary type="html"><![CDATA[Yet another small thing, this time to solve something that annoyed the heck out of me since day one. I really dislike this construction that you have to use when you have to get a reference to a component from a SceneObject]]></summary></entry><entry><title type="html">Finding all script components of a type in a Lens Studio scene</title><link href="https://localjoost.github.io/Finding-all-script-components-of-a-type-in-a-Lens-Studio-scene/" rel="alternate" type="text/html" title="Finding all script components of a type in a Lens Studio scene" /><published>2025-12-06T00:00:00+01:00</published><updated>2025-12-06T00:00:00+01:00</updated><id>https://localjoost.github.io/Finding-all-script-components-of-a-type-in-a-Lens-Studio-scene</id><content type="html" xml:base="https://localjoost.github.io/Finding-all-script-components-of-a-type-in-a-Lens-Studio-scene/"><![CDATA[<p>A short and small one, but one that is very useful for quick proof of concepts and/orhackatons - and Snap likes to organize those at the moment. If you are working with multiple people at a such high paced short interations project, scene stucture tends to change a lot, breaking references to objects in the scene, and sending your project into havoc at every commit or pull - not to mention your nerves and the team spirit. There are, however, in the Spectacles Interaction Kit, a few functions that allow you to quickly find objects in the scene <em>from code by name or type</em> rather than referencing by @input fields, and obtains references that way:</p>
<ul>
  <li>findSceneObjectByName</li>
  <li>findComponentInChildren</li>
  <li>findAllComponentsInChildren</li>
  <li>findScriptComponentInChildren</li>
  <li>findAllScriptComponentsInChildren</li>
  <li>findComponentInParents</li>
  <li>findAllComponentsInParents</li>
  <li>findScriptComponentInParents</li>
  <li>findComponentInSelfOrParents</li>
  <li>findAllScriptComponentsInSelfOrParents</li>
</ul>

<p>If you are coming from Unity, you might easily fall into the trap (as I did) of seeing not differences between <code class="language-plaintext highlighter-rouge">Component</code> and <code class="language-plaintext highlighter-rouge">ScriptComponent</code>, use <code class="language-plaintext highlighter-rouge">findComponentInChildren</code> or <code class="language-plaintext highlighter-rouge">findAllComponentsInChildren</code>, notice that that won’t even compile because it wants a “keyof ComponentNameMap” in stead of a type - and waste a lot of time on that. So if you want to find <code class="language-plaintext highlighter-rouge">ScriptComponent</code>/behaviours that you have placed in the scene and your team mate has moved it <em>somewhere</em>, you should rather use the *findScriptComponent* methods than the *findComponent* methods.</p>

<p><em>However</em>, with the exception of <code class="language-plaintext highlighter-rouge">findSceneObjectByName</code>, these only work if you provide a top <code class="language-plaintext highlighter-rouge">SceneComponent</code>. So if your lovely team mates have created multiple root scene objects, good luck finding your scripts.</p>

<p>So I created this very little helper function that does that for you:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">findAllScriptComponentsInChildren</span> <span class="p">}</span> 
  <span class="k">from</span> <span class="dl">"</span><span class="s2">SpectaclesInteractionKit.lspkg/Utils/SceneObjectUtils</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">DEFAULT_MAX_CHILD_SEARCH_LEVELS</span> <span class="p">}</span> 
  <span class="k">from</span> <span class="dl">"</span><span class="s2">SpectaclesInteractionKit.lspkg/Utils/SceneObjectUtils</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nx">findAllScriptComponentsInScene</span><span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">ScriptComponent</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="nx">scriptClass</span><span class="p">:</span> <span class="k">new</span> <span class="p">(...</span><span class="nx">args</span><span class="p">:</span> <span class="kr">any</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="nx">T</span><span class="p">,</span>
  <span class="nx">maxDepth</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="nx">DEFAULT_MAX_CHILD_SEARCH_LEVELS</span><span class="p">):</span> <span class="nx">T</span><span class="p">[]</span> <span class="o">|</span> <span class="kc">null</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">foundComponents</span><span class="p">:</span> <span class="nx">T</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[];</span>
    <span class="kd">const</span> <span class="nx">rootCount</span> <span class="o">=</span> <span class="nb">global</span><span class="p">.</span><span class="nx">scene</span><span class="p">.</span><span class="nx">getRootObjectsCount</span><span class="p">();</span>
    <span class="k">try</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">rootCount</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">so</span> <span class="o">=</span> <span class="nb">global</span><span class="p">.</span><span class="nx">scene</span><span class="p">.</span><span class="nx">getRootObject</span><span class="p">(</span><span class="nx">i</span><span class="p">);</span>
            <span class="kd">const</span> <span class="nx">components</span> <span class="o">=</span> <span class="nx">findAllScriptComponentsInChildren</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">so</span><span class="p">,</span> <span class="nx">scriptClass</span><span class="p">,</span> <span class="nx">maxDepth</span><span class="p">);</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">components</span> <span class="o">&amp;&amp;</span> <span class="nx">components</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                <span class="nx">foundComponents</span><span class="p">.</span><span class="nx">push</span><span class="p">(...</span><span class="nx">components</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nx">foundComponents</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">?</span> <span class="nx">foundComponents</span> <span class="p">:</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">print</span><span class="p">(</span><span class="dl">"</span><span class="s2">Error while searching for components in scene: </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">error</span><span class="p">);</span>
        <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I created <a href="https://github.com/LocalJoost/FindAnyScriptComponentInSceneDemo.git">a little demo project</a> that demonstrates this. For this I made a trivial <code class="language-plaintext highlighter-rouge">TestTarget</code> ScriptComponent that I sprinkled through the scene:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">component</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">TestTarget</span> <span class="kd">extends</span> <span class="nx">BaseScriptComponent</span> <span class="p">{</span>

    <span class="k">public</span> <span class="nx">getInfo</span><span class="p">():</span> <span class="kr">string</span> <span class="p">{</span>
        <span class="k">return</span> <span class="dl">"</span><span class="s2">I am a TestTarget component on </span><span class="dl">"</span> <span class="o">+</span> <span class="k">this</span><span class="p">.</span><span class="nx">getSceneObject</span><span class="p">().</span><span class="nx">name</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">!</span><span class="dl">"</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And an equally trivial <code class="language-plaintext highlighter-rouge">TestFinder</code> ScriptComponent that finds them:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">findAllScriptComponentsInScene</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">LocalJoost/Utilities/SceneUtils</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">TestTarget</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">TestTarget</span><span class="dl">"</span><span class="p">;</span>

<span class="p">@</span><span class="nd">component</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">TestFinder</span> <span class="kd">extends</span> <span class="nx">BaseScriptComponent</span> <span class="p">{</span>
    <span class="nx">onAwake</span><span class="p">()</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">foundTargets</span> <span class="o">=</span> <span class="nx">findAllScriptComponentsInScene</span><span class="o">&lt;</span><span class="nx">TestTarget</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">TestTarget</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">foundTargets</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">target</span> <span class="k">of</span> <span class="nx">foundTargets</span><span class="p">)</span> <span class="p">{</span>
                <span class="nx">print</span><span class="p">(</span><span class="nx">target</span><span class="p">.</span><span class="nx">getInfo</span><span class="p">());</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Result in the Lens Studio Logger panel shows it actually works:</p>

<p><img src="/assets/2025-12-06-Finding-all-script-components-of-a-type-in-a-Lens-Studio-scene/found.png" alt="found" /></p>

<p>A word of warning: this is highly inefficient as you are bascially traversing the <em>whole scene</em> to find script components. This can be useful at startup, for connecting the dots initially, especially in the circumstances I described. But do not use this very often, like from an Update loop, because especially in a large scene it can be very resource intensive and slow.</p>]]></content><author><name>Joost van Schaik (&apos;LocalJoost&apos;)</name></author><category term="Lens Studio" /><category term="Spectacles" /><category term="ScriptComponent" /><category term="TypeScript" /><summary type="html"><![CDATA[A short and small one, but one that is very useful for quick proof of concepts and/orhackatons - and Snap likes to organize those at the moment. If you are working with multiple people at a such high paced short interations project, scene stucture tends to change a lot, breaking references to objects in the scene, and sending your project into havoc at every commit or pull - not to mention your nerves and the team spirit. There are, however, in the Spectacles Interaction Kit, a few functions that allow you to quickly find objects in the scene from code by name or type rather than referencing by @input fields, and obtains references that way: findSceneObjectByName findComponentInChildren findAllComponentsInChildren findScriptComponentInChildren findAllScriptComponentsInChildren findComponentInParents findAllComponentsInParents findScriptComponentInParents findComponentInSelfOrParents findAllScriptComponentsInSelfOrParents]]></summary></entry><entry><title type="html">Renaming prefabs in Lens Studio</title><link href="https://localjoost.github.io/Renaming-prefabs-in-Lens-Studio/" rel="alternate" type="text/html" title="Renaming prefabs in Lens Studio" /><published>2025-10-31T00:00:00+01:00</published><updated>2025-10-31T00:00:00+01:00</updated><id>https://localjoost.github.io/Renaming-prefabs-in-Lens-Studio</id><content type="html" xml:base="https://localjoost.github.io/Renaming-prefabs-in-Lens-Studio/"><![CDATA[<p>Is this really worth a blog post? Well maybe it is not, but it is something that made me scratch my head. An maybe other people are stymied by it as well, so I thought to write a little something. The issue is this. Suppose I have a prefab MyAwesomePrefab, like this:</p>

<p><img src="/assets/2025-10-31-Renaming-prefabs-in-Lens-Studio/prefab1.png" alt="prefab1" /></p>

<p>Lens Studio unfortunately doesn’t know the concept of prefab variants like Unity does, so if I want another prefab that is almost the same, I need to copy it. Let’s call it MyOtherPrefab. I add an extra text. The top scene object is still called MyAwesomePrefab, so let’s rename that, and hit Apply</p>

<p><img src="/assets/2025-10-31-Renaming-prefabs-in-Lens-Studio/prefab2.png" alt="prefab2" /></p>

<p>You might think we are done, but this is were it gets odd. Because if you drag both MyAwesomePrefab and MyOtherPrefab in the scene…</p>

<p><img src="/assets/2025-10-31-Renaming-prefabs-in-Lens-Studio/prefab3.png" alt="prefab3" /></p>

<p><em>They both show up as MyAwesomePrefab</em>.</p>

<p>This, of course, can be highly confusing. Maybe it’s a bug, or maybe I simply not understand how this is supposed to work. However, this is how I fix it. Fortunately, all files a Lens Studio creates are text files. I don’t know what you call this format, but somewhere you will find this:</p>

<p><img src="/assets/2025-10-31-Renaming-prefabs-in-Lens-Studio/fix.png" alt="fix" /></p>

<p>Change that to MyAwesomePrefab, save the file, and if you now drag MyAwesomePrefab on the scene…</p>

<p><img src="/assets/2025-10-31-Renaming-prefabs-in-Lens-Studio/prefab4.png" alt="prefab4" /></p>

<p>Important note: <em>save your project first before starting the rename</em>. If you haven’t saved it, Lens Studio makes a kind of temporay project on a in a temporary location and that may cause issues when you do things like this. Even better - commit your project to Git before doing scary things like this. A prudent developer always makes small steps so it’s easy to recover from oopsies.</p>

<p>No code this time, as there is no code to share ;)</p>]]></content><author><name>Joost van Schaik (&apos;LocalJoost&apos;)</name></author><category term="Lens Studio" /><category term="Spectacles" /><category term="Prefabs" /><summary type="html"><![CDATA[Is this really worth a blog post? Well maybe it is not, but it is something that made me scratch my head. An maybe other people are stymied by it as well, so I thought to write a little something. The issue is this. Suppose I have a prefab MyAwesomePrefab, like this:]]></summary></entry><entry><title type="html">Deploying on Spectacles from Windows using an USB cable</title><link href="https://localjoost.github.io/Deploying-on-Spectacles-from-Windows-using-an-USB-cable/" rel="alternate" type="text/html" title="Deploying on Spectacles from Windows using an USB cable" /><published>2025-08-23T00:00:00+02:00</published><updated>2025-08-23T00:00:00+02:00</updated><id>https://localjoost.github.io/Deploying-on-Spectacles-from-Windows-using-an-USB-cable</id><content type="html" xml:base="https://localjoost.github.io/Deploying-on-Spectacles-from-Windows-using-an-USB-cable/"><![CDATA[<p>Apparently I am a bit of an oddball in the Snap Spectacles community, as apparently everyone else - including the Snap folks themselves - uses Macs for development. Although I do have a Mac Mini, it’s only being used when I’m working with Apple devices (specifically, Vision Pro). For the rest, I use Windows. I grew up on Windows, used it for over three decades, know it inside and out, it runs like the wind on this machine, and I have a great toolset I’d dearly miss going elsewhere - it’s just my thing.</p>

<p>However, this posed a few problems when I was trying to deploy on Spectacles from Windows using the relatively new “wired connection” option. It became pretty clear no one at Snap used it much - few probably even tried - because it caused some headaches trying to get it to work. Still, this is where Snap’s dedication to their product shows. After posting my woes on the <a href="https://www.reddit.com/r/Spectacles/">Spectacles Reddit</a>, I was contacted by one of their engineers, <a href="https://www.linkedin.com/in/pavelantonenko/">Pasha Antonenko</a>, and we had a well-over-an-hour-long video call (at a rather ungodly early hour for Pasha) in which we established the baseline for successfully connecting and deploying a Spectacles app to a Spectacles using a USB cable from Windows.</p>

<p>In short - the list of requirements is this:</p>

<ol>
  <li>Whenever you connect a Spectacles to a Windows machine, you get the “USB device malfunctioned” error. Always. However confusing that is, you can actually <em>ignore</em> that. You might first see a message that it set up a “composite USB device” first.</li>
  <li>You <em>must</em> have the newest version of the <a href="https://developer.android.com/tools/releases/platform-tools">ABD tools</a> and have that set up in Lens Studio to get a stable connection between Lens Studio and the Spectacles. Do not -  I repeat, <em>do not</em> - be tempted to re-use an ADB tools install from some Unity installation, as this developer initially tried because it seemed the logical thing to do.</li>
  <li>Double-check that wired connection is turned on in the settings. I was sure they were, but they were not. Maybe a result of the recent upgrade on my phone. I use Android, and I guess the Snap folks are primarily on iOS too 😉.</li>
  <li>You can only connect to a <em>native USB-C port</em>, so you need a cable with USB-C plugs on both sides. A converter cable with a USB-A plug on one side will not work properly, regardless of how fast the port or cable are rated.</li>
</ol>

<p>Ad 2: do not forget to actually point to the place where you unpacked the ADB tools in preferences (Lens Studio / Preferences):</p>

<p><img src="/assets/2025-08-23-Deploying-on-Spectacles-from-Windows-using-an-USB-cable/adbtools.png" alt="adbtools" /></p>

<p>Ad 3: you need to set this in the Spectacles app on your phone:</p>

<p><img src="/assets/2025-08-23-Deploying-on-Spectacles-from-Windows-using-an-USB-cable/app.png" alt="app" /></p>

<p>Only when you see this in your Logger is the Spectacles successfully connected to your Windows computer, and the USB cable can be used for deployment.</p>

<p><img src="/assets/2025-08-23-Deploying-on-Spectacles-from-Windows-using-an-USB-cable/connected.png" alt="connected" /></p>

<p>No demo project this time, because there is no code to share.</p>]]></content><author><name>Joost van Schaik (&apos;LocalJoost&apos;)</name></author><category term="Lens Studio" /><category term="Specacles" /><category term="Windows" /><summary type="html"><![CDATA[Apparently I am a bit of an oddball in the Snap Spectacles community, as apparently everyone else - including the Snap folks themselves - uses Macs for development. Although I do have a Mac Mini, it’s only being used when I’m working with Apple devices (specifically, Vision Pro). For the rest, I use Windows. I grew up on Windows, used it for over three decades, know it inside and out, it runs like the wind on this machine, and I have a great toolset I’d dearly miss going elsewhere - it’s just my thing.]]></summary></entry></feed>