<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Biometric on Tarragon</title><link>https://tarrragon.github.io/blog/tags/biometric/</link><description>Recent content in Biometric on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/biometric/index.xml" rel="self" type="application/rss+xml"/><item><title>Biometric fallback 完整設計</title><link>https://tarrragon.github.io/blog/ux-design/02-gate-fallback/biometric-fallback-design/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/02-gate-fallback/biometric-fallback-design/</guid><description>&lt;p>Biometric &lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/gate/" data-link-title="Gate（UX）" data-link-desc="說明使用者操作流程中「必須通過才能繼續」的關卡，以及成功/失敗/不確定三條路徑的設計責任">gate&lt;/a> 的 &lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/ux-fallback/" data-link-title="Fallback（UX）" data-link-desc="說明 gate 未通過時使用者的替代路徑，和 backend fallback（server-side 降級）的語意區別">fallback&lt;/a> 設計需要理解兩件事：平台的認證 API 在不同情境下的行為差異，以及安全收益和可用性代價之間的顯式取捨。&lt;/p>
&lt;h2 id="生物辨識失敗的情境">生物辨識失敗的情境&lt;/h2>
&lt;p>生物辨識失敗有多種原因，每種原因對使用者的影響和合理的 fallback 不同。&lt;/p>
&lt;h3 id="暫時性失敗">暫時性失敗&lt;/h3>
&lt;p>Face ID 因光線不足辨識失敗、指紋因手指潮濕讀取失敗。使用者的生物特徵正常，只是當次辨識條件不佳。重試可能成功。&lt;/p>
&lt;h3 id="持續性失敗">持續性失敗&lt;/h3>
&lt;p>使用者戴口罩讓 Face ID 無法辨識（較舊的 iOS 版本）、手指受傷影響指紋辨識。生物特徵暫時改變，短期內重試都不會成功。需要替代認證方式。&lt;/p>
&lt;h3 id="硬體不可用">硬體不可用&lt;/h3>
&lt;p>裝置沒有 Face ID / Touch ID 模組（較舊機型）、模擬器不支援生物辨識、生物辨識功能被裝置管理策略（MDM）禁用。需要替代認證方式。&lt;/p>
&lt;h3 id="使用者未設定">使用者未設定&lt;/h3>
&lt;p>裝置有硬體但使用者沒有設定 Face ID 或指紋。系統的 &lt;code>canCheckBiometrics&lt;/code> 回傳 &lt;code>true&lt;/code>（硬體存在）但實際認證會失敗。需要引導使用者設定或提供替代認證。&lt;/p>
&lt;h2 id="ios-和-android-的行為差異">iOS 和 Android 的行為差異&lt;/h2>
&lt;h3 id="ioslocalauthentication">iOS（LocalAuthentication）&lt;/h3>
&lt;p>iOS 的 &lt;code>LAContext.evaluatePolicy&lt;/code> 有兩個 policy：&lt;/p>
&lt;ul>
&lt;li>&lt;code>deviceOwnerAuthenticationWithBiometrics&lt;/code>：只接受生物辨識，失敗後不自動提示密碼&lt;/li>
&lt;li>&lt;code>deviceOwnerAuthentication&lt;/code>：先嘗試生物辨識，失敗後系統自動彈出裝置密碼輸入&lt;/li>
&lt;/ul>
&lt;p>Flutter 的 &lt;code>local_auth&lt;/code> 套件的 &lt;code>biometricOnly&lt;/code> 參數對應這兩個 policy。&lt;code>biometricOnly: true&lt;/code> 用前者，&lt;code>biometricOnly: false&lt;/code> 用後者。&lt;/p>
&lt;p>iOS 的行為特點：系統控制認證 UI（不是 app 自行繪製），認證失敗次數過多會自動鎖定（需要輸入密碼解鎖），Face ID 多次失敗後系統會自動提供密碼選項（即使 app 要求 biometricOnly）。&lt;/p>
&lt;h3 id="androidbiometricprompt">Android（BiometricPrompt）&lt;/h3>
&lt;p>Android 的 BiometricPrompt 分成三個 class：&lt;/p>
&lt;ul>
&lt;li>&lt;code>BIOMETRIC_STRONG&lt;/code>：只接受 Class 3 生物辨識（經過硬體安全模組驗證的指紋/面部）&lt;/li>
&lt;li>&lt;code>BIOMETRIC_WEAK&lt;/code>：接受 Class 2 和 Class 3 生物辨識&lt;/li>
&lt;li>&lt;code>DEVICE_CREDENTIAL&lt;/code>：接受裝置 PIN/圖形/密碼&lt;/li>
&lt;/ul>
&lt;p>三個 class 可以用 &lt;code>|&lt;/code> 組合。&lt;code>BIOMETRIC_STRONG | DEVICE_CREDENTIAL&lt;/code> 表示先嘗試強生物辨識，失敗後 fallback 到裝置密碼。&lt;/p>
&lt;p>Android 的行為特點：不同廠商的生物辨識品質差異大（Samsung 的面部辨識和 Pixel 的面部辨識安全等級不同）、部分裝置的指紋感測器在螢幕下方（使用者可能不知道在哪裡觸碰）。&lt;/p>
&lt;h2 id="安全-vs-可用性的顯式取捨">安全 vs 可用性的顯式取捨&lt;/h2>
&lt;p>&lt;code>biometricOnly&lt;/code> 的決策涉及安全和可用性的取捨。這個取捨應該在功能規格中顯式記錄，讓後續的 code review 和維護者能理解決策的背景。&lt;/p>
&lt;p>記錄格式建議：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Gate: biometric authentication
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Decision: biometricOnly = false (allow device credential fallback)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">Security trade-off: device credential (PIN/password) is weaker than biometric
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">Rationale: self-hosted tool, user = owner, availability &amp;gt; auth strength
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">Risk accepted: someone with device PIN can access the app&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>app_tunnel 選擇 &lt;code>biometricOnly: true&lt;/code> 的原始意圖是「安全性更高」，但沒有顯式記錄取捨，也沒有評估「Face ID 不可用時使用者完全無法使用 app」的代價。自用工具的使用者就是 owner，密碼 fallback 的安全風險遠低於完全無法使用的可用性風險（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2&lt;/a>）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Gate 設計的通用方法論 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法&lt;/a>&lt;/li>
&lt;li>開發環境遮蔽 gate 問題 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/dev-vs-real-gate-behavior/" data-link-title="開發環境 vs 真機的 gate 行為差異表" data-link-desc="模擬器、debug build、test 環境中的 gate 行為和真機 release build 不同 — 差異表讓開發者在上機前知道哪些 gate 還沒被真實驗證">開發環境 vs 真機的 gate 行為差異表&lt;/a>&lt;/li>
&lt;li>安全 vs 可用性在 monitoring 中的對應 → &lt;a href="https://tarrragon.github.io/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Biometric <a href="/blog/ux-design/knowledge-cards/gate/" data-link-title="Gate（UX）" data-link-desc="說明使用者操作流程中「必須通過才能繼續」的關卡，以及成功/失敗/不確定三條路徑的設計責任">gate</a> 的 <a href="/blog/ux-design/knowledge-cards/ux-fallback/" data-link-title="Fallback（UX）" data-link-desc="說明 gate 未通過時使用者的替代路徑，和 backend fallback（server-side 降級）的語意區別">fallback</a> 設計需要理解兩件事：平台的認證 API 在不同情境下的行為差異，以及安全收益和可用性代價之間的顯式取捨。</p>
<h2 id="生物辨識失敗的情境">生物辨識失敗的情境</h2>
<p>生物辨識失敗有多種原因，每種原因對使用者的影響和合理的 fallback 不同。</p>
<h3 id="暫時性失敗">暫時性失敗</h3>
<p>Face ID 因光線不足辨識失敗、指紋因手指潮濕讀取失敗。使用者的生物特徵正常，只是當次辨識條件不佳。重試可能成功。</p>
<h3 id="持續性失敗">持續性失敗</h3>
<p>使用者戴口罩讓 Face ID 無法辨識（較舊的 iOS 版本）、手指受傷影響指紋辨識。生物特徵暫時改變，短期內重試都不會成功。需要替代認證方式。</p>
<h3 id="硬體不可用">硬體不可用</h3>
<p>裝置沒有 Face ID / Touch ID 模組（較舊機型）、模擬器不支援生物辨識、生物辨識功能被裝置管理策略（MDM）禁用。需要替代認證方式。</p>
<h3 id="使用者未設定">使用者未設定</h3>
<p>裝置有硬體但使用者沒有設定 Face ID 或指紋。系統的 <code>canCheckBiometrics</code> 回傳 <code>true</code>（硬體存在）但實際認證會失敗。需要引導使用者設定或提供替代認證。</p>
<h2 id="ios-和-android-的行為差異">iOS 和 Android 的行為差異</h2>
<h3 id="ioslocalauthentication">iOS（LocalAuthentication）</h3>
<p>iOS 的 <code>LAContext.evaluatePolicy</code> 有兩個 policy：</p>
<ul>
<li><code>deviceOwnerAuthenticationWithBiometrics</code>：只接受生物辨識，失敗後不自動提示密碼</li>
<li><code>deviceOwnerAuthentication</code>：先嘗試生物辨識，失敗後系統自動彈出裝置密碼輸入</li>
</ul>
<p>Flutter 的 <code>local_auth</code> 套件的 <code>biometricOnly</code> 參數對應這兩個 policy。<code>biometricOnly: true</code> 用前者，<code>biometricOnly: false</code> 用後者。</p>
<p>iOS 的行為特點：系統控制認證 UI（不是 app 自行繪製），認證失敗次數過多會自動鎖定（需要輸入密碼解鎖），Face ID 多次失敗後系統會自動提供密碼選項（即使 app 要求 biometricOnly）。</p>
<h3 id="androidbiometricprompt">Android（BiometricPrompt）</h3>
<p>Android 的 BiometricPrompt 分成三個 class：</p>
<ul>
<li><code>BIOMETRIC_STRONG</code>：只接受 Class 3 生物辨識（經過硬體安全模組驗證的指紋/面部）</li>
<li><code>BIOMETRIC_WEAK</code>：接受 Class 2 和 Class 3 生物辨識</li>
<li><code>DEVICE_CREDENTIAL</code>：接受裝置 PIN/圖形/密碼</li>
</ul>
<p>三個 class 可以用 <code>|</code> 組合。<code>BIOMETRIC_STRONG | DEVICE_CREDENTIAL</code> 表示先嘗試強生物辨識，失敗後 fallback 到裝置密碼。</p>
<p>Android 的行為特點：不同廠商的生物辨識品質差異大（Samsung 的面部辨識和 Pixel 的面部辨識安全等級不同）、部分裝置的指紋感測器在螢幕下方（使用者可能不知道在哪裡觸碰）。</p>
<h2 id="安全-vs-可用性的顯式取捨">安全 vs 可用性的顯式取捨</h2>
<p><code>biometricOnly</code> 的決策涉及安全和可用性的取捨。這個取捨應該在功能規格中顯式記錄，讓後續的 code review 和維護者能理解決策的背景。</p>
<p>記錄格式建議：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Gate: biometric authentication
</span></span><span class="line"><span class="ln">2</span><span class="cl">Decision: biometricOnly = false (allow device credential fallback)
</span></span><span class="line"><span class="ln">3</span><span class="cl">Security trade-off: device credential (PIN/password) is weaker than biometric
</span></span><span class="line"><span class="ln">4</span><span class="cl">Rationale: self-hosted tool, user = owner, availability &gt; auth strength
</span></span><span class="line"><span class="ln">5</span><span class="cl">Risk accepted: someone with device PIN can access the app</span></span></code></pre></div><p>app_tunnel 選擇 <code>biometricOnly: true</code> 的原始意圖是「安全性更高」，但沒有顯式記錄取捨，也沒有評估「Face ID 不可用時使用者完全無法使用 app」的代價。自用工具的使用者就是 owner，密碼 fallback 的安全風險遠低於完全無法使用的可用性風險（<a href="/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2</a>）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Gate 設計的通用方法論 → <a href="/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法</a></li>
<li>開發環境遮蔽 gate 問題 → <a href="/blog/ux-design/02-gate-fallback/dev-vs-real-gate-behavior/" data-link-title="開發環境 vs 真機的 gate 行為差異表" data-link-desc="模擬器、debug build、test 環境中的 gate 行為和真機 release build 不同 — 差異表讓開發者在上機前知道哪些 gate 還沒被真實驗證">開發環境 vs 真機的 gate 行為差異表</a></li>
<li>安全 vs 可用性在 monitoring 中的對應 → <a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安</a></li>
</ul>
]]></content:encoded></item><item><title>U.C2 biometricOnly=true 無密碼 fallback</title><link>https://tarrragon.github.io/blog/ux-design/cases/biometric-only-no-fallback/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/biometric-only-no-fallback/</guid><description>&lt;p>這個案例的核心責任是說明 Gate（使用者必須通過的關卡）的設計不只是「成功時怎麼做」，還必須包含「失敗時的替代路徑」。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 使用 &lt;code>local_auth&lt;/code> 套件進行生物辨識認證。&lt;code>AuthenticationOptions&lt;/code> 設定 &lt;code>biometricOnly: true&lt;/code>，表示只接受生物辨識（Face ID / 指紋），不接受裝置密碼作為 fallback。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 修復前
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nl">options:&lt;/span> &lt;span class="kd">const&lt;/span> &lt;span class="n">AuthenticationOptions&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nl">stickyAuth:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nl">biometricOnly:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Face ID 不可用 → 認證直接失敗
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">// 修復後
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nl">options:&lt;/span> &lt;span class="kd">const&lt;/span> &lt;span class="n">AuthenticationOptions&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nl">stickyAuth:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nl">biometricOnly:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Face ID 不可用 → 系統自動提示輸入裝置密碼
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">),&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>值&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>影響範圍&lt;/td>
 &lt;td>Face ID 不可用時（戴口罩、光線差、指紋模糊、模擬器）完全無法使用 app&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復成本&lt;/td>
 &lt;td>改一個 boolean&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>根因&lt;/td>
 &lt;td>企劃階段未設計 biometric gate 的 fallback&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Gate fallback 是設計問題，不是實作問題&lt;/strong>。&lt;code>biometricOnly&lt;/code> 的預設值是 &lt;code>false&lt;/code>（允許密碼 fallback），開發時特意改成 &lt;code>true&lt;/code> 是因為認為「安全性更高」。但這個判斷沒有考慮 fallback 缺失時的 UX 代價 — 使用者完全無法進入 app。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>開發環境遮蔽了問題&lt;/strong>。iOS 模擬器預設不支援 Face ID，但 &lt;code>isAvailable()&lt;/code> 的實作會檢查 &lt;code>isDeviceSupported()&lt;/code> + &lt;code>getAvailableBiometrics().isNotEmpty&lt;/code>。模擬器回傳 &lt;code>isDeviceSupported() = true&lt;/code> 但 &lt;code>getAvailableBiometrics() = []&lt;/code>，所以在模擬器上 &lt;code>isAvailable()&lt;/code> 回傳 false，直接跳過認證走預設路徑。真實裝置上 &lt;code>isAvailable() = true&lt;/code> 但 Face ID 可能失敗，這時沒有 fallback。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>安全性 vs 可用性的取捨需要顯式記錄&lt;/strong>。&lt;code>biometricOnly: true&lt;/code> 的安全收益是「確保只有生物特徵擁有者能操作」；代價是「任何生物辨識失敗場景都阻擋使用」。自用工具的使用者就是 owner，密碼 fallback 的安全風險遠低於「完全無法使用」的可用性風險。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>每個 gate 設計時列三問&lt;/strong>：成功時做什麼？失敗時做什麼？使用者不知道發生什麼時做什麼？&lt;/li>
