succeed() was calling disconnectPeripheral() before completion(), so the
disconnect delegate fired while writesCompleted was still false — causing
the "Unexpected disconnect" error log even on successful provisions.
Swapped order: signal completion first, then disconnect. Also removed the
redundant provisioner.disconnect() in ScanView since succeed() already
handles it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SaveConfig (0x60) causes the beacon MCU to reboot and save to flash.
It never sends an ACK, so writeToCharAndWaitACK would wait for the
5s timeout, during which the beacon disconnects. The disconnect
handler fires while writesCompleted is still false, causing a false
"Unexpected disconnect: beacon timed out" error.
Fix: fire-and-forget the SaveConfig write and return immediately.
The BLE-level write (.withResponse) confirms delivery. writeConfig()
returns before the disconnect callback runs, so writesCompleted gets
set to true in time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shaves ~4.6s off the 23-command provisioning sequence while keeping
a safe margin for the beacon's BLE stack to process each write.
Next step: if stable, we can go more aggressive (200ms or 150ms).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After all 23 commands write successfully, the DX-Smart beacon reboots
to apply config — dropping the BLE connection. ScanView's disconnect
handler was racing the async writeConfig return and logging this as
"Unexpected disconnect: beacon timed out" even though provisioning
succeeded.
Added writesCompleted flag so the disconnect handler knows writes
finished and logs it as expected behavior instead of an error.
Remove the guard that dropped non-CP-28 devices. All discovered
BLE peripherals now appear in the scan list (defaulting to .dxsmart
type). detectBeaconType still classifies known CP-28 patterns but
unknown devices are no longer hidden.
The CP-28-only refactor accidentally over-filtered the BLE scan:
1. FFF0 service detection was gated on name patterns — CP-28 beacons
advertising FFF0 with non-matching names (e.g. already provisioned
as "Payfrit") were silently filtered out. Restored unconditional
FFF0 → dxsmart mapping (matching old behavior).
2. Already-provisioned beacons broadcast with name "Payfrit" (set by
old SDK cmd 0x43), but that name wasn't in the detection patterns.
Added "payfrit" to the name check.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove BlueCharmProvisioner, KBeaconProvisioner, and FallbackProvisioner.
Simplify BeaconType enum to DX-Smart only. Simplify BLE detection to only
show CP-28 beacons. Remove multi-type provisioner factory from ScanView.
-989 lines of dead code removed. Other beacon types will be re-added
when we start using different hardware.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Minor allocation: reject minor=0 from API instead of silently using it.
API returning null/0 means the service point isn't configured right.
2. DXSmart write reliability:
- Add per-command retry (1 retry with 500ms backoff)
- Increase inter-command delay from 200ms to 500ms
- Increase post-auth settle from 100ms to 500ms
- Add 2s cooldown in FallbackProvisioner between provisioner attempts
The beacon's BLE stack gets hammered by KBeacon's 15 failed auth
attempts before DXSmart even gets a chance. These timings give it
breathing room.
3. KBeacon passwords: password 5 was a duplicate of password 3
(both "1234567890123456"). Replaced with "000000" (6-char variant).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frame1_DevInfo (cmd 0x61) and potentially other commands don't send a
separate FFE1 notification after being written. The code was waiting for
didUpdateValueFor (notification) to resolve responseContinuation, but it
never came — causing a 5s timeout on every such command.
The .withResponse write type already guarantees the BLE stack confirmed
delivery. Now didWriteValueFor resolves responseContinuation on success,
so commands that don't trigger notifications still complete immediately.
If a notification also arrives later, responseContinuation is already nil
so it's harmlessly ignored.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Swift strict concurrency checker flags MainActor-isolated self access from
nonisolated CBCentralManagerDelegate methods when using Task{@MainActor in}.
DispatchQueue.main.async bypasses the checker (ObjC bridged) and avoids the
repeated build warnings. Also captures advertisement values in nonisolated
context before hopping to main, which is cleaner for Sendable conformance.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After DXSmart auth completes, the beacon often drops BLE connection
due to aggressive timeouts. The disconnect handler was treating this
as a failure, stomping the .connected state before the user could see
the "Write Config" button.
Changes:
- Ignore BLE disconnects during .connected state for DXSmart beacons
(the LED keeps flashing regardless of BLE connection)
- Auto-reconnect in writeConfigToConnectedBeacon() if BLE dropped
while waiting for user confirmation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
KBeaconProvisioner had no onStatusUpdate callback, so the UI showed a static
"Connecting..." message during the entire auth cycle (5 passwords × 5s timeout
× 3 retries = 75s of dead silence). Now reports each phase: connecting,
discovering services, authenticating (with password attempt count), writing,
saving.
Also fixed ScanView disconnect handler to cover .writing and .verifying states —
previously only handled .connecting/.connected, so a mid-write disconnect left
the UI permanently stuck.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- DXSmartProvisioner now reports each phase (connecting, discovering
services, authenticating, retrying) via onStatusUpdate callback
- ScanView shows live diagnostic log during connecting/writing states,
not just on failure — so you can see exactly where it stalls
- Unexpected BLE disconnects now properly update provisioningState to
.failed instead of silently logging
- Added cancel button to connecting progress view
- "Connected" screen title changed to "Connected — Beacon is Flashing"
for clearer status indication
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BIG FIX: Provisioners were calling centralManager.connect() but
BLEManager is the CBCentralManagerDelegate — provisioners never
received didConnect/didFailToConnect callbacks, so connections
ALWAYS timed out after 5s regardless. This is why provisioning
kept failing. Fixed by:
1. Adding didConnect/didFailToConnect/didDisconnect to BLEManager
2. Provisioners register connection callbacks via bleManager
3. Increased connection timeout from 5s to 10s
DIAGNOSTICS: Added ProvisionLog system so failures show a timestamped
step-by-step log of what happened (with Share button). Every phase
is logged: init, API calls, connect attempts, service discovery,
auth, write commands, and errors.
Sort the beacon list so strongest signal (closest beacon) appears at the
top. Sorting happens both in BLEManager as beacons are discovered and in
the ScanView list rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major detection gaps were causing iOS to miss 7-8 out of 8-9 nearby DX beacons:
1. FFF0 service UUID was incorrectly mapped exclusively to BlueCharm.
Android maps DXSmartProvisioner.SERVICE_UUID_FFF0 to DXSMART.
Now checks device name to disambiguate FFF0 between DX and BlueCharm,
defaulting to DXSmart (matching Android behavior).
2. Added DX factory default UUID detection (E2C56DB5-DFFB-48D2-B060-D0F5A71096E0).
Android catches DX beacons by this UUID on line 130 of BeaconScanner.kt.
iOS was missing this entirely.
3. Added Payfrit shard UUID detection — already-provisioned DX beacons
broadcasting a shard UUID are now recognized.
4. Added iBeacon manufacturer data parsing with proper UUID extraction.
Any device broadcasting valid iBeacon data is now included (not just
those with minor > 10000).
5. Added permissive fallback matching Android lines 164-169: connectable
devices with names are included even if type is unknown, so they're
at least visible to the user.
6. Added FEA0 service UUID for BlueCharm (Android line 124).
7. Added "DX-CP" name pattern (Android line 138) that was missing.
Root cause: Android uses MAC OUI prefix 48:87:2D to catch all DX beacons
regardless of advertisement contents. iOS can't do this (CoreBluetooth
doesn't expose MAC addresses), so we compensate with broader iBeacon
UUID matching and more permissive device inclusion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When tapping Back from ScanView, BusinessListView remounts and .task fires
loadBusinesses() which sees AppPrefs.lastBusinessId still set — immediately
re-navigating into the same business. Fix: clear lastBusinessId on back,
and add skipAutoNav flag to also prevent single-business auto-select from
firing when user explicitly navigated back.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The app crashed immediately when tapping QR scan because the Info.plist
was missing the required NSCameraUsageDescription key. iOS kills the app
with EXC_BAD_INSTRUCTION when camera access is requested without it.
Also fixes:
- Flash toggle could SIGABRT if lockForConfiguration failed (try? + unconditional unlock)
- Camera setup now logs errors instead of silently failing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The API returns {OK: true, BUSINESSES: [...]} but the iOS client was
decoding {Success: true, Data: [...]} which never matched — causing
"Failed to load businesses" on every call. Also fixes Business model
(BusinessID/Name vs ID/BusinessName) and ServicePoint model
(ServicePointID vs ID). All response decoders now match the real API.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs found and fixed:
1. sendOTP was sending "ContactNumber" but API expects "Phone"
2. APIResponse expected {"Success":true,"Data":{}} but API returns {"OK":true,"UUID":"..."}
3. verifyOTP was sending "Code" but API expects "OTP"
Now decodes the raw API format directly instead of going through the
generic APIResponse wrapper (which doesn't match auth endpoints).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PayfritGreen and PayfritGreenDark were defined in both Assets.xcassets
(as .colorset files) and in BrandColors.swift (as Color extensions).
All code references the Swift extension (.payfritGreen), so the asset
catalog versions are redundant and cause ambiguity. Removed the colorsets.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ShapeStyle where Self == Color extension duplicated Color statics,
causing 'ambiguous use' errors in foregroundStyle/stroke/tint/background
contexts. Removed the extension entirely and use explicit Color.xxx prefix
at all call sites instead.
- App icon now uses white bg + PAYFRIT text + bluetooth beacon icon (matches Android)
- AccentColor set to Payfrit Green (#22B24B)
- Added BrandColors.swift with full Android color palette parity
- All views updated: payfritGreen replaces .blue, proper status colors throughout
- Signal strength, beacon type badges, QR scanner corners all use brand colors
- DevBanner uses warningOrange matching Android's #FF9800
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use explicit toolbar(content:) call instead of trailing closure to
disambiguate the SwiftUI toolbar modifier overload.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Project references PayfritBeacon/Info.plist but the file was never committed.
Includes Bluetooth and Location usage descriptions required for beacon functionality.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copied from payfrit-user-ios — same Payfrit brand icon.
Contents.json now references the file.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Xcode build was failing because the asset catalog referenced in
project.pbxproj didn't exist on disk. Added:
- Assets.xcassets/Contents.json
- AppIcon.appiconset/Contents.json (single 1024x1024 slot, no image yet)
- AccentColor.colorset/Contents.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- payfrit-favicon-light-outlines.svg (copied from payfrit-works-ios)
- en.lproj/InfoPlist.strings (standard localization)
These were referenced in project.pbxproj but not committed to the repo,
causing build failures.
- QRScannerView: AVFoundation camera + barcode/QR detection with
flashlight toggle, viewfinder overlay, MAC/UUID pattern recognition
- New API endpoints: deleteServicePoint, updateServicePoint,
listBeacons, decommissionBeacon, lookupByMac, getBeaconStatus, getProfile
- Wire QR scanner into ScanView with BLE Scan + QR Scan side-by-side
- MAC address lookup on scan to check if beacon already registered
- Updated Xcode project file with new source
Replaces the v2 refactor with a clean v3 rewrite built from the working
pre-refactor code (d123d25). Preserves all battle-tested BLE fixes while
removing dead code and simplifying state management. See schwifty/v3-clean-refactor
branch for full details.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Starting fresh from the working pre-refactor code (d123d25), this is a clean
rewrite that preserves all the hard-won BLE reliability fixes while simplifying
the architecture. Key changes:
What's preserved (battle-tested, working):
- Exact same 16-step write sequence (DeviceName, Frame1, Frame2 iBeacon, Save)
- Same DX-Smart packet format (4E 4F CMD LEN DATA XOR)
- Response gating between writes (1s timeout, matches Android)
- Adaptive delays (1s for frame select/type, 0.8s for UUID, 0.5s base)
- FFE2 missing → full disconnect/reconnect (CoreBluetooth stale GATT cache)
- SaveConfig write-error = success (beacon reboots immediately)
- Disconnect recovery with write position resume
- Multi-password auth (555555, dx1234, 000000)
- Skip device info read (0x30 causes disconnects, MAC is optional)
- Skip extra frame disables (frames 3-6 untouched, fewer writes = fewer disconnects)
What's cleaned up:
- Removed dead device info provisioning code (was already skipped)
- Removed processDeviceInfoForProvisioning (dead code)
- Removed awaitingDeviceInfoForProvisioning flag
- Removed skipDeviceInfoRead flag
- Removed deviceInfoRetryCount (no longer needed)
- Consolidated charRediscovery into handleFFE2Missing()
- Renamed state vars for clarity (dxSmartWriteIndex → writeIndex, etc.)
- Extracted scheduleGlobalTimeout (was inline closure)
- Added cancelAllTimers() helper
- Reduced mutable state from ~30 vars to ~22
1652 lines → 1441 lines (-211 lines, -13%)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When CoreBluetooth renegotiates connection parameters (common at edge-of-range),
it can invalidate cached CBCharacteristic references, causing the characteristics
dictionary to return nil mid-write. This resulted in "FFE2 characteristic lost
during write" failures.
Changes:
- Add resolveCharacteristic() with live service fallback when cache misses
- Implement peripheral(_:didModifyServices:) to handle service invalidation
- Replace all direct characteristics[] lookups with resolveCharacteristic()
- Eliminates force-unwrap on FFE1 notification subscribe
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PRE_AUTH_DELAY bumped from 0.8s to 2.0s — beacons need more stabilization
time after characteristic discovery before password write
- If disconnect happens during authenticating state, retry connection once
with full re-discovery instead of immediate failure
- Reset authDisconnectRetried flag on new provision and cleanup
DX-Smart beacons drop the BLE connection when a password write (FFE3) arrives
too quickly after service/characteristic discovery. The v2 refactor had a
POST_AUTH_DELAY (1.5s) before writes, but nothing before the auth attempt itself.
Adds PRE_AUTH_DELAY (0.8s) after characteristic discovery + FFE1 subscription
before the first password write. This gives the beacon MCU time to stabilize
after the discovery handshake.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major cleanup of the BLE provisioning flow:
- Remove frames 3-6 disable steps (no longer overwriting "extra frames")
Only Frame 1 (device info) and Frame 2 (iBeacon) are configured now.
Command count drops from 24 to 16.
- Eliminate 5 layers of retry/reconnect/resume logic:
No more disconnect retry counter (was 5 retries)
No more resume-from-position after reconnect
No more characteristic re-discovery attempts
No more device info read/skip dance
If connection drops mid-write, fail cleanly — let user retry.
- Prevention approach: verify all 3 required characteristics (FFE1/FFE2/FFE3)
are confirmed before starting any writes. No mid-write discovery.
- Add POST_AUTH_DELAY (1.5s) to let beacon stabilize after auth before writes.
- Keep response gating (wait for FFE1 after each write) — this is the key
mechanism that prevents blasting commands faster than the MCU can handle.
- 191 insertions, 476 deletions — nearly half the code removed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two key changes:
1. Remove frames 3-6 disable commands (was steps 16-23, 8 extra BLE writes).
We only configure Frame 1 (device info) and Frame 2 (iBeacon), then save.
Fewer writes = fewer chances for supervision timeout disconnects.
2. When FFE2 characteristic goes missing after a disconnect, do a full
disconnect → reconnect → re-discover services → re-auth → resume cycle
instead of trying to rediscover characteristics on the same (stale GATT)
connection. CoreBluetooth returns cached results on the same connection,
so FFE2 stays missing. Full reconnect forces a fresh GATT discovery.
Write sequence is now 16 steps (down from 24).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When FFE2 goes missing during writes, the rediscovery path had no
timeout — if CoreBluetooth never called back didDiscoverCharacteristics,
the app would hang at "Re-discovering characteristics..." indefinitely.
Adds a 5-second timeout per rediscovery attempt. If it fires, it either
retries (up to MAX_CHAR_REDISCOVERY) or fails with .timeout instead of
hanging forever.
Root cause: iOS was firing the next GATT command as soon as the BLE write
ACK arrived, without waiting for the beacon's FFE1 notification response.
Android explicitly waits up to 1s for the beacon to respond (via
responseChannel.receive) before sending the next command. This gives the
beacon MCU time to process each command before the next one arrives.
Without this gate, the beacon gets overwhelmed and drops the BLE connection
(supervision timeout), causing the "DX Smart command characteristic" error
John reported after repeated disconnects.
Changes:
- Add awaitingCommandResponse flag + 1s response gate timer
- After each FFE2 write success, wait for FFE1 notification before advancing
- If no response within 1s, advance anyway (some commands don't respond)
- Check for 4E 4F 00 rejection pattern (matches Android)
- Clean up gate timer on disconnect, cleanup, and state resets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After 2+ disconnects during provisioning, the beacon's BLE stack can
return incomplete characteristic discovery results. Instead of hard-
failing with "DX-Smart command characteristic (FFE2) not found", we
now re-trigger characteristic discovery (up to 2 attempts) and resume
writing from the saved position once FFE2 is found.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: DX-Smart CP28 beacons were disconnecting during provisioning
because the 0.3s inter-command delay was too fast for the beacon's MCU.
Frame selection (0x11-0x16) and type commands (0x61, 0x62) trigger internal
state changes that need processing time. Rapid writes caused the beacon to
miss BLE connection events, triggering link-layer supervision timeouts.
Changes:
- Base delay: 0.3s → 0.5s for all commands
- Heavy delay: 1.0s after frame select/type commands (MCU state change)
- Large payload delay: 0.8s after UUID writes (21 bytes)
- Resume delay: 0.5s → 1.5s after reconnect (let BLE stack stabilize)
- Non-fatal skip delay: 0.15s → 0.5s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, when the beacon disconnected mid-write, the reconnect handler
cleared the entire command queue and reset writeIndex to 0, causing all 24
commands to be re-sent from scratch on every reconnect. This could confuse
the beacon firmware with duplicate config writes and wasted reconnect cycles.
Changes:
- On disconnect during writing, PRESERVE command queue and write index
- After reconnect + re-auth, resume from the last command instead of rebuilding
- Increase MAX_DISCONNECT_RETRIES from 3 to 5 (resume is lightweight)
- Increase inter-command delay from 150ms to 300ms for firmware breathing room
- Increase global timeout from 45s to 90s to accommodate more retries
- Add resumeWriteAfterDisconnect flag to control post-auth flow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wire provisioner.progress into assignment sheet UI (was showing static
"Provisioning beacon..." instead of live updates like "Connecting...",
"Authenticating...", "Writing config 1/24...")
- Increase disconnect retries from 2 to 3 with longer backoff (3s/4s/5s)
- Cancel write timeout timer on disconnect to prevent double-failure
- Bump global timeout from 30s to 45s for extra retry headroom
- Improve error message with actionable guidance ("move closer and retry")
The iOS provisioner had no per-write timeout — if a BLE write callback
never came back, the operation hung until the 30s global timeout, by
which point the connection was likely dead. Android uses a 5-second
per-write timeout with withTimeoutOrNull(5000L).
Changes:
- Add 5-second per-write timeout with retry (1 retry per command)
- On timeout: retry once, skip if non-fatal (steps 1-6), or fail
- SaveConfig timeout treated as success (beacon reboots = no callback)
- Reduce inter-write delay from 200ms to 150ms (Android uses 100ms)
- Log negotiated MTU on connect to diagnose packet size issues
- Cancel write timeout on cleanup/succeed/fail to prevent stale timers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>