diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..60fd29b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,101 @@ +# 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 (iBeacon + BLE + QR code) +2. Check UUID against known bulk manufacturer defaults (ban list) +3. Assign table names to beacons (smart-incremented) +4. Provision beacon hardware via BLE GATT (DX-Smart, BlueCharm, KBeacon) +5. Save beacon + auto-create service point records via API + +## Environment + +- **Server**: dev.payfrit.com (DEBUG) / biz.payfrit.com (RELEASE) +- **API Base**: `https://dev.payfrit.com/api` (dev) / `https://biz.payfrit.com/api` (prod) +- **Config**: `APIConfig.swift` — uses `#if DEBUG` compiler flag +- **OTP**: Dev server uses magic OTP `123456` (no Twilio) + +## Database (via API — no direct SQL) + +| 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 + +All endpoints are `.php`. Auth = `X-User-Token` header. + +| 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 | /servicepoints/list.php | Yes | List service points for business | +| POST | /servicepoints/save.php | Yes | Create/update service point | +| POST | /servicepoints/delete.php | Yes | Delete 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/allocate_servicepoint_minor.php | Yes | Auto-assign minor value | +| POST | /beacon-sharding/resolve_business.php | Yes | Resolve business by UUID+Major | +| POST | /beacons/list.php | Yes | List beacons for a business | +| POST | /beacons/lookupByMac.php | Yes | Lookup beacon by MAC address | +| POST | /beacons/wipe.php | Yes | Decommission a beacon | +| GET | /users/profile.php | Yes | Get user profile | + +## Build & Deploy + +Build in Xcode targeting iOS 17+. No local testing — deploy to device for BLE testing. + +## Project Structure + +``` +PayfritBeacon/ +├── App/ +│ ├── AppPrefs.swift UserDefaults keys +│ ├── AppState.swift ObservableObject — auth state, nav, business selection +│ └── PayfritBeaconApp.swift App entry point +├── Models/ +│ ├── BeaconConfig.swift Provisioning config (UUID, major, minor, txPower, etc.) +│ ├── BeaconType.swift Enum: DXSmart, BlueCharm, KBeacon, Unknown +│ ├── Business.swift Business model +│ └── ServicePoint.swift Service point model +├── Provisioners/ +│ ├── ProvisionerProtocol.swift Protocol — all provisioners implement this +│ ├── DXSmartProvisioner.swift DX-Smart CP28 GATT provisioner (24-step write sequence) +│ ├── BlueCharmProvisioner.swift BlueCharm BC037 provisioner +│ ├── KBeaconProvisioner.swift KBeacon provisioner +│ ├── FallbackProvisioner.swift Unknown device fallback +│ └── ProvisionError.swift Shared error types +├── Services/ +│ ├── APIClient.swift Actor-based REST client, all API calls +│ ├── APIConfig.swift Base URLs, timeouts, dev/prod toggle +│ ├── BLEManager.swift CoreBluetooth scanning + beacon type detection +│ └── SecureStorage.swift Keychain-based token storage +├── Utils/ +│ ├── BeaconBanList.swift Known bad UUID prefixes (factory defaults) +│ ├── BeaconShardPool.swift 64 Payfrit shard UUIDs +│ └── UUIDFormatting.swift UUID string formatting extensions +└── Views/ + ├── BusinessListView.swift Business selection screen + ├── DevBanner.swift Orange "DEV" banner overlay + ├── LoginView.swift OTP login screen + ├── QRScannerView.swift Camera-based QR code scanner for beacon pairing + ├── RootView.swift Root navigation + └── ScanView.swift Main scan + provision UI +``` + +## Key Architecture Notes + +- **Modular provisioners**: Each beacon manufacturer has its own provisioner conforming to `ProvisionerProtocol`. No more monolithic `BeaconProvisioner.swift`. +- **Actor-based API**: `APIClient` is a Swift actor (thread-safe by design). +- **Secure storage**: Auth tokens stored in Keychain via `SecureStorage`, not UserDefaults. +- **BLE scanning**: `BLEManager` handles CoreBluetooth device discovery and beacon type identification. +- **DX-Smart protocol**: 24-step write sequence including frames 3-6 disable (full Android parity). +- **QR scanner**: Camera-based QR code scanning for beacon pairing workflows. diff --git a/PayfritBeacon.xcodeproj/project.pbxproj b/PayfritBeacon.xcodeproj/project.pbxproj index cc5c909..47f8381 100644 --- a/PayfritBeacon.xcodeproj/project.pbxproj +++ b/PayfritBeacon.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 103E16DE41E63F1AD9A7BAEC /* UUIDFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B4971261F86B2A4D7579277 /* UUIDFormatting.swift */; }; 281CC856DD918C4CFA00EB67 /* ServicePointListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1775119CBC98A753AE26D84 /* ServicePointListView.swift */; }; - 7D757E1341A0143A2E9EBDF4 /* Pods_PayfritBeacon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F6819A0F2BD2E9D84E7EDB4 /* Pods_PayfritBeacon.framework */; }; D01000000001 /* PayfritBeaconApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000001 /* PayfritBeaconApp.swift */; }; D01000000002 /* Api.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000002 /* Api.swift */; }; D01000000003 /* BeaconBanList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000003 /* BeaconBanList.swift */; }; @@ -30,13 +29,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 1AD023D1003AAD57ED3DBEAA /* Pods-PayfritBeacon.debug-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PayfritBeacon.debug-dev.xcconfig"; path = "Target Support Files/Pods-PayfritBeacon/Pods-PayfritBeacon.debug-dev.xcconfig"; sourceTree = ""; }; 2B4971261F86B2A4D7579277 /* UUIDFormatting.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UUIDFormatting.swift; sourceTree = ""; }; - 7F6819A0F2BD2E9D84E7EDB4 /* Pods_PayfritBeacon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PayfritBeacon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8710F334FDFE10F0625EB86D /* BLEBeaconScanner.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BLEBeaconScanner.swift; sourceTree = ""; }; 964B63D1857877BBEE73F1D1 /* BeaconShardPool.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BeaconShardPool.swift; sourceTree = ""; }; - AFF542870BA2862FAB429A86 /* Pods-PayfritBeacon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PayfritBeacon.debug.xcconfig"; path = "Target Support Files/Pods-PayfritBeacon/Pods-PayfritBeacon.debug.xcconfig"; sourceTree = ""; }; - B22C79E28AA521E6347E7F93 /* Pods-PayfritBeacon.release-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PayfritBeacon.release-dev.xcconfig"; path = "Target Support Files/Pods-PayfritBeacon/Pods-PayfritBeacon.release-dev.xcconfig"; sourceTree = ""; }; C03000000001 /* PayfritBeacon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PayfritBeacon.app; sourceTree = BUILT_PRODUCTS_DIR; }; C7C275738594E225BE7D5740 /* BeaconProvisioner.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BeaconProvisioner.swift; sourceTree = ""; }; D02000000001 /* PayfritBeaconApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayfritBeaconApp.swift; sourceTree = ""; }; @@ -55,7 +50,6 @@ D02000000081 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; E1775119CBC98A753AE26D84 /* ServicePointListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ServicePointListView.swift; sourceTree = ""; }; F100AA6D1E41596FCB1C1A39 /* DebugLog.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DebugLog.swift; path = PayfritBeacon/DebugLog.swift; sourceTree = ""; }; - F1B84CA677516C50F6BE2294 /* Pods-PayfritBeacon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PayfritBeacon.release.xcconfig"; path = "Target Support Files/Pods-PayfritBeacon/Pods-PayfritBeacon.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -63,31 +57,17 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7D757E1341A0143A2E9EBDF4 /* Pods_PayfritBeacon.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 04996117E2F5D5BB2D86CD46 /* Pods */ = { - isa = PBXGroup; - children = ( - AFF542870BA2862FAB429A86 /* Pods-PayfritBeacon.debug.xcconfig */, - F1B84CA677516C50F6BE2294 /* Pods-PayfritBeacon.release.xcconfig */, - 1AD023D1003AAD57ED3DBEAA /* Pods-PayfritBeacon.debug-dev.xcconfig */, - B22C79E28AA521E6347E7F93 /* Pods-PayfritBeacon.release-dev.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; C05000000001 = { isa = PBXGroup; children = ( D05000000002 /* PayfritBeacon */, C05000000009 /* Products */, - 04996117E2F5D5BB2D86CD46 /* Pods */, - EEC06FED6BE78CF9357F3158 /* Frameworks */, F100AA6D1E41596FCB1C1A39 /* DebugLog.swift */, ); sourceTree = ""; @@ -126,14 +106,6 @@ path = PayfritBeacon; sourceTree = ""; }; - EEC06FED6BE78CF9357F3158 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 7F6819A0F2BD2E9D84E7EDB4 /* Pods_PayfritBeacon.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -141,11 +113,9 @@ isa = PBXNativeTarget; buildConfigurationList = C08000000003 /* Build configuration list for PBXNativeTarget "PayfritBeacon" */; buildPhases = ( - 744B96DEA84C89E13D29B8B7 /* [CP] Check Pods Manifest.lock */, C07000000001 /* Sources */, C04000000001 /* Frameworks */, C09000000001 /* Resources */, - 66702B40BAEAF5430876D7CE /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -202,47 +172,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 66702B40BAEAF5430876D7CE /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-PayfritBeacon/Pods-PayfritBeacon-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-PayfritBeacon/Pods-PayfritBeacon-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PayfritBeacon/Pods-PayfritBeacon-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 744B96DEA84C89E13D29B8B7 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-PayfritBeacon-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ C07000000001 /* Sources */ = { @@ -284,7 +213,6 @@ /* Begin XCBuildConfiguration section */ 064DDD2A9238EC6900250593 /* Release-Dev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B22C79E28AA521E6347E7F93 /* Pods-PayfritBeacon.release-dev.xcconfig */; buildSettings = { APP_DISPLAY_NAME = "Payfrit Beacon BETA"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -436,7 +364,6 @@ }; B0D496FEA252D8DDA33F57A0 /* Debug-Dev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 1AD023D1003AAD57ED3DBEAA /* Pods-PayfritBeacon.debug-dev.xcconfig */; buildSettings = { APP_DISPLAY_NAME = "Payfrit Beacon BETA"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -588,7 +515,6 @@ }; C0B000000003 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = AFF542870BA2862FAB429A86 /* Pods-PayfritBeacon.debug.xcconfig */; buildSettings = { APP_DISPLAY_NAME = "Payfrit Beacon"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -623,7 +549,6 @@ }; C0B000000004 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F1B84CA677516C50F6BE2294 /* Pods-PayfritBeacon.release.xcconfig */; buildSettings = { APP_DISPLAY_NAME = "Payfrit Beacon"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;