&lt;li>&lt;strong>在狀態矩陣標注 gate fallback&lt;/strong>：biometric / network / auth 每個 gate 旁邊標注替代路徑，空白 = 使用者被擋住。&lt;/li>
&lt;li>&lt;strong>安全 vs 可用性取捨顯式記錄&lt;/strong>：在 spec 文件記錄「&lt;code>biometricOnly: false&lt;/code> — 接受密碼 fallback，因為自用工具可用性優先於生物辨識強制」。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想設計 Gate fallback 體系 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法&lt;/a>&lt;/li>
&lt;li>想了解 biometric 在不同平台的行為差異 → 待補：iOS/Android biometric API 行為對照&lt;/li>
&lt;li>類似案例（導航死胡同）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1 五個狀態零個退出&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 Gate（使用者必須通過的關卡）的設計不只是「成功時怎麼做」，還必須包含「失敗時的替代路徑」。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 使用 <code>local_auth</code> 套件進行生物辨識認證。<code>AuthenticationOptions</code> 設定 <code>biometricOnly: true</code>，表示只接受生物辨識（Face ID / 指紋），不接受裝置密碼作為 fallback。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 修復前
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nl">options:</span> <span class="kd">const</span> <span class="n">AuthenticationOptions</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nl">stickyAuth:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nl">biometricOnly:</span> <span class="kc">true</span><span class="p">,</span>  <span class="c1">// Face ID 不可用 → 認證直接失敗
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">// 修復後
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="nl">options:</span> <span class="kd">const</span> <span class="n">AuthenticationOptions</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nl">stickyAuth:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nl">biometricOnly:</span> <span class="kc">false</span><span class="p">,</span> <span class="c1">// Face ID 不可用 → 系統自動提示輸入裝置密碼
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="p">),</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>指標</th>
          <th>值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>影響範圍</td>
          <td>Face ID 不可用時（戴口罩、光線差、指紋模糊、模擬器）完全無法使用 app</td>
      </tr>
      <tr>
          <td>修復成本</td>
          <td>改一個 boolean</td>
      </tr>
      <tr>
          <td>根因</td>
          <td>企劃階段未設計 biometric gate 的 fallback</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>Gate fallback 是設計問題，不是實作問題</strong>。<code>biometricOnly</code> 的預設值是 <code>false</code>（允許密碼 fallback），開發時特意改成 <code>true</code> 是因為認為「安全性更高」。但這個判斷沒有考慮 fallback 缺失時的 UX 代價 — 使用者完全無法進入 app。</p>
