Squirreleye LLC Squirreleye Service Notes Naked Labs Mirror · Revival Series Index Tools About
07
Section 07 · follows 06

Credentials belong at runtime, not in source

Written
2026-05-03
Commit
2280a9c

The previous post replaced step 13’s harmful fake-URL write with a WiFi configuration step that read three credential constants from the top of DebugChecklistView.swift. Partway through, I stopped and reconsidered: I didn’t want to hardcode my wifi password into a project I was planning to open source. What I actually needed was something like a .env file, or better: give the debug app the ability to connect the mirror to wifi itself, the same way the production app does.

Good instinct, in hindsight. This post is about why, and about the small SwiftUI sheet that took its place.

§ 7.1

What was wrong with the constants

Three private let declarations at the top of the file:

Fig. 7-1
swift
private let TEST_WIFI_SSID: String = "" // e.g. "<my-home-wifi>"private let TEST_WIFI_SECURITY: String = "4" // 4 = WPA/WPA2 by defaultprivate let TEST_WIFI_PASSWORD: String = "" // your wifi password

Empty by default, // EDIT ME comments. As a quick way to get past the WiFi precondition, this works. As a pattern in a codebase that’s about to go public, it has at least four problems and probably more:

  1. The constants are committable. Even with the most careful workflow, eventually someone’s local copy gets pushed with credentials still in it. There’s no enforcement mechanism, just hope and git diff.
  2. Sample SSIDs leak network identifiers. The example value was my actual home network name. It was already showing up in step 14’s logs and in my blog drafts. Putting it into source as an example would have made it permanent.
  3. The “edit before running” workflow doesn’t survive a git pull. Anyone using the project would have to re-edit after every update unless they kept their local changes carefully stashed.
  4. It’s the wrong shape for the actual use case. I have a WiFi list I can pick from (step 14 emits it). The mirror knows the security type. The only thing source can’t reasonably know is the password. A picker plus a password field is shorter and clearer than three constants.

A .env-style approach (read credentials from a gitignored file) would address (1) but iOS apps don’t naturally have a place to drop a .env, and the workflow would require sideloading a file alongside the build. More overhead than necessary when the official app has already shown the right pattern: an in-app picker.

§ 7.2

What the official app does

ScanInitiationActivity.java and the surrounding fragment classes show the OOBE flow at a high level: scan for available networks, present them to the user, take a password input, write to the mirror, watch for the connection state. The same protocol from NakedV1ConnectToNetworkOperation:

