# 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: 1. Scan for nearby BLE iBeacons 2. Check UUID against known bulk manufacturer defaults (ban list) 3. Assign table names to beacons (smart-incremented) 4. 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 `DEV` compiler 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 1. DeviceName (0x71) → Frame1 select (0x11) → Frame1 type (0x61) → Frame1 RSSI/AdvInt/TxPow 2. Frame2 select (0x12) → Frame2 iBeacon (0x62) → UUID/Major/Minor/RSSI/AdvInt/TxPow 3. TriggerOff (0xA0) 4. Disable frames 3-6 (select 0x13-0x16 + 0xFF for each) 5. 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 sample - `B9407F30` — Estimote default - `FDA50693` — Generic Chinese bulk default - `F7826DA6` — Kontakt.io default - `74278BDA` — Generic bulk default - `00000000` / `FFFFFFFF` — Unconfigured hardware ## Dependencies - SwiftUI + Combine - CoreBluetooth (BLE GATT operations) - CoreLocation (CLBeaconRegion for iBeacon ranging) - Security (Keychain for auth token storage) ## App Flow 1. **Login** → OTP (Keychain-saved token for re-auth) 2. **Select Business** → Auto-select if one, list if multiple 3. **Select Service Point** → Table assignment target 4. **Scan** → BLE scan → show results with status badges 5. **Provision** → Connect → Auth → Write 24-step config → Save 6. **Register** → API call to register hardware 7. **Repeat** or **Done**