- Fix BeaconShardPool: replace all 64 UUIDs to match Android + migration.sql (iOS had a completely different set of UUIDs — would cause shard resolution failures) - Add frames 3-6 disable to write sequence (16 → 24 steps, matches Android's DXSmartProvisioner.writeBeaconConfig() exactly) - Add CLAUDE.md with full project docs, API endpoints, protocol reference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6 KiB
6 KiB
Payfrit Beacon iOS
Native Swift iOS app for provisioning iBeacon hardware at restaurant tables. Repo: payfrit-beacon-ios on Forgejo.
Purpose
Utility app for restaurant setup staff to:
- Scan for nearby BLE iBeacons
- Check UUID against known bulk manufacturer defaults (ban list)
- Assign table names to beacons (smart-incremented)
- Save beacon + auto-create service point records via API
Environment
- Server: dev.payfrit.com (always dev — this is a setup tool)
- API Base:
https://dev.payfrit.com/api - IS_DEV: Driven by
DEVcompiler flag (orange DEV banner on all screens) - OTP: Dev server uses magic OTP (no Twilio)
Database (dev schema — migrated Jan 2026)
Connected via API, not direct SQL. Dev DB uses clean, unprefixed names. PKs are always ID. FKs reference their parent table.
| Table | PK | Key Columns |
|---|---|---|
| Beacons | ID |
UUID, Name, BusinessID, IsActive |
| ServicePoints | ID |
BusinessID, Name, TypeID, Code, SortOrder, IsActive, BeaconID |
| Businesses | ID |
Name, ParentBusinessID |
Note: POST /api/beacons/save.php auto-creates a ServicePoint when saving a beacon.
API Endpoints Used
| Method | Endpoint | Auth | Purpose |
|---|---|---|---|
| POST | /auth/loginOTP.php | No | Send OTP to phone |
| POST | /auth/verifyLoginOTP.php | No | Verify OTP, get token |
| POST | /businesses/list.php | Yes | List user's businesses |
| POST | /businesses/get.php | Yes | Get single business |
| POST | /servicepoints/list.php | Yes | List service points for business |
| POST | /servicepoints/save.php | Yes | Create/update service point |
| POST | /beacon-sharding/allocate_business_namespace.php | Yes | Allocate UUID+Major shard |
| POST | /beacon-sharding/get_beacon_config.php | Yes | Get complete beacon config |
| POST | /beacon-sharding/register_beacon_hardware.php | Yes | Register provisioned device |
| POST | /beacon-sharding/verify_beacon_broadcast.php | Yes | Verify beacon is broadcasting |
| POST | /beacon-sharding/resolve_business.php | Yes | Resolve business by UUID+Major |
| POST | /beacon-sharding/allocate_servicepoint_minor.php | Yes | Auto-assign minor value |
| GET | /beacons/list_all.php | No | All active beacons (UUID→ID map) |
| POST | /beacons/lookup.php | No | Lookup beacon assignments by UUID |
| POST | /beacons/lookupByMac.php | No | Lookup beacon by MAC address |
| POST | /beacons/list.php | Yes | List beacons for a business |
| POST | /beacons/save.php | Yes | Create/update beacon + auto-create service point |
| POST | /beacons/wipe.php | Yes | Wipe/deactivate a beacon |
Build & Deploy
Build in Xcode targeting iOS 17+. No local testing — deploy to device for BLE testing.
Project Structure
PayfritBeacon/
├── Api.swift REST client, all API calls, auth token mgmt
├── BeaconBanList.swift Known bad UUID prefixes (factory defaults)
├── BeaconProvisioner.swift DX-Smart CP28 GATT provisioner (24-step write sequence)
├── BeaconScanner.swift iBeacon CLLocationManager scanner
├── BeaconShardPool.swift 64 Payfrit shard UUIDs (matches Android + migration.sql)
├── BLEBeaconScanner.swift CoreBluetooth BLE scanner + BeaconType enum
├── BusinessListView.swift Business selection screen
├── DebugLog.swift Shared logging singleton
├── DevBanner.swift Orange "DEV" banner overlay
├── LoginView.swift OTP login screen
├── PayfritBeaconApp.swift App entry point
├── RootView.swift Root navigation
├── ScanView.swift Main provisioning hub
├── ServicePointListView.swift Service point list + beacon assignment
└── UUIDFormatting.swift UUID string formatting extensions
DX-Smart CP28 Provisioning Protocol
BLE Service & Characteristics
- Service:
0000FFE0-0000-1000-8000-00805F9B34FB - FFE1 (Notify): Response notifications (RX)
- FFE2 (Write): Command TX
- FFE3 (Write): Password authentication
Packet Format
[4E][4F][CMD][LEN][DATA...][XOR_CHECKSUM]
Header: 4E 4F (fixed). Checksum: XOR of CMD ⊕ LEN ⊕ DATA bytes.
Command Codes
| Code | Purpose |
|---|---|
| 0x11-0x16 | Select frame 1-6 |
| 0x60 | Save config to flash |
| 0x61 | Set frame as device info |
| 0x62 | Set frame as iBeacon |
| 0x71 | Write device name (max 20 ASCII) |
| 0x74 | Write UUID (16 bytes BE) |
| 0x75 | Write Major (2 bytes BE) |
| 0x76 | Write Minor (2 bytes BE) |
| 0x77 | Write RSSI@1m (1 byte signed) |
| 0x78 | Write adv interval (1 byte, x100ms) |
| 0x79 | Write TX power (1 byte, 0-7 index) |
| 0xA0 | Trigger off |
| 0xFF | Disable frame (no data) |
24-Step Write Sequence
- DeviceName (0x71) → Frame1 select (0x11) → Frame1 type (0x61) → Frame1 RSSI/AdvInt/TxPow
- Frame2 select (0x12) → Frame2 iBeacon (0x62) → UUID/Major/Minor/RSSI/AdvInt/TxPow
- TriggerOff (0xA0)
- Disable frames 3-6 (select 0x13-0x16 + 0xFF for each)
- SaveConfig (0x60)
Authentication
Passwords tried in order: 555555, dx1234, 000000. Written to FFE3 with .withResponse.
Ban List UUIDs
Hardcoded in BeaconBanList.swift. Known factory-default prefixes:
E2C56DB5— Apple AirLocate sampleB9407F30— Estimote defaultFDA50693— Generic Chinese bulk defaultF7826DA6— Kontakt.io default74278BDA— Generic bulk default00000000/FFFFFFFF— Unconfigured hardware
Dependencies
- SwiftUI + Combine
- CoreBluetooth (BLE GATT operations)
- CoreLocation (CLBeaconRegion for iBeacon ranging)
- Security (Keychain for auth token storage)
App Flow
- Login → OTP (Keychain-saved token for re-auth)
- Select Business → Auto-select if one, list if multiple
- Select Service Point → Table assignment target
- Scan → BLE scan → show results with status badges
- Provision → Connect → Auth → Write 24-step config → Save
- Register → API call to register hardware
- Repeat or Done