Fig. 7-2
java
this.mBleManager.doWriteOperation( CHAR_CONFIG_WIFI_SET_SSID_UUID, // 62FD7453-… String.format("%s/%s/%s", ssid, securityType, password).getBytes(), callback);

UTF-8 string, slash-separated, written to one characteristic. After the write, the mirror walks wifiConnectionStatus through its enum cases (INVALID(0), CONNECTED(1), CONNECTED_NO_INTERNET(2), CONFIGURED_BAD_AUTH(3), NOT_CONNECTED(4), NOT_CONFIGURED(5), CONNECTING(6)), and I watch.

Fig. 7-3
sequenceDiagram
    participant Phone
    participant Mirror
    Phone->>Mirror: write "ssid/security/password" to wifiSetSSID (62FD7453-…)
    Mirror-->>Phone: notify wifiConnectionStatus = CONNECTING (6)
    Note over Mirror: mirror attempts to join the network
    Mirror-->>Phone: notify wifiConnectionStatus = CONNECTED (1) or CONFIGURED_BAD_AUTH (3)

Mirroring this in the diagnostic was about a hundred lines of SwiftUI plus two new engine methods.

§ 7.3

What’s in the sheet

A new toolbar button (a wifi SF Symbol) in DebugChecklistView opens a WifiSetupSheet:

  • On first appear (and on a refresh button), call engine.discoverWifiNetworks(). This is the same write-then-read-loop that step 14 uses, factored out so the sheet can refresh independently. Returns a [DiscoveredWifiNetwork] parsed from the mirror’s "index/SSID/security/signal" format.
  • Render a sortable list of networks (signal strength descending). Each row shows the SSID, the mirror’s security type integer, the mirror’s index for that network, and the signal as a percentage with a wifi-strength SF Symbol next to it.
  • Tapping a row reveals a Form with a SecureField for the password. .autocorrectionDisabled(), .textInputAutocapitalization(.never), the standard pair of annotations that tell iOS “this is a credential, don’t help.”
  • A Connect button calls engine.connectToWifi(ssid:security:password:). The result is one of a typed enum:
Fig. 7-4
swift
enum WifiConnectionResult { case connected case badAuth // explicit .configuredBadAuth case timedOut(lastSeen: WifiConnectionStatus?) case writeError(String) case missingChars case discoveryFailed(String)}

The sheet renders the result inline: green status text on success, red error text naming the specific cause on failure. No state stored beyond the sheet’s own @State properties; close it and the password is gone.

The toolbar button is disabled when there’s no connected peripheral. The engine grew a new @Published var isPeripheralConnected flipped in didConnect / didDisconnect / reset() to drive that gating, since peripheral.state isn’t observable on its own.

§ 7.4

What step 13 became

Step 13’s purpose has been revised three times across this project, and each revision has been a step toward the right shape:

  1. Original: “Write setScanToken (dry run)”, a write of a fake URL to cloudToken to prove the BLE write path works. Side effect: triggers scanState=.error / scanError=.processorBusy every run, poisoning the mirror’s state for step 15. Removed once it became clear the dry-run write was doing more harm than good.
  2. Brief intermediate: “Configure WiFi (skipped unless TEST_WIFI_* set)”, the hardcoded-credentials version. The pivot away from this is what this post is about.
  3. Current: “WiFi connected”, a pure read of wifiConnectionStatus. Pass if byte 1 (.connected), fail with a pointer to the toolbar button otherwise. No writes, no state changes, no credentials in source.

The general pattern: any diagnostic step that mutates state will eventually get in the way of subsequent steps. Read steps are safe to repeat, idempotent, and don’t require any external configuration. When the choice is “make this step do interesting work” versus “let you trigger interesting work elsewhere,” in a debug tool the second option is almost always cleaner.

§ 7.5

What this build’s next log will show

With no source-side credentials, the diagnostic will not configure WiFi on its own. Step 13 will fail (.notConnected) and step 15 will fail at its own pre-flight WiFi check. That’s correct behavior: failing fast and pointing at the right action.

My flow now:

  1. Run All → step 13 fails with .notConnected, the diagnostic stops at step 15
  2. Tap the WiFi button in the toolbar → the sheet opens
  3. Pick an SSID from the list, type the password, tap Connect
  4. Sheet shows polling progress and the final result inline
  5. Close the sheet, tap Run All again → step 13 should now pass (.connected), step 15 should get past its WiFi precondition for the first time

Whether step 15 reaches .complete then depends on whatever the next unmet precondition is, probably the scale being on the floor in the cameras’ field of view rather than docked on USB-C charging. I’ll find out in the next log.

§ 7.6

Takeaways

  • Credentials in source is the wrong default for any project that might go public. // EDIT ME placeholders look harmless until they’re not. If you find yourself writing one, the better question is whether the value belongs at runtime instead of compile time.
  • Mirror the official client’s UX where you can. I’ve now applied this rule three times (to the BLE protocol map, to the scan-trigger sequence, and to WiFi setup). Each time it was a smaller change than building from intuition would have been, and the resulting behavior is closer to what the device expects to see.
  • A debug tool’s UI doesn’t have to be elegant, but it does have to expose the operations the device requires. The WifiSetupSheet is intentionally plain: a list, a password field, a button, an inline result. No theming, no animations beyond what SwiftUI does for free. Total: about 140 lines of view code. Most of the value is in having the operation rather than how it looks.
  • Read steps are safe to repeat. Write steps are radioactive after one use. Step 13’s three revisions all moved toward the same answer: a precondition check should read state, not change it. Anything that changes state should be triggered intentionally by the user, not as part of an automated walk through the checklist.

The next log will be the first one where I can configure WiFi without recompiling. If WiFi is the only remaining missing precondition, that log shows the mirror moving past alignScale for the first time and I get a scanState walk. If there’s still another precondition, the failure will be specific and I’ll get the next iteration’s hint from it directly.

§ 7.7
§ 7.8
Agent prompts Procedure 7-A as prompts for a coding agent · 6 prompts

Paste into Claude Code or a similar agent, one at a time, from the root of your iOS project. Each prompt maps to the step with the same number above.

1
Remove hardcoded WiFi credentials
Search DebugChecklistView.swift (and any other Swift file) for private `let` constants holding a WiFi SSID or password, such as TEST_WIFI_SSID / TEST_WIFI_PASSWORD. Delete them, including empty-string "EDIT ME" placeholders, and replace any step that reads them with a pure read of wifiConnectionStatus instead.
2
Extract discoverWifiNetworks as a standalone method
Factor the WiFi-discovery body (write Command.getWifiNetworks 0x31 to scannerAction, read wifiNumNetworks, loop-read wifiNetworkSSIDEnum for each index) out of stepRequestWifiNetworks into a standalone public method, e.g. `func discoverWifiNetworks() async -> Result<[DiscoveredWifiNetwork], Error>`, callable directly from UI code without going through the stepped Run All wrapper.
3
Build WifiSetupSheet
Create a SwiftUI `WifiSetupSheet` view: on appear and via a refresh button call `engine.discoverWifiNetworks()`, render the resulting networks in a List sorted by signal strength descending (SSID, security type, index, signal percentage), and on row tap reveal a Form with a SecureField for the password (`.autocorrectionDisabled()`, `.textInputAutocapitalization(.never)`) plus a Connect button.
4
Implement connectToWifi matching official format
Add `engine.connectToWifi(ssid:security:password:)` that writes a UTF-8 string formatted exactly as "%@/%@/%@" (ssid/securityType/password) to the WiFi-configuration characteristic (62FD7453-…), matching NakedV1ConnectToNetworkOperation.java's String.format call byte-for-byte.
5
Surface a typed WifiConnectionResult
Define a `WifiConnectionResult` enum with cases `.connected`, `.badAuth`, `.timedOut(lastSeen: WifiConnectionStatus?)`, `.writeError(String)`, `.missingChars`, `.discoveryFailed(String)`. After connectToWifi's write, poll or subscribe to wifiConnectionStatus and map its byte values (1 = connected, 3 = configuredBadAuth, etc.) to this enum, then render the specific case in WifiSetupSheet instead of a generic success/failure message.
6
Convert write-test steps to pure reads
Find any diagnostic step that writes a value purely to test a BLE path that's already exercised elsewhere (e.g. the old step 13 WiFi-credential write). Replace it with a pure read of the relevant characteristic's current status (wifiConnectionStatus byte 1 = .connected) that passes or fails without mutating mirror state, and point the failure detail at the UI control (e.g. the toolbar WiFi button) the user should use instead.