<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Van Thuong Dao</title>
    <description>The latest articles on DEV Community by Van Thuong Dao (@vanthuongdao).</description>
    <link>https://dev.to/vanthuongdao</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4170233%2F25a8d780-1409-4672-8df5-2f7685bf7a0d.webp</url>
      <title>DEV Community: Van Thuong Dao</title>
      <link>https://dev.to/vanthuongdao</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vanthuongdao"/>
    <language>en</language>
    <item>
      <title>Why "wrong passcode" was lying to users of my encrypted vault app</title>
      <dc:creator>Van Thuong Dao</dc:creator>
      <pubDate>Thu, 08 Oct 2026 06:22:46 +0000</pubDate>
      <link>https://dev.to/vanthuongdao/why-wrong-passcode-was-lying-to-users-of-my-encrypted-vault-app-27i5</link>
      <guid>https://dev.to/vanthuongdao/why-wrong-passcode-was-lying-to-users-of-my-encrypted-vault-app-27i5</guid>
      <description>&lt;p&gt;A user wrote in: "I tried to restore my backup and it keeps saying my passcode is wrong. I'm 100% sure it's right." They weren't wrong to be sure. The passcode was fine. My error handling was lying to them.&lt;/p&gt;

&lt;p&gt;Lockboxy (the encrypted vault app I build) lets you back up your vault to a file and restore it later — new phone, reinstall, whatever. Restoring needs your passcode (or a recovery key) to decrypt the backup. The restore screen's error handling looked like this, roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;restoreFromBackup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Incorrect passcode. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple. Also wrong. That catch block fires for &lt;em&gt;any&lt;/em&gt; failure during restore — wrong passcode, sure, but also a corrupted backup file, a backup from an incompatible app version, a truncated file from a failed download, a file that isn't a Lockboxy backup at all. Every single one of those got the same message: blame the passcode.&lt;/p&gt;

&lt;p&gt;So someone with a perfectly correct passcode and a backup file that got mangled in transit would sit there, re-typing a passcode they already know is right, told every time that they're wrong. There's no worse debugging experience than an error message confidently pointing at the wrong cause.&lt;/p&gt;

&lt;p&gt;The fix wasn't complicated — it just required actually having distinct error types instead of one catch-all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;restoreFromBackup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;IncorrectPasscodeError&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;IncorrectRecoveryKeyError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;usingRecoveryKey&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;That recovery key doesn't match this backup.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Incorrect passcode. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Anything else — corrupted file, bad format, whatever — is NOT a credential problem.&lt;/span&gt;
  &lt;span class="nf"&gt;setPickError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This backup file couldn't be read. It may be corrupted or from a different app.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual fix is almost boring: &lt;code&gt;instanceof&lt;/code&gt; checks on a proper error hierarchy (&lt;code&gt;CryptoServiceError&lt;/code&gt; → &lt;code&gt;IncorrectPasscodeError&lt;/code&gt;, &lt;code&gt;IncorrectRecoveryKeyError&lt;/code&gt;, and others) that already existed in the crypto layer. The bug wasn't a missing feature, it was a UI layer that caught everything and collapsed it into one message because that was the easy path.&lt;/p&gt;

&lt;p&gt;Here's the part that makes this more than a generic "handle your errors properly" lesson: Lockboxy is zero-knowledge. I don't hold your passcode, I can't reset it, and if you truly forget it, your data is gone — that's the whole point of the security model. Which means when a user sees a credential error, the ONLY two things that can possibly be true are "I mistyped it" or "I forgot it." There's no third option, no "contact support to reset it," nothing. The support cost of a misleading error message is much higher here than in an app where someone can just click "forgot password." If my error message blames the passcode and it's actually the file, the user has no way to find that out themselves — they'll assume their data is unrecoverable and give up, when the actual fix might be as simple as re-downloading the backup file.&lt;/p&gt;