</li>
<li>
<p><strong>開發環境遮蔽了問題</strong>。iOS 模擬器預設不支援 Face ID，但 <code>isAvailable()</code> 的實作會檢查 <code>isDeviceSupported()</code> + <code>getAvailableBiometrics().isNotEmpty</code>。模擬器回傳 <code>isDeviceSupported() = true</code> 但 <code>getAvailableBiometrics() = []</code>，所以在模擬器上 <code>isAvailable()</code> 回傳 false，直接跳過認證走預設路徑。真實裝置上 <code>isAvailable() = true</code> 但 Face ID 可能失敗，這時沒有 fallback。</p>
</li>
<li>
<p><strong>安全性 vs 可用性的取捨需要顯式記錄</strong>。<code>biometricOnly: true</code> 的安全收益是「確保只有生物特徵擁有者能操作」；代價是「任何生物辨識失敗場景都阻擋使用」。自用工具的使用者就是 owner，密碼 fallback 的安全風險遠低於「完全無法使用」的可用性風險。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li><strong>每個 gate 設計時列三問</strong>：成功時做什麼？失敗時做什麼？使用者不知道發生什麼時做什麼？</li>
<li><strong>在狀態矩陣標注 gate fallback</strong>：biometric / network / auth 每個 gate 旁邊標注替代路徑，空白 = 使用者被擋住。</li>
<li><strong>安全 vs 可用性取捨顯式記錄</strong>：在 spec 文件記錄「<code>biometricOnly: false</code> — 接受密碼 fallback，因為自用工具可用性優先於生物辨識強制」。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計 Gate fallback 體系 → <a href="/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法</a></li>
<li>想了解 biometric 在不同平台的行為差異 → 待補：iOS/Android biometric API 行為對照</li>
<li>類似案例（導航死胡同）→ <a href="/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1 五個狀態零個退出</a></li>
</ul>
]]></content:encoded></item><item><title>模組二：Gate 與 Fallback 設計</title><link>https://tarrragon.github.io/blog/ux-design/02-gate-fallback/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/02-gate-fallback/</guid><description>&lt;p>回答「使用者過不了關卡時怎麼辦」。&lt;/p>
&lt;h2 id="對應-findings">對應 findings&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Finding&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>UF-4&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2&lt;/a>&lt;/td>
 &lt;td>biometricOnly 安全收益 vs 可用性代價 — &lt;strong>本模組主寫&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UF-5&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2&lt;/a>&lt;/td>
 &lt;td>開發環境遮蔽 gate 問題（模擬器行為 vs 真機）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Gate 分類與三問設計法（成功 / 失敗 / 使用者不知道發生什麼）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Biometric fallback 完整設計（iOS/Android 差異）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 網路斷線 UX 模式（offline-first / retry / degraded mode）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Permission 請求時機與措辭&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 開發環境 vs 真機的 gate 行為差異表&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/" data-link-title="模組一：測試策略分層" data-link-desc="Unit / Protocol Integration / Screen State 三層測試各自的職責、盲區和判斷原則">testing 模組一 測試策略&lt;/a>：gate fallback 的 mock vs 真機行為差異需要 protocol test&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安&lt;/a>：biometric fallback 的安全 vs 可用性取捨&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「使用者過不了關卡時怎麼辦」。</p>
