Commit graph

34 commits

Author SHA1 Message Date
6832a8ad53 feat: add QR scanner view + 7 missing API endpoints
- 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
2026-03-22 17:17:49 +00:00
cfa78679be feat: complete rebuild of PayfritBeacon iOS from scratch
100% fresh codebase — no legacy code carried over. Built against
the Android beacon app as the behavioral spec.

Architecture:
- App: SwiftUI @main, AppState-driven navigation, Keychain storage
- Views: LoginView (OTP + biometric), BusinessListView, ScanView (provisioning hub)
- Models: Business, ServicePoint, BeaconConfig, BeaconType, DiscoveredBeacon
- Services: APIClient (actor, async/await), BLEManager (CoreBluetooth scanner)
- Provisioners: KBeacon, DXSmart (2-step auth + flashing), BlueCharm
- Utils: UUIDFormatting, BeaconBanList, BeaconShardPool (64 shards)

Matches Android feature parity:
- 4-screen flow: Login → Business Select → Scan/Provision
- 3 beacon types with correct GATT protocols and timeouts
- Namespace allocation via beacon-sharding API
- Smart service point naming (Table N auto-increment)
- DXSmart special flow (connect → flash → user confirms → write)
- Biometric auth, dev/prod build configs, DEV banner overlay

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:13:36 +00:00
66053508d3 refactor: v3 BeaconProvisioner — clean restart from pre-refactor baseline
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>
2026-03-22 05:08:21 +00:00
37e3364e1e fix: resolve FFE2 characteristic lost during write race condition
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>
2026-03-22 04:48:04 +00:00
fc13986396 fix: increase pre-auth delay to 2s and add one-shot reconnect on auth disconnect
- 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
2026-03-22 04:37:52 +00:00
972291e93b fix: add pre-auth stabilization delay to prevent disconnect during authentication
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>
2026-03-22 04:28:52 +00:00
1e3e0198b1 refactor: v2 BeaconProvisioner — prevention over recovery
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>
2026-03-22 03:57:02 +00:00
41c26acad3 fix: add response gating between BLE writes to prevent beacon disconnects
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>
2026-03-22 03:08:53 +00:00
9319cad2d6 fix: retry characteristic discovery when FFE2 missing after disconnect
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>
2026-03-22 03:03:08 +00:00
bfbc2a5d8c fix: adaptive inter-command delays to prevent BLE supervision timeouts
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>
2026-03-22 02:53:37 +00:00
aeab67ea64 fix: resume writing from saved position on BLE disconnect instead of restarting
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>
2026-03-22 02:48:42 +00:00
cb3e8107fc fix: show live provisioning progress + improve disconnect resilience
- 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")
2026-03-22 02:38:36 +00:00
91669515bd fix: add per-write timeouts and reduce inter-write delay to match Android
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>
2026-03-22 02:14:32 +00:00
28aefd1bdf fix: broaden disconnect retry to cover all active provisioning phases
Previously retry logic only caught disconnects during .authenticating
and .writing states. Beacons can also drop during .discoveringServices
(characteristic discovery) and .verifying (broadcast check), which
would bypass retry and immediately hard-fail.

Now all active provisioning phases get the same reconnect retry
treatment with backoff delays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:23:01 +00:00
df6601c50b fix: add reconnect retry for unexpected disconnects during auth/write
Instead of immediately failing on disconnect during authenticating or
writing states, retry up to 2 times with backoff. Resets passwordIndex
on reconnect so re-auth starts fresh (fixes issue where burned password
attempts caused retry failures). Also fixes passwordIndex reset in the
device-info safety-net reconnect path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:54:13 +00:00
4c1c1adf85 fix: skip device info read during provisioning to prevent disconnect failures
The DX-Smart 0x30 device info query consistently causes beacon BLE disconnects,
which exhausted the retry counter and failed provisioning. The MAC address is
optional — API falls back to iBeacon UUID as hardware ID. Device info read is
still available in readConfig/check mode where it doesn't block provisioning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:50:08 +00:00
64e3684209 fix: use dedicated retry counter for device info disconnect
The device info disconnect handler was sharing connectionRetryCount with
the initial connection retry logic. If earlier connection attempts burned
through retries, the device info handler had zero retries left and
immediately hit "retries exhausted" — causing the "Disconnected while
reading device information" error John reported.

Now uses a separate deviceInfoRetryCount (max 2) so device info retries
are independent of connection retries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:35:15 +00:00
3d56a1e31d fix: auto-reconnect on disconnect during device info read instead of failing
The beacon sometimes drops BLE connection during the optional MAC address
query (0x30) after auth. Previously this failed with "Disconnected after
auth during device info read". Now we reconnect and skip the MAC read on
retry, going straight to config write. MAC is nice-to-have, not required.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:32:14 +00:00
58be00cb38 docs: fix misleading comment on post-auth disconnect path
The comment said "treat as non-fatal" but the code calls fail() — which
is correct behavior since we can't write config without a connection.
Updated comment to accurately describe the fail-with-retry-prompt flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:03:43 +00:00
54fa973d34 fix: prevent re-entrant disconnect callbacks and handle all disconnect states
- Add isTerminating flag to guard succeed()/fail() against double invocation
  from racing didWriteValueFor + didDisconnectPeripheral callbacks
- Only call cancelPeripheralConnection when peripheral.state == .connected
  (avoids triggering spurious didDisconnectPeripheral on already-disconnected peripheral)