&lt;p&gt;Lesson, stated plainly: if your app can't offer a human a way around a problem, your error message is the only diagnostic tool they have. It has to be right, not just plausible.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ios</category>
      <category>swift</category>
      <category>programming</category>
    </item>
    <item>
      <title>Apple rejected my TestFlight upload — because of a version I'd already shipped</title>
      <dc:creator>Van Thuong Dao</dc:creator>
      <pubDate>Thu, 08 Oct 2026 05:08:18 +0000</pubDate>
      <link>https://dev.to/vanthuongdao/apple-rejected-my-testflight-upload-because-of-a-version-id-already-shipped-l9i</link>
      <guid>https://dev.to/vanthuongdao/apple-rejected-my-testflight-upload-because-of-a-version-id-already-shipped-l9i</guid>
      <description>&lt;p&gt;I spent half an hour yesterday convinced something was broken on my end. It wasn't. Apple just doesn't tell you this rule anywhere obvious.&lt;/p&gt;

&lt;p&gt;Here's what happened:&lt;/p&gt;

&lt;p&gt;I'd fixed two real bugs in my app (Lockboxy, an encrypted vault for Mac/iOS) — one was a confusing error message, the other added zip preview support. Normal patch release. Same version number as my last TestFlight build (1.2.15), since nothing user-facing about the &lt;em&gt;version&lt;/em&gt; had changed — just a new build number.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;xcodebuild -exportArchive&lt;/code&gt; failed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: exportArchive
code 90186: Invalid Pre-Release Train. The train version
'1.2.15' is closed for new build submissions.

error: exportArchive
code 90062: ... must contain a higher version than
that of the previously approved version.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both errors, same root cause: &lt;strong&gt;1.2.15 had already been approved and released live on the App Store.&lt;/strong&gt; Once a version number ships, Apple closes its "pre-release train" — permanently. Doesn't matter that I only wanted it for TestFlight. Doesn't matter that no one outside my test group would ever see it. The version number itself is burned the moment it goes live.&lt;/p&gt;

&lt;p&gt;The fix is simple once you know it: bump to a new version number (1.2.16), even for an internal TestFlight-only build. But here's the part that actually cost me time — &lt;strong&gt;the version number is baked into the binary at archive time, not export time.&lt;/strong&gt; I bumped &lt;code&gt;MARKETING_VERSION&lt;/code&gt; in the Xcode project, re-ran &lt;code&gt;-exportArchive&lt;/code&gt; against my &lt;em&gt;existing&lt;/em&gt; archive, and it exported fine with no error — but silently still reported the old version inside the binary. I had to throw away the archive and run &lt;code&gt;xcodebuild archive&lt;/code&gt; again from scratch before export would actually reflect the new version.&lt;/p&gt;

&lt;p&gt;So the real sequence, if you hit this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bump your marketing version (and build number, while you're at it) in Xcode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-archive from scratch.&lt;/strong&gt; Don't reuse an old &lt;code&gt;.xcarchive&lt;/code&gt; — &lt;code&gt;MARKETING_VERSION&lt;/code&gt; is burned in at archive time.&lt;/li&gt;
&lt;li&gt;Then export/upload as normal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One side effect worth knowing if your app shows release notes in-app ("What's New"): if your code asserts the current app version matches the latest entry in your changelog data (mine does, as a test), bumping the version means you need a new changelog entry too — which, if you support multiple locales like I do (10 languages), means translating "What's New" into all of them before you can ship. Small cascade, easy to forget.&lt;/p&gt;

&lt;p&gt;Nothing about this is documented clearly in one place — it's scattered across old Apple Developer Forums threads and Stack Overflow answers from 2019. If you're staring at code 90186 or 90062 right now: it's not your signing, not your provisioning profile, not your export options plist. It's just the version number. Bump it, re-archive, move on.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>indiehackers</category>
      <category>mobiledev</category>
    </item>
  </channel>
</rss>