<h2 id="對應-findings">對應 findings</h2>
<table>
  <thead>
      <tr>
          <th>Finding</th>
          <th>來源</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>UF-4</td>
          <td><a href="/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2</a></td>
          <td>biometricOnly 安全收益 vs 可用性代價 — <strong>本模組主寫</strong></td>
      </tr>
      <tr>
          <td>UF-5</td>
          <td><a href="/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2</a></td>
          <td>開發環境遮蔽 gate 問題（模擬器行為 vs 真機）</td>
      </tr>
  </tbody>
</table>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> Gate 分類與三問設計法（成功 / 失敗 / 使用者不知道發生什麼）</li>
<li><input checked="" disabled="" type="checkbox"> Biometric fallback 完整設計（iOS/Android 差異）</li>
<li><input checked="" disabled="" type="checkbox"> 網路斷線 UX 模式（offline-first / retry / degraded mode）</li>
<li><input checked="" disabled="" type="checkbox"> Permission 請求時機與措辭</li>
<li><input checked="" disabled="" type="checkbox"> 開發環境 vs 真機的 gate 行為差異表</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/testing/01-test-strategy-layers/" data-link-title="模組一：測試策略分層" data-link-desc="Unit / Protocol Integration / Screen State 三層測試各自的職責、盲區和判斷原則">testing 模組一 測試策略</a>：gate fallback 的 mock vs 真機行為差異需要 protocol test</li>
<li>→ <a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安</a>：biometric fallback 的安全 vs 可用性取捨</li>
</ul>
]]></content:encoded></item></channel></rss>