- Handle disconnect during device info read (post-auth) with specific error message
- Include state info in unexpected disconnect errors for easier debugging
- Early-return structure in disconnect handler for clearer control flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:01:30 +00:00
c62dace54d fix: handle SaveConfig write-error path + add disconnect diagnostics
The previous fix only caught the disconnect callback path. But CoreBluetooth
can also fire didWriteValueFor with an error when the beacon reboots mid-ATT
response. This was hitting fail() at the isNonFatalCommand check instead of
being treated as success.

Now handles both paths:
1. didDisconnectPeripheral with state=.writing at last command → succeed()
2. didWriteValueFor error for SaveConfig (last command) → succeed()

Also added detailed state/index logging to disconnect handler for diagnostics.
2026-03-21 22:47:11 +00:00
e387b9ceb1 fix: handle expected BLE disconnect after SaveConfig command
The DX-Smart CP28 beacon reboots after receiving SaveConfig (0x60) to
persist config to flash. This drops the BLE connection before the write
callback fires, causing the app to report "Unexpected disconnect" even
though the config was successfully saved.

Now we check if we're on the last command (SaveConfig) when disconnect
occurs — if so, treat it as success instead of failure.

Co-Authored-By: Luna <luna@payfrit.com>
2026-03-21 22:28:47 +00:00
06b258ac18 fix: reset passwordIndex in readConfig() to prevent stale auth state
Without this reset, if provision() was called first and incremented
passwordIndex, a subsequent readConfig() call would start at the wrong
password index and potentially skip the correct password entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:31:01 +00:00
ee366870ea feat: multi-password auth, structured error codes, missing API endpoints
- DX-Smart auth now tries multiple passwords in sequence (555555, dx1234, 000000)
  instead of hardcoding a single password. Matches Android behavior for better
  compatibility across firmware versions.

- Added ProvisioningError enum with structured error codes (CONNECTION_FAILED,
  AUTH_FAILED, SERVICE_NOT_FOUND, WRITE_FAILED, etc.) matching Android's
  BeaconConfig error codes. All fail() calls now tagged with codes for better
  debugging and error reporting.

- Added ProvisioningResult.failureWithCode case and handling in ScanView.

- Added missing API endpoints that Android has:
  - getBusiness() - single business fetch
  - getBusinessName() - cached business name lookup
  - allocateServicePointMinor() - minor value allocation

- Fixed stray print() in Api.swift to use DebugLog.shared.log() for consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:31:39 +00:00
237ac38557 refactor: consolidate UUID formatting into shared String extension
Created UUIDFormatting.swift with .normalizedUUID and .uuidWithDashes
String extensions, replacing 4 duplicate formatUuidWithDashes() methods
and 6+ inline .replacingOccurrences(of: "-", with: "").uppercased() calls
across Api.swift, BeaconScanner.swift, ScanView.swift,
ServicePointListView.swift, BeaconBanList.swift, and BeaconProvisioner.swift.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:02:58 +00:00
John Pinkyfloyd
8b413020ff Fix HardwareId field name to match backend (camelCase)
Backend expects "HardwareId" not "HardwareID" or "hardware_id"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-16 17:37:40 -07:00
John Pinkyfloyd
02592ea249 Read MAC address during provisioning and use as hardware_id
- Modified BeaconProvisioner to read device info (0x30) before writing config
- Extract MAC address from beacon and return in ProvisioningResult
- Use MAC address as hardware_id field (snake_case for backend)
- Reorder scan view: Configurable Devices section now appears first
- Add debug logging for beacon registration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-16 17:35:28 -07:00
John Pinkyfloyd
2ec195243c Migrate API endpoints from CFML to PHP
- Replace all .cfm endpoints with .php (PHP backend migration)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-14 17:17:02 -07:00
John Pinkyfloyd
5283d2d265 Fix DX-Smart provisioning protocol and add debug logging
Fix critical packet format bugs matching SDK: frame select/type/trigger/disable
commands now send empty data, RSSI@1m corrected to -59 dBm. Add DebugLog,
read-config mode, service point list, and dev scheme.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:01:12 -08:00
John Pinkyfloyd
8c2320da44 Add ios-marketing idiom, iPad orientations, launch screen
- Fixed App Store icon display with ios-marketing idiom
- Added iPad orientation support for multitasking
- Added UILaunchScreen for iPad requirements
- Removed unused BLE permissions and files from build

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 19:38:11 -08:00
John Pinkyfloyd
c013c8fcd7 Update app icon
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 13:22:48 -08:00
John Mizerek
b5004c1dc7 Add BeaconShardPool for iBeacon shard UUID scanning
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 14:46:18 -08:00
John Pinkyfloyd
962a767863 Rewrite app: simplified architecture, CoreLocation beacon scanning, UI fixes
- Flatten project structure: remove Models/, Services/, ViewModels/, Views/ subdirs
- Replace APIService actor with simpler Api class, IS_DEV flag controls dev vs prod URL
- Rewrite BeaconScanner to use CoreLocation (CLBeaconRegion ranging) instead of
  CoreBluetooth — iOS blocks iBeacon data from CBCentralManager
- Add SVG logo on login page with proper scaling (was showing green square)
- Make login page scrollable, add "enter 6-digit code" OTP instruction
- Fix text input visibility (white on white) with .foregroundColor(.primary)
- Add diagonal orange DEV ribbon banner (lower-left corner), gated on Api.IS_DEV
- Update app icon: logo 10% larger, wifi icon closer
- Add en.lproj/InfoPlist.strings for display name localization
- Fix scan flash: keep isScanning=true until enrichment completes
- Add Podfile with SVGKit, Kingfisher, CocoaLumberjack dependencies

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:07:39 -08:00
John Pinkyfloyd
4faec5499d Initial commit: Payfrit Beacon iOS native app 2026-02-01 23:39:29 -08:00